Skip to content

feat(lineage): resolve child session links from parent-side dispatch evidence - #4690

Merged
Sinity merged 4 commits into
masterfrom
lane/lineage-links
Sep 5, 2026
Merged

feat(lineage): resolve child session links from parent-side dispatch evidence#4690
Sinity merged 4 commits into
masterfrom
lane/lineage-links

Conversation

@Sinity

@Sinity Sinity commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

Populates session_links.parent_tool_use_block_id from parent-side dispatch evidence: Claude Code progress records carrying parentToolUseID and the agent-*.meta.json sidecar's toolUseId are lowered as typed dispatch observations; the canonical session-link writer joins the exact parent tool_use block 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_facts consumes 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.py plus the Claude Code parser, sidecar, normalization, delegation, lineage and topology suites: 154 passed, then 43 passed after 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

@Sinity
Sinity enabled auto-merge (squash) September 5, 2026 15:03
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 36e8f97a-d16a-4858-a40c-8b7fe130e3fe


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T15:14:14.741326Z b424bdd PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +7374 to +7375
if source_conn is None or origin != Origin.CLAUDE_CODE_SESSION.value:
return set()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +1106 to +1107
if source_conn is not None:
graph_kwargs["source_conn"] = source_conn

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +7388 to +7389
"SELECT source_path, blob_hash FROM raw_sessions WHERE origin = ? AND source_path LIKE ?",
(origin, f"%/subagents/{stem}.meta.json"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +7457 to 7459
resolved = _canonical_identity_session_ids(conn, origin, identity_values)
if resolved is None or child_session_id not in resolved:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +7387 to +7389
rows = source_conn.execute(
"SELECT source_path, blob_hash FROM raw_sessions WHERE origin = ? AND source_path LIKE ?",
(origin, f"%/subagents/{stem}.meta.json"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@Sinity
Sinity merged commit 0780832 into master Sep 5, 2026
4 checks passed
@Sinity
Sinity deleted the lane/lineage-links branch September 5, 2026 15:23
Sinity added a commit that referenced this pull request Sep 5, 2026
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
Sinity added a commit that referenced this pull request Sep 5, 2026
## 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>
Sinity added a commit that referenced this pull request Sep 5, 2026
#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant