fix(sources): stop colliding Claude Code fork/resume carryover identity - #3472
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 22 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
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 |
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>
7fac0db to
8274cdb
Compare
Ref polylogue-jc4q Co-Authored-By: Claude <noreply@anthropic.com>
Summary
Fixes the dominant remaining cause of ambiguity in
claude-code-sessionrevision membership: fork/resume/usage-limit-quirk boundary records that Claude Code stamps with an ANCESTOR session'ssessionIdwere composing that ancestor's bare id as their own identity, colliding every such carryover fragment onto the ancestor'slogical_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-sessioncohorts resolved cleanly, far belowchatgpt-export/claude-ai-export(>90%). Deep-diving one cohort (logical key0213d48f...) 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
sessionIdeven 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/exitsent 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.pycomposedprovider_session_idfrom that in-recordsessionId, so every such fragment — from every fork/resume/quirk descendant of one ancestor — collided on the ancestor's ownlogical_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 cohortconflict, 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 largestsessionId-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'sparentUuidresolves 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_idstarting withagent-) 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 explicittrust_fallback_idflag (threaded throughLoweredPayloadSpec,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 ownsessionId" default, but only for these dispatch-proven carryover fragments — ordinary calls (the overwhelming majority, including most test call sites wherefallback_idis just a caller-supplied label) are unaffected. The ancestor id becomesparent_session_id, sosession_linksresolution carries the relationship instead of colliding revision membership on it.INDEX_SCHEMA_VERSIONbumped to 53 with aSEMANTIC_REPARSEIndexDeltaDeclaration(storage/sqlite/lifecycle.py) — this changessessions.native_id(generated fromprovider_session_id) andsession_linkslineage edges for affected raw acquisitions, so it requirespolylogue 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 intoclassify_membership_revisionsbecause it trips a real write-back invariant (archive.py'sapply_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 testacross 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}.py— 320 passed, 2 failures confirmed pre-existing and unrelated via a throwawaygit worktree add --detach origin/mastercheckout with zero of this PR's changes applied (same failures reproduce byte-for-byte): filed as polylogue-yl8t (streaming/eagerclaude_parse_coverageevent-order mismatch) and polylogue-ihro (Codex long-rollout message-count 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 unresolvedsession_linksedge instead of colliding.polylogue ops reset --index && polylogued runagainst 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