fix(handoff-search): stop spending the top hit on the doc the session just read - #1399
Conversation
… just read MEASURED over the 20 post-fix `/resume` runs that queried the corpus (2026-09-08): 23 of 60 hit slots were the session's OWN handoff — the document it had read minutes earlier in step 3 — and it was the #1 hit in 13 of 20 queries, taking all three slots twice. That is arithmetic, not a ranker bug. Step 4 tells the caller to query the handoff's TOPIC, and the best text match for a document's topic is that document. The re-keying that made the step unconditional in #1332 is the same thing that aimed it at itself. It matters because YIELD was answered in the same pass and it is 1 of 20: one session had a hit change what it did (it dropped the backlog it was resuming for an unclaimed forcing:security item in another effort, five hours later, off a hit it had carried in context). The other 19 got nothing — and a third of what they were shown, they had already read. --exclude-slug SLUG|PATH (repeatable) Takes the doc path `resume-state.sh` printed on its `handoff:` line, or a bare slug; a path is normalised through the indexer's own `slug_for`, never a second hand-rolled strip. `/resume` step 4's fence now passes it. It is a SCOPE filter, not a post-hoc drop of rows the query returned: * it goes through `_selected` / `_filter_predicates`, the single predicate both `stats` and `search` read, so the counter and the query cannot come to different views of what is in scope; * excluding everything is therefore `empty-scope` (rc 4) — "your filter emptied the corpus" — never `no-match` (rc 0), which would assert the corpus is silent about a topic it was never allowed to answer on; * it is NEVER silent: `excluded=` prints on the scope line of every status including the zeros, and `in_scope_*` appears beside `indexed_*`. Also corrects "How to verify" check 3, which could not have worked as written: the DB path needs `nix develop`, not just `KUBECONFIG` — a bare python3 dies with `psycopg2 is required`. Coverage: 13 new tests. An 8-mutant sweep (positive control included) was run under PYTHONDONTWRITEBYTECODE=1 with each mutant verified PRESENT in the file before scoring; all 8 died, each by its own named test. The two that matter most are the silent ones — scoped stats blind to the exclusion (turns "your filter emptied it" into "the corpus is silent") and the exclude list bound before sections in the Postgres params, which psycopg2 binds positionally and so yields a valid query against the wrong columns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwwnnDzefhhE8cG9sqjafE Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwwnnDzefhhE8cG9sqjafE Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
…clude-own-doc Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
…with a green suite Ten findings from the round-1 adversarial audit, all fixed. The one that mattered: every layer was pinned in isolation — exclusion_slug, the row filter, both backends' bound params, the renderer, `filtered` — and NOTHING pinned that main() hands `exclude` to run_search. Deleting `exclude=exclude` from the CLI call site left the flag completely inert (excluded doc back as hit #1, no `excluded=` line, no scope pair) with 304/304 tests green. The pre-existing guard whose docstring says "🔴 THE SEAM" proves only that argparse ACCEPTS the flag: it passes `--limit 0`, which returns rc 2 before any store is built. RULES.md — "verified in isolation is the new vacuous green"; "a count of DECLARATIONS is not a count of INSTANCES"; a guard's description claiming coverage it does not provide is worse than none, because it stops anyone looking. Both call sites are now covered: the offline one behaviourally, the postgres one — which no test here can execute — by an AST ledger that fails if a call site is added without `exclude=` as well as if one is removed. Also fixed: * The no-match remedy told exclusion-only runs to "widen --repo / --section". I had fixed exactly this class in the empty-scope branch and written a comment citing it, while its neighbour kept the defect — and no-match is the branch that matters (against a 401-doc corpus one exclusion can essentially never empty the scope). Now ONE function, `active_filter_flags`, answers it for both. * exclusion_slug silently no-opped on an absolute path, on a `handoff-x` basename with no `.md`, and on a trailing space — each printing a confident `excluded=<garbage>` while filtering nothing. Normalisation is unconditional now. A filter that declines to filter renders identically to one that worked. * SKILL.md stated rc 0 for a case measured at rc 4, contradicting this module's own comment and its own test, in the paragraph an agent reads to decide whether a zero is an answer. * The fenced command was not valid shell (`bash -n` rc 2) — an unquoted placeholder carrying an apostrophe and backticks. * "drops in_scope_docs by one" was false in both directions. * test_the_CLI_dedupes_repeats_without_reordering asserted on its own inline expression and never invoked the CLI. * A bare `str` exclude iterates per character (a wrong scope, not a no-op); a None element makes postgres return zero while memory returns everything. Both refused loudly. * scripts/README.md did not know the flag exists. Doc corrections, both mine: the "8-mutant sweep" was true but incomplete — an independent 13-mutant sweep found 3 survivors it never covered; and the adoption annotation claimed a session-id exclusion the published script does not perform (20/22 was reached by subtracting by hand afterwards). 🔴 THE FIX ROUND RESET THE GATE, AND TWO OF ITS OWN FIXES SHIPPED UNGUARDED. Re-sweeping the fixes rather than the findings: restoring the silent-no-op exclusion_slug, and disabling the bare-str guard, both SURVIVED a 306-test green suite. Ten tests added; the re-sweep now kills all 8 mutants, each by its own named test, tree restored byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwwnnDzefhhE8cG9sqjafE Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
…clude-own-doc Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
Round 1 — fixes landedRound 1 audited Gate on the merged tree
|
…HAT TO DO wrong
Six findings from the round-2 delta re-audit, all fixed. The instructive
one: round 1 consolidated which flags each remedy branch names, and left
the VERB open-coded — so the same two branches disagreed a second time,
one level in. `no-match` told an exclusion-only run to "widen
--exclude-slug", which is advice that guarantees the zero stays a zero:
widening an exclusion excludes MORE. Its sibling three lines up already
said "Widen or drop". Consolidation is a bug-finding instrument, and this
PR has now been caught by that rule twice.
* `_exclusion_list` was called unconditionally on the memory backend and
inside `if exclude:` on postgres, so `exclude=""` raised on one and was
silently accepted by the other — a NEW cross-backend divergence, in the
guard whose docstring says divergence is what it prevents. Coerced
before the bool() on both. Pinned as a RELATIONSHIP over a shared table
of shapes, not two per-backend tests: two tests that each pass alone is
exactly how it survived.
* The AST ledger pinned that `exclude=` is PRESENT, not that it carries
the value. `exclude=()` at the postgres site survived 314 tests — a
guard on a NAME, walkable by supplying a different value under it. It
now pins the value is the normalised local.
* "the whole suite green at 304 passed" named a scope nobody measured:
304 is two files; the repo has ~21k tests. In the docstring of the
guard against "verified in isolation".
* Nothing asserted the FENCE parses. `bash -n` rc 0 was true of the line
#1399 fixed and false of the block, because the untouched `cairn recall
--repo <path>` line parses `<path>` as a redirect — and bash aborts the
whole block, so a verbatim paste ran NEITHER command. Quoted, and
pinned by a test over the whole fence with a positive control.
* The "1,251 B no-match floor" was never a floor: the response echoes the
query, so it is a function of query length (1,261 B at 15 chars, 1,289
at 43, ~1,436 when excluding). Corrected to state the dependency rather
than swap in another falsifiable number.
Two instrument failures during this round, both of which produced
confident results that were worthless:
🔴 The sweep script restored SKILL.md with `git checkout --`. Those edits
were UNCOMMITTED, so it reverted them to the last commit and silently
destroyed two of them. RULES.md says restore from `cp -a`, never `git
checkout --`; I wrote the violation into my own tooling. Recovered and
re-applied; the replacement uses `cp -a` and says why.
🔴 Two mutants scored SURVIVED off runs that never executed. The tell was
an empty summary — `SURVIVED ()`. zsh does not word-split unquoted
parameters, so two test paths in a variable became one impossible
filename; pytest errored and printed no verdict. The sweep now REQUIRES a
summary line before scoring and prints VOID otherwise, runs a baseline
control first, and uses a bash array. Re-run isolated (mutating the
production value only, never the guard beside it): both KILLED, each by
its own named test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwwnnDzefhhE8cG9sqjafE
Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
…clude-own-doc Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
Round 2 — fixes landedRound 2 audited Gate on merged tree
|
… shape, not the site
Round 3 returned four 🟢 claim-corrections and NO behavioural defect. All
four fixed, and the third is the one worth recording.
* The comment claiming "one rule, one place — the phrasing included" was
false: round 2 consolidated WHICH flags each remedy branch names and
left the VERB as two literals. Rather than correct the sentence, this
makes it true — `widen_or_drop_clause` is the single literal both
branches read, pinned by a test that breaks BOTH when it breaks.
* The refusal for `exclude=""` ended `Pass ['']`, and `[""]` IS accepted:
it filters nothing while printing a confident `excluded=` line. An
error message naming the next defect as its own remedy — the silent
no-op this PR spent three rounds closing, offered as the fix for its
own trigger. The worked example is now suppressed for the empty string
and kept for every other shape.
* 🔴 AN UNSCOPED TEST COUNT, FOR THE FOURTH TIME IN THIS PR. "the whole
suite green at 304", "SURVIVED all 314 tests", "a 306-test green
suite", "304 of 304 tests passed" — every one numerically TRUE and
every one naming ~300 tests across two files against a repo of ~21,000.
Three rounds each caught one site and fixed that site. It only went
away when swept at all four. When a round fixes an instance of a shape,
grep for the shape — which is the rule I was quoting while breaking it.
* `_step4_fence` checks a STRIPPED reconstruction, not the pasted bytes.
Both forms are rc 0 today so nothing is masked; the residual is now
named in the docstring rather than left implicit (a heredoc added to
that fence would make the two disagree).
Sweep: baseline green (321), D1 (shared clause back to bare "widen") and
D2 (refusal offers [''] again) both KILLED, each by its own named test,
tree restored byte-identical via cp -a.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwwnnDzefhhE8cG9sqjafE
Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
…clude-own-doc Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
Round 3 — fixes landedRound 3 audited Note for round 4: these fixes introduced new code, not only prose —
|
…-able with a green suite
Round 4 returned category (b): only claim-corrections, no behavioural
defect. All three fixed. The first is the round-1 seam lesson recurring
one class over.
* 🔴 THE CONSOLIDATION PIN WAS TEXT-AGREEMENT, NOT STRUCTURE. Round 4
reverted both renderer branches to independent open-coded literals,
leaving `widen_or_drop_clause` defined and ENTIRELY UNREFERENCED — and
the suite passed 321/321. Every assertion I had written compared
strings; none observed where the string came from, so the test whose
NAME says "comes from ONE literal shared by both branches" could not
see the branches stop sharing it. Now an AST ledger: both branches must
CALL the helper, and the verb may not survive as a literal in render().
Same instrument, and same lesson, as the run_search seam.
* The unscoped-count sweep I claimed was complete missed a FIFTH site —
four quoted counts, five textual occurrences — and the survivor was
inside the very test the doc bullet held up as the exemplar. The bullet
asserting the sweep was done was itself an instance of the shape it
describes. Corrected, and it now says to grep AND COUNT.
* The example-suppression was keyed on emptiness while its own comment
named the hazard as "accepted and filters nothing". `exclude=" "` — one
character different — was still told to pass `[' ']`. Re-keyed on
`.strip()`, the predicate the sentence actually described.
NOT fixed, filed as rank 2 with a closing condition: `--exclude-slug " "`
is still a silent no-op reachable from the CLI. The fix is an input
rejection at main() (rc 2), which is a behaviour change — it would reset
the gate and require another round, so it is recorded rather than absorbed.
Sweep, in a DETACHED cp -a copy with .git removed (round 3 flagged that
earlier sweeps mutated the shared checkout in place, where a concurrent
session could read a mutant): baseline green 322; E1 (helper unreferenced
— round 4's own survivor) and E2 (suppression back to emptiness) both
KILLED by their own named tests. E2's first attempt returned NO VERDICT
because the marker comment commented out a closing paren; the VOID guard
caught it instead of scoring a survivor, and the re-run parse-checks the
mutant before running it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwwnnDzefhhE8cG9sqjafE
Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
…clude-own-doc Claude-Session-Id: f0fffa8e-a2ab-464e-bd7c-dfe9bd8955f0
Round 4 — fixes landed, and the ladder STOPS here🔴 Why the ladder stops on a stated criterion rather than a clean roundRound 4 returned category (b): only claim-corrections, no behavioural defect. Round 3 did too. Under the findings-keyed rule a round with findings is followed by another round — and round 4 had three — so this stop is taken on the prose-payload criterion, and the reasoning is written here rather than left implicit, because a report that ENDS on that criterion is otherwise indistinguishable from one that converged. All three conditions hold:
Why the rounds would not stop on their own: the payload here is substantially prose, so "fixed a defect" and "reworded a claim" are the same edit. Each round's fixes write new sentences for the next round to find; that is not convergence, and continuing would be indistinguishable from progress. The attribution gate cannot fire either (payload was 191 → 35 → 50 → 50, never two consecutive zero rounds). What is deliberately left open is item 4 above, with a closing condition anyone can check. The ladder, in full
Three of the four rounds caught coverage claimed and not held — a seam nothing pinned, a "consolidation" that was dead-code-able, a sweep that missed a fifth site, four unscoped test counts. None was a logic bug. The shipped mechanism has been correct since round 1 and has been measured green in every round. Final gate — merged tree
|
The measurement
Rank 1 of
claudedocs/handoff-handoff-search-index.mdwas "does an index hit change what a session does?" — the question the whole effort rides on, because an index queried by every session and never useful is a cost, not a capability.It is now answered, and a second finding falls out of the same read.
Adoption held. Re-ran check 1 at n=22 (the doc had n=4): 20 of 22 post-fix
/resumeruns on the workbench queried the corpus (91%), controlcairn recall21/22, against 2 of 11 (18%) pre-fix. The measuring session is excluded from both halves by session id. The laptop still contributes 0 runs — measured over ssh, not assumed.Yield is 1 of 20. Nineteen sessions went straight from the tool result to
claim-workorgh pr view; not one opened a hit document or reported the output. The single exception is real and is exactly the value proposition — a session dropped the entire backlog it was resuming for an unclaimedforcing: securityitem in a different effort, off a hit it had carried in context for five hours — but the delivery mechanism was context carryover, not the step.Why the other 19 got nothing: 23 of 60 hit slots (38%) were the session's OWN handoff, read minutes earlier in step 3, and it was the #1 hit in 13 of 20 queries — all three slots in two of them.
That is arithmetic, not a ranker bug. Step 4 says to query the handoff's TOPIC, and the best text match for a document's topic is that document. The re-keying that made the step unconditional in #1332 is the same thing that aimed it at itself.
The change
--exclude-slug SLUG|PATH(repeatable). Takes the doc pathresume-state.shalready printed on itshandoff:line, or a bare slug; a path is normalised through the indexer's ownslug_for, never a second hand-rolled strip./resumestep 4's fence passes it.Measured differential on the live corpus, one flag apart:
It is a scope filter, not a post-hoc drop, and that distinction is the design:
_selected/_filter_predicates— the single predicate bothstatsandsearchread — so the counter and the query cannot come to different views of what is in scope;empty-scope(rc 4), "your filter emptied the corpus", neverno-match(rc 0), which would assert the corpus is silent about a topic it was never allowed to answer on;excluded=prints on the scope line of every status including the zeros, andin_scope_*appears besideindexed_*.Also corrects
How to verifycheck 3, which could not have worked as written — the DB path needsnix develop, not justKUBECONFIG; a barepython3dies withpsycopg2 is required. Verified:backend=postgres,indexed_sections=5125, the same number the timer's own run reported writing.Coverage
13 new tests. An 8-mutant sweep with a positive control, run under
PYTHONDONTWRITEBYTECODE=1, each mutant verified PRESENT in the file before scoring, tree restored byte-identical afterwards. All 8 killed, each by its own named test. The two that matter are the silent ones:Instrument note
The 38% had to be rebuilt mid-measurement. The first pass grepped transcripts for
claudedocs/handoff-<slug>.mdand reported a suspiciously perfect 100% — because the search's own output prints each hit's doc path, so the needle was matching the instrument. Rebuilt from two sources the search cannot write (resume-state.sh'shandoff:line andclaim-work --slug-forarguments) it is 38%. Same family as the~24×mention-vs-invocation overcount this effort already recorded.🤖 Generated with Claude Code
https://claude.ai/code/session_01SwwnnDzefhhE8cG9sqjafE