Skip to content

fix: distinguish subagent self-compaction from main-session acompact#3069

Merged
Sinity merged 3 commits into
masterfrom
feature/gpt-pro/lin03-subagent
Jul 18, 2026
Merged

fix: distinguish subagent self-compaction from main-session acompact#3069
Sinity merged 3 commits into
masterfrom
feature/gpt-pro/lin03-subagent

Conversation

@Sinity

@Sinity Sinity commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

agent-acompact-* is overloaded by Claude Code: a main-session auto-compact
replays 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 continuation
of 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 observed
agent-acompact-* files (9 at 0% overlap) are Task-subagent
self-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 (root
    sidechain user record, explicit agentId/promptId, no parentUuid,
    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 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 with an unresolvable/spawned-fresh parent)
    reclassifies to SIDECHAIN/spawned-fresh (branch_point_message_id
    explicitly 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) and
    child-before-parent (_reextract_prefix_tail_db, including the
    session_links natural-key PK-lane move when link_type changes from
    continuation to sidechain or vice versa).
  • polylogue/sources/dispatch.py: fix a twin-path fallback_id
    divergence flagged in the bead's own investigation notes — the eager
    grouping path (_claude_code_grouped_record_specs) now assigns
    fallback_id to the first group the same way 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 (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.
  • Companion fix (second commit): the demo/scenario corpus's own
    agent-acompact-demo.jsonl fixture reused a shared generic-workload
    session 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 --check generated
    surface (part of render all --check) — this is the exact
    misclassification 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_attempt construct
    query 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 passed
  • devtools 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 clean origin/master, unrelated to this change.
  • tests/integration/test_demo_daemon_convergence.py::test_import_demo_converges_through_live_daemon_path 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.
  • 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 - reports Verifier result: ok, 0 problems.
  • mypy clean on the whole polylogue/ package (1031 files) and on every touched module individually.
  • Anti-vacuity: temporarily forced _acompact_content_membership_ratio to always return 1.0 (the pre-patch unconditional-continuation behavior). Both parametrized cases (parent-first, child-first) of test_subagent_self_compaction_stays_whole_and_never_composes_main_prefix failed as expected ('continuation' != 'sidechain'), confirming the test exercises the real gate. Reverted after confirming.
  • Checked for collision with feat: harden archive continuity closure #3051 (orchestration.py / Workflow tool-call session events, already on master): its code_parser.py/dispatch.py changes are additive and touch an unrelated concept (explicit Workflow tool invocations), no overlap with the acompact classification path.
  • No schema/migration files touched (git diff --stat against polylogue/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:

WITH acompact AS (
    SELECT
        s.session_id,
        s.branch_type,
        l.link_type,
        l.inheritance,
        l.branch_point_message_id
    FROM sessions AS s
    LEFT JOIN session_links AS l
      ON l.src_session_id = s.session_id
     AND l.method = 'parser-parent'
    WHERE s.origin = 'claude-code-session'
      AND (
          s.native_id LIKE 'agent-acompact-%'
          OR s.native_id LIKE '%:agent-acompact-%'
      )
)
SELECT
    COALESCE(branch_type, '<NULL>') AS branch_type,
    COALESCE(link_type, '<NO-LINK>') AS link_type,
    COALESCE(inheritance, '<NULL>') AS inheritance,
    COUNT(DISTINCT session_id) AS sessions
FROM acompact
GROUP BY 1, 2, 3
ORDER BY sessions DESC, 1, 2, 3;

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-compactions
move to sidechain / sidechain / spawned-fresh; the 9 zero-overlap
artifacts move branch/link topology from continuation to sidechain (they
may already have had spawned-fresh inheritance from the older
zero-contiguous-alignment path).

Post-rebuild integrity check (expect 0):

WITH acompact AS (
    SELECT
        s.session_id,
        s.branch_type,
        l.link_type,
        l.inheritance,
        l.branch_point_message_id
    FROM sessions AS s
    LEFT JOIN session_links AS l
      ON l.src_session_id = s.session_id
     AND l.method = 'parser-parent'
    WHERE s.origin = 'claude-code-session'
      AND (
          s.native_id LIKE 'agent-acompact-%'
          OR s.native_id LIKE '%:agent-acompact-%'
      )
)
SELECT COUNT(*) AS malformed_sidechain_acompacts
FROM acompact
WHERE branch_type = 'sidechain'
  AND (
      link_type IS NULL
      OR link_type <> 'sidechain'
      OR inheritance IS NULL
      OR inheritance <> 'spawned-fresh'
      OR branch_point_message_id IS NOT NULL
  );

(Both queries verified against the current session_links schema in this
container — the method column and the 'parser-parent' literal it
filters on are confirmed present in polylogue/storage/sqlite/archive_tiers/index.py
and written by _write_session_link; only the live-corpus row counts are
unverified.)

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/TESTS
claims were independently re-verified against current code rather than
trusted, per the task's instructions. Not merged — left for coordinator
review.

@coderabbitai

coderabbitai Bot commented Jul 18, 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: 49 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

Run ID: 2ac79aaf-5f78-4553-832d-a8c170aacb69

📥 Commits

Reviewing files that changed from the base of the PR and between 2d8f689 and 00c889e.

📒 Files selected for processing (18)
  • docs/design/session-lineage-model.md
  • docs/plans/demo-corpus-construct-audit.md
  • docs/plans/topology-target.yaml
  • docs/topology-status.md
  • polylogue/demo/constructs.py
  • polylogue/demo/seed.py
  • polylogue/scenarios/__init__.py
  • polylogue/scenarios/corpus.py
  • polylogue/sources/dispatch.py
  • polylogue/sources/parsers/claude/code_parser.py
  • polylogue/storage/sqlite/archive_tiers/write.py
  • tests/fixtures/claude-code/normalization-lineage-subagent-acompact.jsonl
  • tests/unit/demo/test_demo_construct_anti_vacuity.py
  • tests/unit/demo/test_demo_seed_verify.py
  • tests/unit/scenarios/test_corpus.py
  • tests/unit/sources/test_claude_code_normalization_laws.py
  • tests/unit/sources/test_compaction.py
  • tests/unit/sources/test_dispatch_payloads.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/gpt-pro/lin03-subagent

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

Comment on lines +267 to +270
membership = _acompact_content_membership_ratio(
parent_composed,
_parsed_acompact_prefix_signatures(messages),
)

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

Sinity and others added 3 commits July 18, 2026 05:04
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>
@Sinity
Sinity force-pushed the feature/gpt-pro/lin03-subagent branch from 2164314 to 00c889e Compare July 18, 2026 03:08
@Sinity
Sinity merged commit 2b755f9 into master Jul 18, 2026
1 of 2 checks passed
@Sinity
Sinity deleted the feature/gpt-pro/lin03-subagent branch July 18, 2026 03:08
Sinity added a commit that referenced this pull request Jul 20, 2026
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