Repository navigation
fix(sessions): latch sidechain flag only when explicitly present - #1468
senanurcetin wants to merge 5 commits into
Conversation
Preserve a missing isSidechain value until the first explicit boolean appears in incremental SessionStore transcript entries. Include regressions for mixed metadata entries, batched appends, explicit false values, and InMemorySessionStore list filtering. Local verification: 1,633 passed, 21 skipped; Ruff and targeted mypy clean.
A metadata-only first JSONL line can precede the first isSidechain flag. The disk lite-reader previously ignored a later explicit flag and diverged from the incremental SessionStore summary. Read the first top-level boolean flag in the bounded head buffer and add disk/store parity regressions. Local verification: 1,636 passed, 21 skipped; Ruff clean.
mangeshraut712
left a comment
There was a problem hiding this comment.
Checking isinstance(entry.get('isSidechain'), bool) in fold_session_summary ensures that initial header or summary records without an isSidechain key do not prematurely latch is_sidechain=False and mask subsequent subagent entries.
Matching this in _parse_session_info_from_lite by checking lines across head preserves parity between disk fast-path listing and in-memory session folding. The unit and parity tests in test_session_summary.py cover append batches and nested metadata checks thoroughly.
mangeshraut712
left a comment
There was a problem hiding this comment.
Thanks for the fix — the "first explicit boolean wins" semantics in fold_session_summary and the new regression tests are clear, and all 57 tests in tests/test_session_summary.py pass locally.
One parity gap remains: the lite fast path only scans head (the first 64 KiB, LITE_READ_BUF_SIZE), and tail — which _read_session_lite already reads — is never scanned for isSidechain. src/claude_agent_sdk/_internal/sessions.py:438 still misses a subagent whose first explicit "isSidechain":true lands after the head windowestrategy, so the batch path included a session the fold path excluded.
Reproduction (all entries before the flag are valid JSON, no truncation at the boundary):
lines = ['{"type":"summary","summary":"agent task"}',
'{"type":"user","message":{"role":"user","content":"first prompt"}}']
lines += ['{"type":"assistant","message":{"role":"assistant","content":"'+'p'*7000+'"}}' for _ in range(10)]
lines.append('{"type":"user","message":{"role":"user","content":"subagent prompt"},"isSidechain":true}')
jsonl = "\n".join(lines) + "\n" # 70,848 bytes; flag line starts past byte 65,536
lite = _jsonl_to_lite(jsonl, 0)
_parse_session_info_from_lite("sid", lite, DIR) # -> SDKSessionInfo (included)
fold_session_summary(None, key, [json.loads(l) for l in lines]) # is_sidechain True -> summary_entry_to_sdk_info -> None (excluded)A subagent transcript with a large tool result or long assistant context before its first user message easily exceeds 64 KiB, so this is the same "subagent appears in the regular session list" bug this PR fixes — it just moved from "first line" to "first 64 KiB". The same edge applies to a JSON line that straddles the head/tail read boundary: json.loads fails via the continue on ValueError at line 443 and the flag is silently dropped even though the full line sits in tail.
Suggest scanning tail as well (it is already in memory; if head and tail overlap, dedupe by size/last-newline head.find(), and skip lines already seen in head when tail == head), and/or adding a test with a >64 KiB transcript where the first explicit flag is past the window.
The lite disk reader only considered isSidechain flags in the first 64 KiB, so long subagent transcripts could be listed as main sessions. Determine the first explicitly present boolean from the sampled bytes, reconstruct overlapping head/tail reads exactly for <=128 KiB files, and inspect the tail for larger transcripts without extra file reads. Add six regression tests for late flags, head false precedence, long JSONL lines spanning read boundaries, real disk reads, disjoint tail windows and multibyte UTF-8. Verified locally: 1,642 SDK tests passed, 21 skipped; 63 session summary tests, Ruff lint/format, and mypy passed.
|
Thanks @mangeshraut712 for the detailed reproduction and the boundary-case analysis. I inspected the current PR head ( One additional consideration: scanning the tail alone may not guarantee first explicit boolean wins for larger transcripts, because the first flag could occur in the unexamined middle. A complete fix should preserve that ordering, potentially using a streaming fallback when the head contains no explicit flag. The regression coverage should include:
The current implementation does not resolve these cases yet. No passing validation or code fix is claimed by this response. |
For transcripts exceeding two 64-KiB windows, sampled head/tail flags cannot prove which explicit top-level boolean occurred first. Preserve the fast path when the complete head is decisive; otherwise scan JSONL records sequentially, on disk via a file iterator and in the already resident store bytes via BytesIO. Never allow a tail flag to override an earlier explicit flag in the unobserved middle. Add five regressions for conflicting middle/tail booleans, oversized single JSONL lines, Unicode-escaped flag keys and head fast-path preservation. These fail before and pass after the fix. Local verification: 68/68 session summary tests; 1,647 SDK tests passed, 21 skipped; Ruff lint/format and targeted mypy passed.
|
@mangeshraut712 follow-up on the head/tail parity gap you found: it's fixed in two commits, and the current head is
New regression tests cover:
The Locally at Would you mind re-running your boundary reproduction against |
Keep this PR to the fold_session_summary fix and its regression tests. The head/tail and ordered JSONL scans added to _read_session_lite and _jsonl_to_lite could read a whole large transcript per session during list_sessions(), which the lite reader exists to avoid. The disk lite path goes back to its original first-line check, matching the TypeScript SDK. A later first explicit flag on that path is a known limitation, left for a separate issue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@mangeshraut712 thanks again for the reproduction. You were right that the disk lite path can disagree with the fold when the first explicit flag comes after the head. I've cut the PR back to the So the gap you found is still there: |
Summary
Subagent transcripts could appear as regular sessions in
list_sessions_from_store(). If the first entry had noisSidechainkey (for example asummaryrecord),fold_session_summary()latchedis_sidechain=Falseand ignored a later explicitisSidechain: true.Changes
session_summary.py: latchis_sidechainonly when the entry has an explicit booleanisSidechain. The first explicit value wins, including across append batches.tests/test_session_summary.py: threefold_session_summaryregression tests (flag after an unmarked first entry, flag across append batches, explicitfalsestays latched) and onelist_sessions_from_storefast-path test.No public API or CLI protocol changes.
Out of scope
The disk lite reader (
_parse_session_info_from_lite()) is unchanged. It still checks only the first line of the 64 KiB head window, as the TypeScript SDK does. A transcript whose first explicitisSidechaincomes after the first line is still listed bylist_sessions(). Fixing that without reading more than the head and tail on the listing hot path needs its own discussion, so I've left it for a separate issue.Testing
On
1d7344b(Windows, Python 3.11): full suite1633 passed, 21 skipped;tests/test_session_summary.py54 passed;ruff check,ruff format --checkandmypy src/ scripts/clean.AI assistance
This contribution was developed with AI assistance (ChatGPT). The bug was reproduced, regression tests were added, and the changes were verified against the local SDK test suite before submission.
The rework that reduced this PR to the minimal fix was done with Claude Code (Claude Opus 5.5).