feat(lineage): resolve child session links from parent-side dispatch evidence - #4690
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DNGJ3awfNrsLaMdHgQZvid
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b424bdd1a1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if source_conn is None or origin != Origin.CLAUDE_CODE_SESSION.value: | ||
| return set() |
There was a problem hiding this comment.
Pass source evidence into semantic replay
For a Claude parent that contains the dispatching tool_use but lacks result/progress child identity, with toolUseId available only in the child's meta sidecar, live ingest can bind the edge but the v94 semantic rebuild cannot: revision_governance.write_with_reparse_receipt invokes write_parsed_session_to_archive without source_conn, and the cross-shard graph-resolution pass likewise omits it, so this branch drops the sole witness and rebuilds the link with dispatch-evidence-absent. Pass the durable source connection and its blob store through both replay routes so reindex preserves the live result.
AGENTS.md reference: AGENTS.md:L60-L63
Useful? React with 👍 / 👎.
| if source_conn is not None: | ||
| graph_kwargs["source_conn"] = source_conn |
There was a problem hiding this comment.
Re-resolve links when a sidecar arrives
When the parent and child transcripts are already indexed without result-side dispatch identity, acquiring the matching agent-*.meta.json afterward only stores/materializes a fact and does not save either parsed session, so _resolve_session_graph never calls this new source lookup. The link therefore remains NULL with dispatch-evidence-absent, and delegation_facts stays unresolved until an unrelated transcript rewrite happens; sidecar admission needs to enqueue graph convergence for the affected parent/child edge.
AGENTS.md reference: AGENTS.md:L109-L115
Useful? React with 👍 / 👎.
| "SELECT source_path, blob_hash FROM raw_sessions WHERE origin = ? AND source_path LIKE ?", | ||
| (origin, f"%/subagents/{stem}.meta.json"), |
There was a problem hiding this comment.
Match nested subagent sidecar paths
For an admitted sidecar path such as /<parent>/subagents/nested/agent-a.meta.json, which is allowed by the recursive agent_sidecar_meta pattern in origin_specs.py, this query requires the agent filename to immediately follow subagents and therefore never returns the row; additionally, the later parts[-3] check would compare nested rather than the parent identity. Such a child is incorrectly recorded as dispatch-evidence-absent instead of binding its declared toolUseId; locate the subagents segment and match the exact basename independently of nesting depth.
AGENTS.md reference: AGENTS.md:L175-L176
Useful? React with 👍 / 👎.
| resolved = _canonical_identity_session_ids(conn, origin, identity_values) | ||
| if resolved is None or child_session_id not in resolved: | ||
| continue |
There was a problem hiding this comment.
Revisit bound siblings when dispatch identities resolve
Given one parent dispatch observation that names both agent-a and agent-b, an agent-a sidecar naming that tool, and child A arriving before child B, _canonical_identity_session_ids returns None while B is absent, so this observation is skipped and the sidecar binds A. When B later arrives, only B's graph is evaluated; A remains bound even though the same observation now resolves to two different sessions, producing a resolved delegation from a contradictory witness set. Keep unresolved competing identities as blockers and invalidate/re-resolve already-bound siblings when a new identity claim appears.
AGENTS.md reference: AGENTS.md:L60-L63
Useful? React with 👍 / 👎.
| rows = source_conn.execute( | ||
| "SELECT source_path, blob_hash FROM raw_sessions WHERE origin = ? AND source_path LIKE ?", | ||
| (origin, f"%/subagents/{stem}.meta.json"), |
There was a problem hiding this comment.
Avoid rescanning all Claude source rows per edge
On an archive with N Claude raw artifacts and M child edges, every dispatch resolution executes this leading-wildcard LIKE; the available source_path index cannot serve it, so SQLite uses only idx_raw_sessions_origin and scans all Claude rows for each child. A live bulk ingest or replay of thousands of children therefore becomes O(N×M) work on the daemon's sole writer before blob reads are counted. Materialize an indexed parent/child sidecar identity or build one bounded per-batch lookup instead of repeating the corpus scan.
AGENTS.md reference: AGENTS.md:L109-L115
Useful? React with 👍 / 👎.
Master's side kept for every conflicted hunk: #4690's dispatch-link functions and #4692's attachment writer in write.py, INDEX_SCHEMA_VERSION 94, the daemon-probe status path, and the tests master advanced with them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DNGJ3awfNrsLaMdHgQZvid
## Summary Integrates the reindex-recovery work: the physical blob disposition plan and its guarded apply, embedding-vector preservation, archive-ingest parse-pool sizing, the query-execution envelope lab check, the conversational-evidence rule in the one-shot importer, AI Studio attachment ownership on same-timestamp document turns, and attachment-reacquisition coverage. Rebased onto master f80542b. ## Review outcome The adversarial review's blocking finding was the branch's own attachment writer: retaining an owner-ambiguous attachment wrote a ref-less row that `blob-reference-closure` and `attachment-coverage` classify as blocking debt, and that crashed `polylogue ops maintenance blob-reference-closure`. Master #4692 has since shipped its own version of that writer change, so this branch keeps master's side of `write.py` in full -- the file is now byte-identical to master, which also preserves #4690's dispatch-link functions and `INDEX_SCHEMA_VERSION = 94`. What remained was the independent crash the review proved, which master does not fix: `attachment_relink._match_session_payload` called `attachment_message_owner_key` with no handler, unlike `_write_attachments`. Any ref-less attachments row makes `_read_orphaned_attachment_ids` non-empty, so the raw re-parse scan reaches the same ambiguity and raises `MessageOwnerAmbiguityError` out of `blob_reference_closure.py`, past the CLI's `BlobReferenceClosureError`-only handler. ## Solution `_match_session_payload` classifies the ambiguity as `UnrecoverableAttachmentReason.OWNER_AMBIGUOUS` -- the same typed-unrecoverable shape as `NO_AUTHORITATIVE_RAW` and `MESSAGE_MISSING`. No consumer change is needed: closure blockers are already filtered to `acquired` orphans, and both required archive-verification checks key on acquired-and-unreferenced, which the unowned row is not. Two tests, each naming its anti-vacuity condition: - `test_owner_ambiguous_orphan_is_reported_typed_not_raised` -- writes the ambiguous session through the production writer, then runs plan and relink. Removing the handler makes it red with the review's traceback. - `test_unowned_attachment_evidence_keeps_closure_and_coverage_clean` -- `blob-reference-closure` and `attachment-coverage` on a fixture archive carrying the #4692 unowned shape. Giving the row `acquired` turns both ERROR. ## Verification ``` .venv/bin/python -m devtools test tests/unit/maintenance/test_archive_verification.py tests/unit/storage/test_attachment_relink.py 1 failed, 118 passed, 3 warnings in 77.00s .venv/bin/python -m devtools verify --quick 13 gates ok, no "out of sync" ``` The single failure is `test_empty_covered_table_without_stats_is_not_missing_coverage`, confirmed failing on master itself and tracked separately. ## Residuals - The review's two should-fix items are untouched and have beads: the destructive branch of `blob_disposition_apply` (the `unlink_unreferenced_blob_hashes_under_exclusion` call) has no test that reaches a real unlink, and `embedding_preservation` still has no production caller. - An owner-ambiguous attachment carrying inline bytes reaches `_write_attachment_row` with a preacquired `acquired` tuple, which would produce the acquired-and-unreferenced shape both checks reject. The new test covers the unfetched shape master produces without preacquired bytes; the acquired variant is master's behavior, not this branch's. - Envelope `_temp_used_bytes` measures the TMPDIR filesystem, not the process (review nit 5), and the one-shot importer's evidence refusal increments no `ParseResult` counter (nit 6). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DNGJ3awfNrsLaMdHgQZvid --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#4701) ## Summary Follow-up to #4697. That PR typed the writer's owner-ambiguous attachment as `attachment_unowned` in `source-conservation`, but two further declarations count the same row, so seeded archives still verify non-green and the convergence tests stay red. ## Problem Running the previously-red files at #4697's head, the registry assertion is still tripped, now by a different pair: ``` archive verification registry is not green: [ ('attachment-coverage', 'error', 'acquired attachment debt: missing_blob=0, unreachable=1', ...), ('blob-reference-closure', 'error', '0 raw session(s) and 1 acquired attachment(s) lack canonical refs', ...)] ``` 14 of the 19 failures in that run reduce to this one attachment. Both checks ask "is this acquired attachment reachable through a ref?", and both answer no for a row the writer deliberately created without one. Three separate call sites carried their own copy of the "acquired and ref-less" SQL, so fixing one would have left the others reporting the same row. ## Solution One shared predicate, `acquired_attachment_missing_ref_predicate()` in `polylogue/storage/blob_liveness.py`, used by `closure_counts`, the `blob-reference-closure` sample query, and `scan_attachment_coverage`. An acquired ref-less attachment is debt only when its `ref_count` is non-zero. That is the polylogue-w06b state these checks exist to catch: the sweep set the count while refs existed, then the refs disappeared without the sweep running, leaving a row unreachable from every read path. A row inserted with `ref_count` 0 and deliberately kept out of the sweep never had a ref to lose. `scan_attachment_coverage` reports those as their own `unowned_count` dimension, and the OK summary names them rather than claiming every acquired attachment has a live reference. ## Verification All three declarations, same fixture, only `ref_count` varying: ``` === owner-ambiguous (ref_count=0) === attachment-coverage ok all 1 acquired attachment(s) have bytes and a live attachment reference; 1 retained unowned blob-reference-closure ok every raw session and acquired attachment has canonical reference closure source-conservation ok ... non_session_artifact=1; attachment_unowned=1 === stale ref_count (ref_count=2) === attachment-coverage error acquired attachment debt: missing_blob=0, unreachable=1 blob-reference-closure error 0 raw session(s) and 1 acquired attachment(s) lack canonical refs source-conservation error ... attachment_unreferenced=1! ``` The `ref_count=2` column is the anti-vacuity control: the exemption does not disable any of the three checks, it only stops them claiming the explained case. `.venv/bin/python -m devtools verify --quick` — exit 0, all 13 gates ok, no `out of sync`. `ruff` and `mypy` clean on the changed modules. ## Residuals - `test_acquired_unreachable_attachment_debt_is_blocking` previously seeded `ref_count` 0. That is now precisely the writer's deliberate shape, so the test seeded a case that is no longer debt; it seeds the stale non-zero count that actually witnesses lost refs. The scenario it describes is unchanged. - The affected-file run is queued behind a full `verify_all` on the host's single pytest slot; I will post it here when it lands and will not enable auto-merge before then. - Two failures in that run are inherited from master, not addressed here: `test_empty_covered_table_without_stats_is_not_missing_coverage` (already red in the 2026-09-05 corpus; `sqlite_stat1` holds 3 rows for an empty `action_pairs`, contradicting the premise #4692 documented) and a `dispatch_reason` key added to `session_links.evidence_json` by #4690. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DNGJ3awfNrsLaMdHgQZvid --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Populates
session_links.parent_tool_use_block_idfrom parent-side dispatch evidence: Claude Code progress records carryingparentToolUseIDand theagent-*.meta.jsonsidecar'stoolUseIdare lowered as typed dispatch observations; the canonical session-link writer joins the exact parenttool_useblock by provider tool id and the child transcript stem (agent-<agentId>), order-independently (parent-first and child-first ingest converge), with typed non-guessed outcomes for missing evidence, absent blocks, and contradicting witnesses.delegation_factsconsumes the canonical edge.Problem
The column was NULL on all 9,497 live links: no parser set the child-side field the resolver read, and the direct-row unit test manufactured a field production cannot emit (beads polylogue-kblcs, polylogue-b2elq).
Verification
devtools test tests/unit/storage/test_dispatch_link_resolution.pyplus the Claude Code parser, sidecar, normalization, delegation, lineage and topology suites:154 passed, then43 passedafter one expectation update (a dispatched child is named by its transcript stem).devtools verify --quick: all gates ok after regenerating the schema disposition.Residual risk
This lane was interrupted overnight and finished from its checkpoint; the 5yig audit (prefix order by content position, never timestamps) is only partially covered by the new tests. A read-only profile of how many of the 9,497 live links resolve was not run.
Beads: polylogue-kblcs, polylogue-b2elq
🤖 Generated with Claude Code
https://claude.ai/code/session_01DNGJ3awfNrsLaMdHgQZvid