feat: Phase 7A.1 — autonomous merge tiering (sonnet → opus → human)

Dedup detector now merges high-confidence duplicates silently instead
of piling every proposal into a human triage queue. Matches the 3-tier
escalation pattern that auto_triage already uses.

Tiering decision per cluster:
  TIER-1 auto-approve: sonnet confidence >= 0.8 AND min_pairwise_sim >= 0.92
                       AND all sources share project+type → auto-merge silently
                       (actor="auto-dedup-tier1" in audit log)
  TIER-2 escalation:   sonnet 0.5-0.8 conf OR sim 0.85-0.92 → opus second opinion.
                       Opus confirms with conf >= 0.8 → auto-merge (actor="auto-dedup-tier2").
                       Opus overrides (reject) → skip silently.
                       Opus low conf → human triage with opus's refined draft.
  HUMAN triage:        Only the genuinely ambiguous land in /admin/triage.

Env-tunable thresholds:
  ATOCORE_DEDUP_AUTO_APPROVE_CONF (0.8)
  ATOCORE_DEDUP_AUTO_APPROVE_SIM (0.92)
  ATOCORE_DEDUP_TIER2_MIN_CONF (0.5)
  ATOCORE_DEDUP_TIER2_MIN_SIM (0.85)
  ATOCORE_DEDUP_TIER2_MODEL (opus)

New flag --no-auto-approve for kill-switch testing (everything → human queue).

Tests: +6 (tier-2 prompt content, same_bucket edges, min_pairwise_similarity
on identical + transitive clusters). 395 → 401.

Rationale: user asked for autonomous behavior — "this needs to be intelligent,
I don't want to manually triage stuff". Matches the consolidation principle:
never discard details, but let the brain tidy up on its own for the easy cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 15:46:26 -04:00
parent 028d4c3594
commit 56d5df0ab4
3 changed files with 374 additions and 29 deletions

View File

@@ -53,6 +53,50 @@ OUTPUT — raw JSON, no prose, no markdown fences:
On action=reject, still fill content with a short explanation and set confidence=0."""
TIER2_SYSTEM_PROMPT = """You are the second-opinion reviewer for AtoCore's memory-consolidation pipeline.
A tier-1 model (cheaper, faster) already drafted a unified memory from N near-duplicate source memories. Your job is to either CONFIRM the merge (refining the content if you see a clearer phrasing) or OVERRIDE with action="reject" if the tier-1 missed something important.
You must be STRICTER than tier-1. Specifically, REJECT if:
- The sources are about different subjects that share vocabulary (e.g., different components within the same project)
- The tier-1 draft dropped specifics that existed in the sources (numbers, dates, vendors, people, part IDs)
- One source contradicts another and the draft glossed over it
- The sources span a timeline of a changing state (should be preserved as a sequence, not collapsed)
If you CONFIRM, you may polish the content — but preserve every specific from every source.
Same output schema as tier-1:
{
"action": "merge" | "reject",
"content": "the unified memory content",
"memory_type": "knowledge|project|preference|adaptation|episodic|identity",
"project": "project-slug or empty",
"domain_tags": ["tag1", "tag2"],
"confidence": 0.5,
"reason": "one sentence — what you confirmed or why you overrode"
}
Raw JSON only, no prose, no markdown fences."""
def build_tier2_user_message(sources: list[dict[str, Any]], tier1_verdict: dict[str, Any]) -> str:
"""Format tier-2 review payload: same sources + tier-1's draft."""
base = build_user_message(sources)
draft_summary = (
f"\n\n--- TIER-1 DRAFT (for your review) ---\n"
f"action: {tier1_verdict.get('action')}\n"
f"confidence: {tier1_verdict.get('confidence', 0):.2f}\n"
f"proposed content: {(tier1_verdict.get('content') or '')[:600]}\n"
f"proposed memory_type: {tier1_verdict.get('memory_type', '')}\n"
f"proposed project: {tier1_verdict.get('project', '')}\n"
f"proposed tags: {tier1_verdict.get('domain_tags', [])}\n"
f"tier-1 reason: {tier1_verdict.get('reason', '')[:300]}\n"
f"---\n\n"
f"Return your JSON verdict now. Confirm or override."
)
return base.replace("Return the JSON object now.", "").rstrip() + draft_summary
def build_user_message(sources: list[dict[str, Any]]) -> str:
"""Format N source memories for the model to consolidate.