7.7 KiB
Contact Triage Loop Workflow (Vision + Operating Pattern)
Why this exists
This document captures Jason's intended repeatable contact-review loop so work can proceed in deterministic batches without reprocessing the same contacts immediately.
Captured from uploaded project note on 2026-07-04 (saved into repo context to avoid losing the design intent).
Raw source capture:
/home/jason/projects/redmine/docs/contact_triage_loop_raw_notes_2026-07-04.md
Core loop pattern
This is a multi-pass routing system, not a final one-shot classifier.
- Select a small batch (10-20 contacts) where
updated_onis old enough and optional tag constraints match. - Sort newest stale first (recently stale / last-updated descending), so the loop works through the freshest cooled-down contacts first.
- For each contact:
- quick contact-only triage
- inspect recent helpdesk tickets/messages when the current pass requires it
- route by adding/removing tags that represent either classifications or workflow state
- Apply tag updates:
- if the pass identifies a route, add that route/classification tag
- otherwise add a status marker like
ai-checked
- Because contact metadata was updated,
updated_onadvances. - On future runs, recently processed contacts fall out of the immediate stale batch; later passes can pick them up again when stale or when a designated tag routes them to another worker.
- Later filters/workers may deliberately remove status tags such as
ai-checkedafter consuming them, allowing the contact to re-enter another pass of the conveyor belt.
This forms a self-advancing conveyor belt for incremental cleanup/enrichment. Tags are not all semantic classifications: some are routing/status markers. For example, ai-checked means this pass touched the contact; it does not mean the contact's final identity is "AI checked." Likewise, spam/promotions can be step 1 in a longer handling pipeline, not the final disposition.
Operational objective: reducing clutter in the customer-service/sales channel is valid even when a sender is technically a vendor, billing system, automailer, or promotional sender, because Redmine customer-service is not the only channel available to them.
Existing tooling alignment (current)
redmine_contacts.py already supports key building blocks:
- batch candidate selection:
batch-update --max-contacts- date/tag filters (
--updated-before,--with-tag,--without-tag,--no-tags) - deterministic ordering (
--order newest-updated-firstetc)
- safe mutation workflow:
- dry-run by default
- write only with
--apply
What is still missing for full loop automation:
- richer FILTER-2 workers that classify contacts and mutate tickets/contacts when needed
- scoring engines that map evidence to tags with thresholds
- downstream queue workers for enrichment/NMI/company/person linking
Implemented tracer pieces:
redmine_contacts.py loop-run- profile contract in
docs/contact_loop_filter_contract.md - sample tracer profile:
profiles/contact-loop/noop-ai-checked.json - sample no-op FILTER-2 worker:
profiles/contact-loop/filters/noop_filter2.py
The intended architecture is now: runner owns conveyor-belt mechanics; FILTER-2 owns domain-specific ticket/contact mutation logic.
Suggested scoring framework (v0)
customer
Signals:
- internal/team response exists (
+1) - discussion indicates warehouse packages/items (
+1)
promo + spam
Signals:
- sender hints (
no-reply,noreply,deals,promotions) (+1) - message mentions discount/coupon/deal language (
+1)
Threshold model (initial):
- tag set applied when score >=
2 - if no tag set crosses threshold: add
ai-checked
Diagrams
Diagram 1 — Conveyor-belt loop
flowchart TD
A[Candidate pool\nRedmine contacts] --> B[Select stale contacts\nupdated_before + tag filters\nmax 10-20]
B --> C[Sort newest stale first\nupdated_on descending]
C --> D{For each contact}
D --> E[Run current pass\nFILTER-1 / worker gate]
E -->|pass can route immediately| F[Add route/status/classification tags]
E -->|needs evidence| G[Fetch recent tickets/messages]
G --> H[Manual review or FILTER-2 worker]
H --> I{Route decided?}
I -->|yes| F
I -->|no| J[Add status marker\nai-checked]
F --> K[Update contact/tickets as allowed]
J --> K
K --> L[updated_on advances\ncontact leaves immediate batch]
L --> M[Later passes use staleness + tags\nto route next work]
M --> N[Later worker may remove status tag\ne.g. ai-checked]
N --> B
Diagram 2 — Tag/status re-entry lifecycle
stateDiagram-v2
[*] --> Eligible: updated_on older than cutoff\nand tag filters match
Eligible --> InCurrentPass: newest-stale-first selection
InCurrentPass --> Routed: worker adds route tag\nspam/promotions/etc.
InCurrentPass --> Checked: no route yet\nadd ai-checked
Routed --> CoolingDown: tag update bumps updated_on
Checked --> CoolingDown: status tag update bumps updated_on
CoolingDown --> HeldOut: too fresh for current cutoff
HeldOut --> Eligible: time passes / becomes stale again
Checked --> DownstreamQueued: later worker selects ai-checked\nor another status tag
DownstreamQueued --> ReEntered: worker removes ai-checked\nafter consuming status marker
ReEntered --> Eligible: tag filters match next pass
Future queue/tag orchestration pattern
The long-term architecture should treat tags as explicit work queues, for example:
needs-summary-> summary worker processes recent communications, then removes tagneeds-enrichment:*-> enrichment workers (social/entity/profile data)needs-nmi-mapping-> account-linkage worker for NMI mapping tasksspecial-handling:*-> SLA/VIP/high-risk handling workflows
This lets one loop classify and route, while specialized workers execute downstream tasks.
Proposed implementation phases
Phase 1 (implemented tracer)
- Add
loop-runcommand that performs a per-contact review loop in dry-run mode:- select candidates
- run FILTER-1
- fetch recent tickets/messages
- call a profile-defined FILTER-2 command
- print proposed runner-owned tag updates
Phase 2
- Replace no-op FILTER-2 with a classifier/mutator worker.
- Use FILTER-2 to update contacts/tickets directly through helper CLI calls.
- Keep runner-owned fallback
ai-checkedand RTAG removal semantics.
Phase 3
- Add queue tags + worker handoff contracts (
needs-summary,needs-nmi-mapping, etc.).
Phase 4
- Integrate external systems (NMI mapping tables, obsidian profile updates, comm summaries, etc.) with strict scoped guards.
Practical run pattern (current CLI)
Dry-run stale-first loop tracer:
./redmine_contacts.py \
--base-url http://192.168.50.170 \
--project customer-service \
loop-run \
--profile profiles/contact-loop/noop-ai-checked.json \
--source cache \
--updated-before 2026-06-01 \
--without-tag ai-checked \
--max-contacts 10 \
--order newest-updated-first
Apply runner-owned fallback/default tags after reviewing output:
./redmine_contacts.py \
--base-url http://192.168.50.170 \
--project customer-service \
loop-run \
--profile profiles/contact-loop/noop-ai-checked.json \
--source cache \
--updated-before 2026-06-01 \
--without-tag ai-checked \
--max-contacts 10 \
--order newest-updated-first \
--apply
Older dry-run batch-update skeleton:
./redmine_contacts.py \
--base-url http://192.168.50.170 \
--project customer-service \
batch-update \
--source cache \
--updated-before 2026-06-01 \
--with-tag customer \
--max-contacts 20 \
--order newest-updated-first \
--set background=ai-checked
Replace payload fields/tags only after reviewing dry-run output.