Files
redmine/docs/contact_triage_loop_workflow.md
Jason Thistlethwaite 9ee9eb3ec6 Organized
2026-10-17 20:50:35 -04:00

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.

  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

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 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:

./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.