Skip to content

fix(sources): stop colliding Claude Code fork/resume carryover identity - #3472

Merged
Sinity merged 4 commits into
masterfrom
fix/lineage/subagent-identity-collision
Jul 31, 2026
Merged

fix(sources): stop colliding Claude Code fork/resume carryover identity#3472
Sinity merged 4 commits into
masterfrom
fix/lineage/subagent-identity-collision

Conversation

@Sinity

@Sinity Sinity commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the dominant remaining cause of ambiguity in claude-code-session revision membership: fork/resume/usage-limit-quirk boundary records that Claude Code stamps with an ANCESTOR session's sessionId were composing that ancestor's bare id as their own identity, colliding every such carryover fragment onto the ancestor's logical_source_key.

Problem

Measured while verifying polylogue-oycw's set-containment fix (#3401/#3405) against real ambiguous cohorts: only 56/185 (30.3%) of claude-code-session cohorts resolved cleanly, far below chatgpt-export/claude-ai-export (>90%). Deep-diving one cohort (logical key 0213d48f...) showed 3 raw revisions: the real ancestor session (213 msgs) plus two tiny (3-msg, 5-msg) fragments carved from two DIFFERENT, unrelated child files (a3a274a2...jsonl, cbea0c3a...jsonl) — verified directly against real ~/.claude/projects/ files, not inferred from the parser.

Root cause: Claude Code re-stamps a handful of records with an ancestor's sessionId even inside a file that is otherwise entirely a different session's own content — either a leading resume/fork boundary record (the file opens with one record still tagged with the parent), or a mid-file quirk (a /exit sent right after a "usage limit reached" notice gets tagged with the ancestor's id despite chaining straight off the file's own preceding record). code_parser.py composed provider_session_id from that in-record sessionId, so every such fragment — from every fork/resume/quirk descendant of one ancestor — collided on the ancestor's own logical_source_key. classify_membership_revisions (polylogue-aggz) requires a strict pairwise containment chain across a cohort; two mutually-incomparable carryover fragments (each individually a strict subset of the real ancestor, but not of each other) made the whole cohort conflict, quarantining even the ancestor's own large revision.

session_links/lineage normalization exists specifically to model this ("a forked/resumed child as ITS OWN session with a recorded relationship to the parent, not as a same-identity revision of the parent") but was never being used for it — this bug routed the relationship through raw identity collision instead.

Solution

  • polylogue/sources/dispatch.py: _claude_code_grouped_record_specs (eager) and _claude_code_stream_sessions (streaming) now identify each file's own real content — the largest sessionId-grouped run — as "primary", and detect a carryover run via two structural signals: it occurs before the primary's first record (the common leading-boundary shape), or its root record's parentUuid resolves to a uuid the primary already produced (the mid-file quirk shape). A carryover run's identity is qualified (f"{ancestor_id}:{fallback_id}") instead of the bare ancestor id, so siblings off one ancestor stay distinct from each other and from the ancestor. Subagent/self-compaction files (fallback_id starting with agent-) keep their existing, unrelated identity scheme (session_id:fallback_id) untouched — this bug was specific to main-transcript files.
  • polylogue/sources/parsers/claude/code_parser.py: a new explicit trust_fallback_id flag (threaded through LoweredPayloadSpec, parse_code/parse_code_stream, _parse_code_records) makes dispatch.py's proven identity choice override the parser's long-standing "trust the record's own sessionId" default, but only for these dispatch-proven carryover fragments — ordinary calls (the overwhelming majority, including most test call sites where fallback_id is just a caller-supplied label) are unaffected. The ancestor id becomes parent_session_id, so session_links resolution carries the relationship instead of colliding revision membership on it.
  • Schema: INDEX_SCHEMA_VERSION bumped to 53 with a SEMANTIC_REPARSE IndexDeltaDeclaration (storage/sqlite/lifecycle.py) — this changes sessions.native_id (generated from provider_session_id) and session_links lineage edges for affected raw acquisitions, so it requires polylogue ops reset --index && polylogued run, not a free fast-forward.

Rejected alternative

Wiring in _maximal_evidence_fallback (deterministic largest-revision-wins for an irreducible conflict) instead of fixing identity was considered and rejected: it's already designed/unit-tested but deliberately not wired into classify_membership_revisions because it trips a real write-back invariant (archive.py's apply_raw_membership_classification, documented incident polylogue-miwv/#3211). Fixing identity so the ambiguous cohort never forms in the first place is the "unrepresentable, not detected-and-classified" direction this repo's design already points at.

Verification

  • devtools verify --quick: green (ruff format/check, mypy --strict, render all --check, layering, closure-matrix, schema-versioning policy, schema promotion audit).
  • devtools test across the affected surface: tests/unit/sources/{test_dispatch_payloads,test_parsers_claude_code_artifacts,test_claude_code_sidecar_evidence,test_tool_result_sidecars,test_claude_code_normalization_laws,test_source_laws}.py, tests/unit/pipeline/test_archive_ingest_shared_raw.py, tests/unit/storage/{test_revision_replay,test_index_fast_forward_lifecycle,test_schema_policy_contracts,test_archive_tiers_ddl}.py320 passed, 2 failures confirmed pre-existing and unrelated via a throwaway git worktree add --detach origin/master checkout with zero of this PR's changes applied (same failures reproduce byte-for-byte): filed as polylogue-yl8t (streaming/eager claude_parse_coverage event-order mismatch) and polylogue-ihro (Codex long-rollout message-count test).
  • New regression test test_sibling_fork_carryovers_off_one_ancestor_do_not_collide_identity (tests/unit/pipeline/test_archive_ingest_shared_raw.py) replicates the exact measured production shape: two separate resume/fork files both carrying a boundary record from the same ancestor session. Proves they get distinct, qualified identities and record the ancestor as an unresolved session_links edge instead of colliding.
  • Before/after resolution-rate on the live archive was not re-measured in this PR (would require polylogue ops reset --index && polylogued run against the real archive, out of scope for a code-only PR); the new regression test directly proves the collision no longer occurs for the measured shape.

Ref polylogue-jc4q

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Sinity, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2914b1c3-0420-4e2b-a11b-64e00ecaeb42

📥 Commits

Reviewing files that changed from the base of the PR and between dc98d3a and 8274cdb.

📒 Files selected for processing (9)
  • .beads/issues.jsonl
  • polylogue/sources/dispatch.py
  • polylogue/sources/parsers/claude/code_parser.py
  • polylogue/storage/sqlite/archive_tiers/index.py
  • polylogue/storage/sqlite/lifecycle.py
  • tests/fixtures/claude-code/claude-normalization-main.jsonl
  • tests/unit/pipeline/test_archive_ingest_shared_raw.py
  • tests/unit/sources/test_claude_code_normalization_laws.py
  • tests/unit/sources/test_source_laws.py

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.

Sinity and others added 4 commits July 31, 2026 18:21
Problem: measuring polylogue-oycw's set-containment revision-membership fix
against real claude-code-session ambiguous cohorts found only 56/185 (30.3%)
resolved cleanly, far below chatgpt-export/claude-ai-export (>90%). Root
cause: Claude Code re-stamps a handful of records with an ANCESTOR session's
sessionId even inside a file that is otherwise entirely a different session's
own content -- a resume/fork boundary replay, or a `/exit` sent right after a
"usage limit reached" notice. The parser composed identity from that
in-record sessionId, so every fork/resume/quirk descendant of one ancestor
collided its carryover fragment onto the ancestor's own `logical_source_key`.
classify_membership_revisions requires a strict pairwise containment chain
across a cohort; two mutually-incomparable carryover fragments (each
individually a strict subset of the real ancestor, but not of each other)
made the whole cohort `conflict`, quarantining even the ancestor's own large
revision.

Solution: dispatch.py's Claude Code grouping
(`_claude_code_grouped_record_specs` eager / `_claude_code_stream_sessions`
streaming) now identifies the file's own real content ("primary": the
largest sessionId-grouped run) and detects a carryover run by two signals --
it occurs before the primary's first record (the common shape: a
resume/fork file opens with one boundary record), or its root record's
parentUuid resolves to a uuid the primary already produced (the mid-file
quirk shape). A carryover run's identity is now qualified
(`f"{ancestor_id}:{fallback_id}"`) instead of the bare ancestor id, keeping
every sibling carryover distinct; the ancestor id is recorded as
`parent_session_id` so `session_links`/lineage resolution carries the
relationship instead of colliding revision membership on it -- using the
mechanism lineage normalization already exists for, per the bead's own
framing ("model a forked/resumed child as ITS OWN session with a recorded
relationship to the parent, not as a same-identity revision of the parent").

A new explicit `trust_fallback_id` flag (threaded through
`LoweredPayloadSpec`, `parse_code`/`parse_code_stream`, and
`_parse_code_records`) makes dispatch.py's proven identity choice override
the parser's long-standing "trust the record's own sessionId" default only
for these carryover fragments -- ordinary calls (the overwhelming majority,
where `fallback_id` is just a caller-supplied label) are unaffected.
Subagent/self-compaction files (`fallback_id` starting with `agent-`) keep
their existing, unrelated identity scheme untouched.

This is a SEMANTIC_REPARSE index-tier change (bumps INDEX_SCHEMA_VERSION to
53, declared in lifecycle.py): it changes `sessions.native_id` for affected
raw acquisitions and the `session_links` lineage edges recorded for them,
recoverable only via `polylogue ops reset --index && polylogued run`.

Ref polylogue-jc4q

Co-Authored-By: Claude <noreply@anthropic.com>
Bumps INDEX_SCHEMA_VERSION to 53 and adds the matching
IndexDeltaDeclaration in lifecycle.py, per devtools lab policy
schema-versioning's requirement that every index bump above the
compatibility floor declare a delta class. The Claude Code fork/resume
carryover identity fix changes sessions.native_id (a generated column) and
session_links lineage edges for affected raw acquisitions, so this is
SEMANTIC_REPARSE, not a free fast-forward.

Ref polylogue-jc4q

Co-Authored-By: Claude <noreply@anthropic.com>
…ixtures

- New regression test in test_archive_ingest_shared_raw.py replicating the
  bead's measured shape exactly: two SEPARATE resume/fork files
  (sibling-a.jsonl, sibling-b.jsonl) both carrying a boundary record from
  the SAME ancestor session. Before the dispatch.py/code_parser.py fix,
  both siblings' carryover fragments would collide on the ancestor's bare
  provider_session_id; this proves they now get distinct, qualified
  identities and record the ancestor as an unresolved session_links edge
  instead.
- test_grouped_carryover_sessions_share_one_raw_row (polylogue-sjf6)
  renamed its fixture files to parent-session.jsonl/child-session.jsonl --
  Claude Code's own convention of naming each file after its own session id
  -- since dispatch.py's carryover-vs-independent detection anchors on that
  convention; updated its assertions for the new (fixed) identity/lineage
  shape.
- test_claude_code_normalization_laws.py's family fixture renamed
  normalization-family.jsonl -> claude-normalization-main.jsonl and its
  fallback_id call sites updated to match, for the same reason.
- test_source_laws.py's two pure-splitting unit tests updated to pass a
  fallback_id matching their primary group's real content id (the
  production-realistic shape) instead of an arbitrary "fallback" label, and
  a third test's parse_code mock gained the new trust_fallback_id kwarg.

Ref polylogue-jc4q

Co-Authored-By: Claude <noreply@anthropic.com>
Ref polylogue-jc4q

Co-Authored-By: Claude <noreply@anthropic.com>
@Sinity
Sinity force-pushed the fix/lineage/subagent-identity-collision branch from 7fac0db to 8274cdb Compare July 31, 2026 16:25
@Sinity
Sinity merged commit 39d72ad into master Jul 31, 2026
3 checks passed
@Sinity
Sinity deleted the fix/lineage/subagent-identity-collision branch July 31, 2026 16:28
Sinity added a commit that referenced this pull request Jul 31, 2026
Ref polylogue-jc4q

Co-Authored-By: Claude <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