Skip to content

Commit 8c831cb

Browse files
committed
fix: skip corrupt/truncated lines in import_session_to_store
_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.
1 parent 5513b20 commit 8c831cb

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

src/claude_agent_sdk/_internal/session_import.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,22 @@ async def _append_jsonl_file_in_batches(
128128
"""Stream-read a JSONL file line-by-line, parsing each line and flushing to
129129
``store.append()`` in batches of ``batch_size`` entries (or
130130
``MAX_PENDING_BYTES`` of line text, whichever comes first). Skips blank
131-
lines."""
131+
lines and corrupt/truncated lines."""
132132
batch: list[SessionStoreEntry] = []
133133
nbytes = 0
134134
with file_path.open(encoding="utf-8") as f:
135135
for line in f:
136136
line = line.rstrip("\n")
137137
if not line:
138138
continue
139-
batch.append(json.loads(line))
139+
try:
140+
entry = json.loads(line)
141+
except (json.JSONDecodeError, ValueError):
142+
# Match the read path (_parse_transcript_entries): a
143+
# partially-written final line — the normal outcome when the
144+
# CLI is killed mid-append — is skipped, not raised.
145+
continue
146+
batch.append(entry)
140147
nbytes += len(line)
141148
if len(batch) >= batch_size or nbytes >= MAX_PENDING_BYTES:
142149
await store.append(key, batch)

tests/test_session_import.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,30 @@ async def test_skips_blank_lines(
109109
key: SessionKey = {"project_key": project_key, "session_id": SESSION_ID}
110110
assert store.get_entries(key) == [_entry(0), _entry(1)]
111111

112+
@pytest.mark.anyio
113+
async def test_skips_corrupt_truncated_line(
114+
self, claude_dir: Path, cwd: Path, project_key: str
115+
) -> None:
116+
"""A partially-written final line (the normal outcome when the CLI is
117+
killed mid-append) is skipped, not raised — matching the read path."""
118+
path = claude_dir / f"{SESSION_ID}.jsonl"
119+
path.write_text(
120+
json.dumps(_entry(0))
121+
+ "\n"
122+
+ json.dumps(_entry(1))
123+
+ "\n"
124+
+ '{"type": "user", "uuid": "u2", "messa', # truncated, unterminated
125+
encoding="utf-8",
126+
)
127+
128+
store = InMemorySessionStore()
129+
await import_session_to_store(
130+
SESSION_ID, store, directory=str(cwd), batch_size=1
131+
)
132+
133+
key: SessionKey = {"project_key": project_key, "session_id": SESSION_ID}
134+
assert store.get_entries(key) == [_entry(0), _entry(1)]
135+
112136
@pytest.mark.anyio
113137
async def test_nonpositive_batch_size_uses_default(
114138
self, claude_dir: Path, cwd: Path, project_key: str

0 commit comments

Comments
 (0)