Addresses the triage-quality problems the user observed:
- Candidates getting wrong project/product attribution
- Stale facts promoted as if still true
- "Hard to decide" items reaching human queue without real value
Solution: let sonnet handle the easy 80%, escalate borderline cases
to opus, auto-discard (or flag) what two models can't resolve.
Plus enrich the context the triage model sees so it can catch
misattribution, contradictions, and temporal drift earlier.
THE 3-TIER FLOW (scripts/auto_triage.py):
Tier 1: sonnet (fast, cheap)
- confidence >= 0.8 + clear verdict → PROMOTE or REJECT (done)
- otherwise → escalate to tier 2
Tier 2: opus (smarter, sees tier-1 verdict + reasoning)
- second opinion with explicit "sonnet said X, resolve the uncertainty"
- confidence >= 0.8 → PROMOTE or REJECT with note="[opus]"
- still uncertain → tier 3
Tier 3: configurable (default discard)
- ATOCORE_TRIAGE_TIER3=discard (default): auto-reject with
"two models couldn't decide" reason
- ATOCORE_TRIAGE_TIER3=human: leave in queue for /admin/triage
Configuration via env vars:
ATOCORE_TRIAGE_MODEL_TIER1 (default sonnet)
ATOCORE_TRIAGE_MODEL_TIER2 (default opus)
ATOCORE_TRIAGE_TIER3 (default discard)
ATOCORE_TRIAGE_ESCALATION_THRESHOLD (default 0.75)
ATOCORE_TRIAGE_TIER2_TIMEOUT_S (default 120 — opus is slower)
ENRICHED CONTEXT shown to the triage model (both tiers):
- List of registered project ids so misattribution is detectable
- Trusted project state entries (ground truth, higher trust than memories)
- Top 30 active memories for the claimed project (was 20)
- Tier 2 additionally sees tier 1's verdict + reason
PROJECT MISATTRIBUTION DETECTION:
- Triage prompt asks the model to output "suggested_project" when it
detects the claimed project is wrong but the content clearly belongs
to a registered one
- Main loop auto-applies the fix via PUT /memory/{id} (which canonicalizes
through the registry)
- Misattribution is the #1 pollution source — this catches it upstream
TEMPORAL AGGRESSIVENESS:
- Prompt upgraded: "be aggressive with valid_until for anything that
reads like 'current state' or 'this week'. When in doubt, 2-4 week
expiry rather than null."
- Stale facts decay automatically via Phase 3's expiry filter
CONFIDENCE GRADING (new in prompt):
- 0.9+: crystal clear durable fact or clear noise
- 0.75-0.9: confident but not cryptographic-certain
- 0.6-0.75: borderline — WILL escalate
- <0.6: genuinely ambiguous — human or discard
Tests: 356 → 366 (10 new, all in test_triage_escalation.py):
- High-confidence tier-1 promote/reject → no tier-2 call
- Low-confidence tier-1 → tier-2 escalates → decides
- needs_human always escalates regardless of confidence
- tier-2 uncertain → discard by default
- tier-2 uncertain → human when configured
- dry-run skips all API calls
- suggested_project flag surfaces + gets printed
- parse_verdict captures suggested_project
Runtime behavior unchanged for the clear cases (sonnet still handles
them). The 20-30% of candidates that currently land as needs_human
will now route through opus, and only the genuinely stuck get a human
(or discard) action.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>