fix: distinguish subagent self-compaction from main-session acompact#3069
Conversation
|
Warning Review limit reached
Next review available in: 49 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 Run ID: 📒 Files selected for processing (18)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2164314177
ℹ️ 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".
| membership = _acompact_content_membership_ratio( | ||
| parent_composed, | ||
| _parsed_acompact_prefix_signatures(messages), | ||
| ) |
There was a problem hiding this comment.
Reclassify acompact children after parent revisions
When a genuine main-session acompact is ingested while its parent contains only a partial/hot-file revision, this snapshot can fall below 90% and permanently classify the child as a fresh sidechain. On a later parent replacement, _resolve_session_graph invokes _reextract_prefix_tail_db only for links whose resolved_dst_session_id IS NULL; this child's link is already resolved, so the completed parent never reruns the membership check, and an unchanged child is normally skipped by content hash. The resulting topology therefore depends on revision timing until the index is reset; resolved acompact children should be reevaluated when parent content changes.
Useful? React with 👍 / 👎.
Problem: Claude Code's agent-acompact-* artifact family is overloaded. The parser unconditionally classified every agent-acompact-* record as a CONTINUATION of its main session, but ~39/187 observed files (9 at 0% overlap) are actually Task-subagent self-compactions replaying only their own fresh transcript. Composition then prepended the wrong (main-session) transcript onto these subagent reads. What changed: - code_parser.py: add `_is_fresh_task_prompt_head`, a narrow structural predicate (root sidechain user record with explicit agentId/promptId, no parentUuid, not human-authored) that gives a conservative SIDECHAIN hint when the parser can only see one JSONL group in isolation. - write.py: add the authoritative gate — a bounded multiset content-membership ratio (90% threshold) between the child's pre-summary prefix and the parent's composed transcript, computed once parent content is resolvable. Below threshold (or no fresh-head hint and a spawned-fresh parent) reclassifies to SIDECHAIN/spawned-fresh (branch_point_message_id explicitly NULL, never left dangling); at or above threshold, parent-content membership overrides even a conservative fresh-head hint back to CONTINUATION/prefix-sharing. Both ingest orders are handled: parent-known (`write_parsed_session_to_archive`) and child-before-parent (`_reextract_prefix_tail_db`, including the session_links natural-key PK-lane move when link_type changes). - dispatch.py: fix a twin-path fallback_id divergence — the eager grouping path (`_claude_code_grouped_record_specs`) now assigns `fallback_id` to the first group like the streaming path (`_claude_code_stream_sessions`) already did, so composed_session_id and classification agree between both ingest routes. - docs/design/session-lineage-model.md: correct the stale claim that all 187 agent-acompact-* sessions are 100% main-session copies; ~148 are, ~39 are Task self-compactions below the 90% membership threshold. - New fixture + regression tests for both cases (true main-session acompact -> parent=main; subagent self-compact -> sidechain, no inherited prefix), each parametrized over parent-first/child-first ingest order, plus a dispatch-path-parity test. Verification: - devtools test tests/unit/sources/test_compaction.py tests/unit/sources/test_claude_code_normalization_laws.py tests/unit/sources/test_dispatch_payloads.py -> 56 passed - mypy --strict clean on the three changed modules - ruff check + format --check clean - Anti-vacuity: temporarily forced `_acompact_content_membership_ratio` to always return 1.0 (pre-patch unconditional-continuation behavior); both parametrized cases of test_subagent_self_compaction_stays_whole_and_never_composes_main_prefix failed as expected, confirming the test exercises the real gate; reverted after confirming. - No schema/migration files touched (git diff --stat against storage/sqlite/migrations/ is empty) — pure logic fix. - Checked for collision with #3051 (orchestration.py / Workflow tool events, already on master): its code_parser.py/dispatch.py changes are additive and touch an unrelated concept (explicit Workflow tool-call session events), no overlap with the acompact classification path. Live re-ingest of the ~39 known-mismatched files driving the mis-parented-subagent-acompact count to zero remains an operator step (requires the live archive) and is not verified in this container. Ref polylogue-4ts.3 Ref gh-2471 Co-Authored-By: Claude <noreply@anthropic.com>
Problem: the previous commit's write.py content-membership gate broke the
required `devtools render demo-corpus-datasheet --check` generated-surface
gate (part of `render all --check` / the pre-push `devtools verify --quick`
hook). The demo/scenario corpus's `agent-acompact-demo.jsonl` fixture
(polylogue/demo/seed.py `_write_demo_lineage_sources`, predating this
branch) reused the shared generic-workload session
(`DEMO_CLAUDE_CODE_SESSION_ID` / `claude-code/demo-00.jsonl`, a large
synthetic corpus used by several unrelated demo families) as its "parent",
but authored its own distinct clock-test narrative rather than literally
replaying that parent's content. Under the old unconditional-continuation
classifier this went unnoticed; the new >= 90% membership gate correctly
rejected it (0% real overlap) and reclassified it as SIDECHAIN, flipping
`continuation_links` from 1 to 0 and `sidechain_sessions` from 1 to 2 in
the generated datasheet. This is the same misclassification shape the bead
describes, just previously hiding in the demo corpus's own fixtures — the
gate is working as intended, the fixture wasn't construct-valid.
What changed:
- polylogue/scenarios/corpus.py: give the compaction fixture its own
dedicated parent session id (`DEMO_CLAUDE_CODE_LINEAGE_COMPACTION_PARENT_
SESSION_ID`, raw id `demo-lineage-compaction-parent`) instead of reusing
the shared generic-workload session, so this fixture pair's content can
legitimately match without touching the many other constructs that key
off the generic session.
- polylogue/demo/seed.py: extract the compaction narrative (flaky-clock-test
fix, including the structurally failed edit attempt) into a shared
`_compaction_narrative_records` helper, write it once as the new
dedicated parent session (`claude-code/lineage-compaction-parent.jsonl`)
and replay it verbatim into `agent-acompact-demo.jsonl` before the
summary — matching real Claude Code auto-compact behavior (replay whole
conversation + summary) and clearing the >= 90% membership gate honestly
(100% match).
- polylogue/demo/constructs.py: `compaction_omits_failed_attempt`'s SQL
now checks for the failed action across {child, parent} physical session
ids. Genuine prefix-sharing dedup (#2467) now correctly stores the
replayed failed-attempt row once, under the parent's own physical
session, not duplicated under the child — a raw per-session query has to
account for that the same way a composed transcript read already does
(recompose parent-up-to-branch + child tail).
- tests/unit/demo/test_demo_construct_anti_vacuity.py,
tests/unit/demo/test_demo_seed_verify.py, tests/unit/scenarios/test_corpus.py:
updated to the new parent id / physical-storage location and to the
corrected expectation. test_demo_seed_verify.py previously asserted
`("continuation", "spawned-fresh") in links` — that pairing (a
"continuation" link_type with "spawned-fresh" inheritance, i.e. no
verified shared content) is exactly the incoherent state polylogue-4ts.3
is about; corrected to `("continuation", "prefix-sharing")`, the
genuinely-verified pairing.
Verification:
- devtools render demo-corpus-datasheet --output - -> "Verifier result: ok",
0 problems, docs/plans/demo-corpus-construct-audit.md regenerated in sync.
- devtools verify --quick -> exit 0 (ruff format/check, mypy, render all,
topology/layering/closure-matrix/manifests/ci-workflows/doc-commands/
docs-coverage/test-infra-currency/test-clock-hygiene/pytest-timeout-
overrides/degrade-loudly all ok).
- mypy (whole polylogue/ package, 1031 files) -> no issues.
- devtools test tests/unit/sources tests/unit/demo tests/unit/scenarios
tests/unit/cli/test_demo_command.py -> 1825 passed, 2 pre-existing
failures unrelated to this change (ChatGPT DOM-fallback fidelity test,
reproduced identically on clean origin/master).
- tests/integration/test_demo_daemon_convergence.py::
test_import_demo_converges_through_live_daemon_path also reproduces
identically on clean origin/master (live-daemon 20s convergence timeout
plus several unrelated missing constructs — browser-capture, embeddings,
gemini session-id suffix drift); confirmed pre-existing, not caused by
either commit on this branch.
Ref polylogue-4ts.3
Co-Authored-By: Claude <noreply@anthropic.com>
2164314 to
00c889e
Compare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QUsH3Rhq6oAZpYPWcsZqnZ
Summary
agent-acompact-*is overloaded by Claude Code: a main-session auto-compactreplays the whole parent transcript (genuine continuation), but a Task
subagent can self-compact under the same filename prefix (a fresh sidechain
with no relation to the main session's content). The parser previously
classified every
agent-acompact-*record as an unconditional continuationof its main session, so composed reads of subagent self-compactions
incorrectly prepended the wrong (main-session) transcript.
Problem
Code-confirmed (gh#2471, bead
polylogue-4ts.3): ~39/187 observedagent-acompact-*files (9 at 0% overlap) are Task-subagentself-compactions with less than 90% content overlap with their claimed
main session, yet were stored/composed as if they were verbatim
continuations of it.
Solution
polylogue/sources/parsers/claude/code_parser.py: add_is_fresh_task_prompt_head, a narrow structural predicate (rootsidechain user record, explicit
agentId/promptId, noparentUuid,not human-authored) giving a conservative SIDECHAIN hint when the parser
can only see one JSONL group in isolation.
polylogue/storage/sqlite/archive_tiers/write.py: the authoritativegate — a bounded multiset content-membership ratio (90% threshold)
between the child's pre-summary prefix and the parent's composed
transcript, computed once parent content is resolvable. Below threshold
(or no fresh-head hint with an unresolvable/spawned-fresh parent)
reclassifies to SIDECHAIN/
spawned-fresh(branch_point_message_idexplicitly NULL, never left dangling — respects the documented
not-a-FK invariant). At or above threshold, parent-content membership
overrides even a conservative fresh-head hint back to
CONTINUATION/
prefix-sharing. Both ingest orders are handled:parent-known (
write_parsed_session_to_archive) andchild-before-parent (
_reextract_prefix_tail_db, including thesession_linksnatural-key PK-lane move whenlink_typechanges fromcontinuation to sidechain or vice versa).
polylogue/sources/dispatch.py: fix a twin-pathfallback_iddivergence flagged in the bead's own investigation notes — the eager
grouping path (
_claude_code_grouped_record_specs) now assignsfallback_idto the first group the same way the streaming path(
_claude_code_stream_sessions) already did, socomposed_session_idand classification agree between both ingest routes.
docs/design/session-lineage-model.md: correct the stale claim that all187
agent-acompact-*sessions are 100% main-session copies; ~148 are,~39 are Task self-compactions below the 90% membership threshold.
tests/fixtures/claude-code/normalization-lineage-subagent-acompact.jsonl)and regression tests for both cases (true main-session acompact ->
parent=main; subagent self-compact -> sidechain, no inherited prefix),
each parametrized over parent-first/child-first ingest order, plus a
dispatch-path-parity test.
agent-acompact-demo.jsonlfixture reused a shared generic-workloadsession as its "parent" but authored unrelated content, relying on the
old unconditional-continuation classifier. The new membership gate
correctly rejected it (0% real overlap) and flipped it to SIDECHAIN,
breaking the
devtools render demo-corpus-datasheet --checkgeneratedsurface (part of
render all --check) — this is the exactmisclassification shape the bead describes, just hiding in the demo
corpus's own fixtures. Gave the fixture a dedicated parent session with
literally-replayed content (matching real Claude Code auto-compact
behavior) and updated the
compaction_omits_failed_attemptconstructquery and its anti-vacuity test to check both physical locations
(child + parent), since genuine prefix-sharing dedup now stores the
replayed failed-attempt row once under the parent rather than
duplicating it under the child.
Verification
devtools test tests/unit/sources/test_compaction.py tests/unit/sources/test_claude_code_normalization_laws.py tests/unit/sources/test_dispatch_payloads.py-> 56 passeddevtools test tests/unit/sources tests/unit/demo tests/unit/scenarios tests/unit/cli/test_demo_command.py-> 1825 passed; 2 pre-existing failures (test_chatgpt_normalization_survivors.py, ChatGPT DOM-fallback fidelity) reproduced identically on cleanorigin/master, unrelated to this change.tests/integration/test_demo_daemon_convergence.py::test_import_demo_converges_through_live_daemon_pathreproduces identically on cleanorigin/master(live-daemon 20s convergence timeout plus several unrelated missing constructs — browser-capture, embeddings, gemini session-id suffix drift); confirmed pre-existing.devtools verify --quick-> exit 0 (ruff format/check, mypy,render all, topology/layering/closure-matrix/manifests/ci-workflows/doc-commands/docs-coverage/test-infra-currency/test-clock-hygiene/pytest-timeout-overrides/degrade-loudly all ok).devtools render demo-corpus-datasheet --output -reportsVerifier result: ok, 0 problems.mypyclean on the wholepolylogue/package (1031 files) and on every touched module individually._acompact_content_membership_ratioto always return1.0(the pre-patch unconditional-continuation behavior). Both parametrized cases (parent-first, child-first) oftest_subagent_self_compaction_stays_whole_and_never_composes_main_prefixfailed as expected ('continuation' != 'sidechain'), confirming the test exercises the real gate. Reverted after confirming.orchestration.py/ Workflow tool-call session events, already onmaster): itscode_parser.py/dispatch.pychanges are additive and touch an unrelated concept (explicitWorkflowtool invocations), no overlap with the acompact classification path.git diff --statagainstpolylogue/storage/sqlite/migrations/is empty) — pure logic fix, consistent with the bead's "no schema change" framing.Follow-up / operator verification (cannot be run in this container — no live archive)
After a live re-ingest against the real ~38GB archive (
polylogue ops reset --index && polylogued run, or targeted reprocess of the ~39 known-mismatched files), confirm the reclassification actually lands. Pre/post-rebuild probe:Expected movement (not verified in this container): total acompact session
count stays ~187; ~148 true main-session artifacts read
continuation / continuation / prefix-sharing; ~39 Task self-compactionsmove to
sidechain / sidechain / spawned-fresh; the 9 zero-overlapartifacts move branch/link topology from continuation to sidechain (they
may already have had
spawned-freshinheritance from the olderzero-contiguous-alignment path).
Post-rebuild integrity check (expect
0):(Both queries verified against the current
session_linksschema in thiscontainer — the
methodcolumn and the'parser-parent'literal itfilters on are confirmed present in
polylogue/storage/sqlite/archive_tiers/index.pyand written by
_write_session_link; only the live-corpus row counts areunverified.)
Ref polylogue-4ts.3
Ref gh-2471
Generated from a review/verification pass over an externally-authored
patch (packet
lin-03-subagent-compaction-r01); its own HANDOFF/TESTSclaims were independently re-verified against current code rather than
trusted, per the task's instructions. Not merged — left for coordinator
review.