fix(lifecycle): research_required nonterminal disposition + recall A/B honesty + hook hardening

Pre-PL1 closure (handoff 2026-09-10):

1. research_required: new nonterminal task status (records enum, state
   machine edges created/running -> research_required -> running/
   completed/failed/cancelled/superseded). The operator's RESEARCHED_CHANGE
   disposition now transitions to research_required instead of failed —
   research routing is not failure. Live canary: status=research_required,
   outcome_ref=research_dispatch, route_ref set.

2. Experience consumer skips research-dispatch tasks — no false
   failure-learning evidence in PL0 (verified: 0 FAILURE records for
   research tasks).

3. recall_uplift A/B documented honestly in the module: latency 521-1213ms
   → 14-18ms; terms bounded (893-1665 → 16-21); recall ROW COUNTS identical
   per query but 1/8-7/7 of recalled IDs differ (expansion terms differ →
   different lexical candidates). Marked PROVISIONAL — quality not proven
   equivalent; harness original retained for rollback.

4. Hook hardening: durable fire-trace (/tmp/rune-bridge-fire.log, one line
   per actual injection with token count) + harness-internal-step guard
   (checkpoint-writer/compaction steps no longer receive capsules).

5. QX7 canary fully purged: entries, contentless-FTS ghosts, both stores;
   retrieval-level verification shows zero hits.

1227 tests pass; ratchet 353/80; henxels clean.
This commit is contained in:
Nick committed 2026-09-10 20:08:50 +01:00
1 parent 3dfb4ee9e6
commit bf5eb53d1e
8 files changed
+120 -32

No files matched your search

+5
View File
@@ -158,6 +158,11 @@ def record_operator_outcomes(task_state_path: str | None = None, ledger_path: st
status = t.get("status", "")
if status not in ("completed", "failed"):
continue
# research_required tasks: routing disposition, NOT failure evidence
# (handoff §2/§C). The dispatch lives in route.jsonl; recording it
# as FAILURE would poison PL0 with false failure-learning pressure.
if t.get("outcome_ref") == "research_dispatch" or t.get("result") == "RESEARCH_REQUIRED":
continue
task_id = t.get("task_id", "")
if not task_id or task_id in seen_tasks:
continue