From fa9f0032924b73bf028e6011a0f807f18863c13b Mon Sep 17 00:00:00 2001 From: otiscuilei Date: Wed, 8 Jul 2026 18:31:46 +0800 Subject: [PATCH] fix: skip corrupt/truncated lines in import_session_to_store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _append_jsonl_file_in_batches parsed each line with a bare json.loads(line), so a partially-written final line — the normal outcome when the CLI is killed mid-append to a .jsonl — made import_session_to_store raise json.JSONDecodeError. Every read path in the package tolerates this: _parse_transcript_entries (used by get_session_messages) wraps json.loads in try/except and skips corrupt lines, so the same file reads fine. Because entries flush in batches, a large session also left the store partially populated before the crash. Skip corrupt/truncated lines to match the read path. import_session_to_store's documented Raises (ValueError, FileNotFoundError) already excludes JSONDecodeError, so this aligns behavior with the contract. Adds a regression test. --- .../_internal/session_import.py | 11 +++++++-- tests/test_session_import.py | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/claude_agent_sdk/_internal/session_import.py b/src/claude_agent_sdk/_internal/session_import.py index 30d7823bb..4798f1afd 100644 --- a/src/claude_agent_sdk/_internal/session_import.py +++ b/src/claude_agent_sdk/_internal/session_import.py @@ -128,7 +128,7 @@ async def _append_jsonl_file_in_batches( """Stream-read a JSONL file line-by-line, parsing each line and flushing to ``store.append()`` in batches of ``batch_size`` entries (or ``MAX_PENDING_BYTES`` of line text, whichever comes first). Skips blank - lines.""" + lines and corrupt/truncated lines.""" batch: list[SessionStoreEntry] = [] nbytes = 0 with file_path.open(encoding="utf-8") as f: @@ -136,7 +136,14 @@ async def _append_jsonl_file_in_batches( line = line.rstrip("\n") if not line: continue - batch.append(json.loads(line)) + try: + entry = json.loads(line) + except (json.JSONDecodeError, ValueError): + # Match the read path (_parse_transcript_entries): a + # partially-written final line — the normal outcome when the + # CLI is killed mid-append — is skipped, not raised. + continue + batch.append(entry) nbytes += len(line) if len(batch) >= batch_size or nbytes >= MAX_PENDING_BYTES: await store.append(key, batch) diff --git a/tests/test_session_import.py b/tests/test_session_import.py index dae428c93..6a714cb08 100644 --- a/tests/test_session_import.py +++ b/tests/test_session_import.py @@ -109,6 +109,30 @@ async def test_skips_blank_lines( key: SessionKey = {"project_key": project_key, "session_id": SESSION_ID} assert store.get_entries(key) == [_entry(0), _entry(1)] + @pytest.mark.anyio + async def test_skips_corrupt_truncated_line( + self, claude_dir: Path, cwd: Path, project_key: str + ) -> None: + """A partially-written final line (the normal outcome when the CLI is + killed mid-append) is skipped, not raised — matching the read path.""" + path = claude_dir / f"{SESSION_ID}.jsonl" + path.write_text( + json.dumps(_entry(0)) + + "\n" + + json.dumps(_entry(1)) + + "\n" + + '{"type": "user", "uuid": "u2", "messa', # truncated, unterminated + encoding="utf-8", + ) + + store = InMemorySessionStore() + await import_session_to_store( + SESSION_ID, store, directory=str(cwd), batch_size=1 + ) + + key: SessionKey = {"project_key": project_key, "session_id": SESSION_ID} + assert store.get_entries(key) == [_entry(0), _entry(1)] + @pytest.mark.anyio async def test_nonpositive_batch_size_uses_default( self, claude_dir: Path, cwd: Path, project_key: str