# 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. 1. Select a small batch (10-20 contacts) where `updated_on` is old enough and optional tag constraints match. 2. Sort **newest stale first** (recently stale / last-updated descending), so the loop works through the freshest cooled-down contacts first. 3. 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 4. Apply tag updates: - if the pass identifies a route, add that route/classification tag - otherwise add a status marker like `ai-checked` 5. Because contact metadata was updated, `updated_on` advances. 6. 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. 7. Later filters/workers may deliberately remove status tags such as `ai-checked` after 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-first` etc) - 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 ```mermaid 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 ```mermaid 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 tag - `needs-enrichment:*` -> enrichment workers (social/entity/profile data) - `needs-nmi-mapping` -> account-linkage worker for NMI mapping tasks - `special-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-run` command 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-checked` and 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: ```bash ./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: ```bash ./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: ```bash ./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.