feat(session-search): treat compression continuations as one conversation (lineage-aware recall) - #91312
Conversation
Port the accepted #68 resolver kernel onto the current upstream state-search layer: one query-local memo/path-compression pass resolves each ranked owner candidate to its positive compression-continuation root under a global B=2000 successful uncached row-fetch budget. Generic parentage is not lineage; branch/delegation/tool/reset boundaries stay distinct; missing parents and proven positive cycles fail closed; a B-exhausted partial path is operational uncertainty, never memoized as unresolved.
…emantics Winner selection now consumes the current-upstream ranked search_messages candidate rows and dedupes by positive compression-root identity instead of generic parentage: one query-local memo, one coherent read snapshot, early-K stop, owner-once resolution, and the first displayable anchor rule. Current-session and exact-title exclusion share the same root meaning; a B-exhausted scan stops and is surfaced by the tool as an explicit truncated/warning result, never as a silently complete answer. The tool _discover path routes through resolve_lineage_winners while _scroll keeps its generic live-context parentage.
Port the accepted #68 acceptance matrix: compression-continuation collapse, branch/delegation/tool/generic-parent separation, foreign markers, best-owner anchor, memo reuse, missing-parent and 2-node/long/tail-cycle fail-closed, exact B=1999/2000/2001 boundaries, cycle-at-B, no memo poisoning, early-K, current/title exclusion parity, one-read-transaction snapshot, fresh-reset discoverability, and safe-prefix truncation surfaced by the tool.
Close the #129 acceptance matrix gaps from review: real-default B=2000 boundaries (a root on lookup 1999/2000 succeeds; 2001 truncates before the lookup), a 10k-node acyclic chain cut by the default budget rather than a semantic depth cap, defensive internal larger-K stress, and lineage-lookup coherence under a concurrent writer. Adds a BEGIN/COMMIT bulk chain fixture so the 2k-10k node graphs stay fast.
The _discover rewrite routed current-context visibility through the new winner phase, orphaning _is_compacted_message (its only production caller was removed). Remove the helper and its now-dead unit tests; _get_message_storage_state keeps its live _scroll caller.
…yload Review cleanups: resolve_lineage_winners' accepted_roots was written but never read (remove slot/init/write); _resolve_lineage dropped its speculative generic-parent fallback (no stub caller, SessionDB always has the resolver); _discover builds one empty payload and sets warning/message only on the bound-hit vs no-match branch instead of duplicating six shared keys.
_seed_bound_chain re-implemented the PRAGMA/UPDATE/COMMIT link loop a third time; it now composes _chain_sessions + _link_positive_chain + append_message.
2443ec2 to
e1033b7
Compare
|
Status update — ready for review.
|
This is the right model and it is argued with unusual rigor: keeping compression-lineage identity (positive edges only — parent exists, parent ended by Points:
|
What does this PR do?
The bug class. Search results currently conflate generic parent ancestry with compression identity. A long conversation that context-compression rotated into a chain of physical session rows (root
-[compression]->mid-[compression]->tip) is either merged with unrelated sessions — because anyparent_session_idedge is treated as conversation identity — or split into fragments that recall misses: a match living only in a compressed continuation is dropped when the query is absent from the root transcript (upstream #6507). The same_discoverseam already fails for/new-reset recall (#85756, fixed by #86652) and own-early-context recall (#90939, open #90974); this PR closes the compression-continuation half and makes the fresh-reset predecessor discoverable at the winner level.The invariant. Only positive compression-continuation edges define one searchable conversation. The exact edge condition: the parent exists, the parent ended with
end_reason='compression', the child is not atoolsession, and the child's_branched_from/_delegate_frommarkers do not explicitly point at that parent (foreign markers pointing elsewhere do not disqualify). Generic parent ancestry (branches, delegates/subagents, tool/reset children) is not conversation identity. Resolution is fail-closed for missing parents and proven cycles, and query-bounded (exactB=2000successful uncached-row-fetch budget, early-K stopping) with no semantic depth cap — a 10k-deep chain resolves correctly, not by capping depth.Concrete behavioral contracts (from the implementation)
B=2000counts exactly one successful uncached lineage-node row fetch. Memo hits, absent-row fetches, and traversal-local cycle checks consume nothing; the archive-metadata batch queries (ownerend_reason+ per-messagecompacted) are not lineage-node fetches and do not count toward the budget.truncated: trueplus a warning ("results are a safe ranked prefix and may be incomplete") andstats.lineage_bound_hit; an exhausted partial path is operational uncertainty, not semantic evidence, so it is never memoized as unresolved and never fabricated into a root.compacted=1).out_of_context_end_reasons(reset set +new_session) so a/new-style predecessor of the current session is not hidden (session_search discovery hides ALL hits from the current session's own /new-reset lineage (gateway recall goes blind) #85756) — complementing fix(session-search): recall /new-reset sessions in the current lineage (#85756) #86652 at the winner level.session_id— it is never rewritten to the root._scrollis deliberately unchanged in identity semantics. Scroll's guard is about live context, not search lineage: a live delegation/branch parent is still in the active context even though it is a distinct compression root, so scroll keeps generic parentage (fix: prevent false positives in recursive delete detection #68). This PR changes only discovery/winner semantics.Snapshot / concurrent-writer note (explicit)
The winner phase runs inside a single read context +
BEGINread transaction (archive metadata, lineage resolution, and current-lineage ancestors; rolled back on exit). Candidate discovery (search_messages) happens on the existing connection model before that transaction, so the discover→resolve sequence is not one atomic snapshot; concurrent writers are not blocked. Missing parents and proven cycles are fail-closed at read time.Implementation choices supporting that contract
Query-local resolver memo + path compression (a memo hit path-compresses the visited prefix to the known root); winner dedupe, current-session exclusion, and exact-title exclusion all using the same compression-root meaning (the exact-title slot is pre-resolved with the same resolver); preservation of the highest-ranked anchor per conversation; deferred expensive hydration;
_chunked(size 400) batch reads for SQLite var-limit safety;SESSION_WINNERS_LINEAGEtelemetry logging candidate/winner/work/memo/bound-hit counts so truncation is observable.Related Issues
/new-reset lineage recall. The/newcase (upstream fix(session-search): recall /new-reset sessions in the current lineage (#85756) #86652) and the compression case (this PR) are two halves of the same "same-lineage content is already in context" guard in_discover; this PR also makes the fresh-reset predecessor discoverable at the winner level viaout_of_context_end_reasons._discoverseam; complementary.Overlap disposition (classified by contract, not file overlap)
Prior art — closed/unmerged; this PR is the first to land the contract
Open upstream PRs — COEXIST (different surface, no equivalent invariant implemented)
get_compression_tip. Different surface (export), same edge semantics.session_searchto the current conversation; explicitly preserves compression-lineage recall. Different contract (scope)./sessionslist +sessions searchUX with row-per-compression-chain dedupe. Its chain identity (compression-chain root,COMPRESSION_CHAIN_MAX_HOPS) matches this PR's positive-compression-continuation roots — not a conflicting identity — but its surface (listing/pagination/lane-scoping) is different and it does not implement this PR's bounded/fail-closed lineage reconstruction for the agent tool (no B=2000, no memo, no lineage summarization). If feat(cli): sessions list/search options, columns, pagination #75496 merges before this PR: stop mechanical rebasing, re-audit ownership of the lineage seams, and explicitly test whether its chain-identity semantics remain equivalent to compression-continuation roots. Its continued openness is not a veto on this PR.Merged substrate (reused, not replaced)
#69544, #12960, #38393, #39062, #57685, #86652, #90357.
Test evidence
tests/test_session_search_lineage.py— 37 tests:B=1999/2000/2001boundary, 10k-deep chain,K=60early-K, cycle/missing-parent fail-closed, current-session exclusion, anchor preservation, concurrent-writer lineage coherence.tests/tools/test_session_search.py— 51 tests.tests/test_hermes_state.py -k search— 19 state-search tests.scripts/run_tests.sh: all three targeted files green. A full-suite run on a local Windows box is blocked by pre-existing environmental failures unrelated to this PR (POSIX-only modulestermios/os.chown/AF_UNIX, missinganthropicSDK, brokenaiohttpwheel, missing platform/API credentials); the run reached ~12k tests with zero failures in any file this PR touches. Upstream CI (Linux) is the authoritative broad gate.Notes for maintainers
_discoverintools/session_search_tool.py; coordinate the compaction-escape-hatch handling (this PR replaces_is_compacted_messagewith the winner-phase_is_archivedcheck, fix(session_search): recall the current session own early context (#90939) #90974 relies on the helper).Checklist
scripts/run_tests.sh— 107 passed (37+51+19)