The Agent SDK assigns a session id and persists conversation state internally. With ClaudeSDKClient, the session lives as long as the client; with query(), it is single-use. You can resume a session by id to continue where you left off — the SDK reloads internal state and applies your new prompt.
JSONL as the durable record
Your code can write every message the SDK yields to a JSONL file the moment it arrives. Each line is a typed record: assistant text and tool calls, tool results, streamed deltas, the final result. cwkPippa makes this its ground truth — SQLite and ChromaDB are derived mirrors that can be rebuilt from the JSONL. Recovery is purge-and-replay, not patch-and-reconcile.
Resume across processes
If your worker process restarts, you can spin up a new ClaudeSDKClient with resume set to the stored session id, and the CLI reloads that session. cwkPippa took the other road on 2026-05-14: it stopped resuming CLI sessions and replays the whole conversation from its own record into a fresh client every turn, so a restart loses nothing its JSONL holds. Both are legitimate; pick one and know which one you picked.
Principle: Treat JSONL as the source of truth. Anything else (DB rows, indices) is a cache that must be rebuildable.
Code
Write every SDK message to JSONL before showing it·python
import dataclasses, json, pathlib
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
def to_record(message) -> dict:
# SDK messages are dataclasses (there is no .dict()); asdict() walks nested blocks too.
return {"type": type(message).__name__, **dataclasses.asdict(message)}
async def run_with_jsonl(conv_id: str, prompt: str, forward_to_user):
log_path = pathlib.Path(f"/var/lib/myagent/sessions/{conv_id}.jsonl")
log_path.parent.mkdir(parents=True, exist_ok=True)
options = ClaudeAgentOptions(cwd="/srv", include_partial_messages=True) # deltas arrive as StreamEvent
with log_path.open("a", buffering=1) as log: # line-buffered
async with ClaudeSDKClient(options=options) as client:
await client.query(prompt)
async for message in client.receive_response():
log.write(json.dumps(to_record(message), default=str) + "\n")
log.flush() # write before show
forward_to_user(message)
Resume a session by id·python
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
# Every ResultMessage (and every streamed message) carries the CLI session's id.
# Store it beside the conversation; here it came from the last turn's result.
session_id = last_result.session_id
options = ClaudeAgentOptions(cwd="/srv", resume=session_id) # the CLI reloads that session
async with ClaudeSDKClient(options=options) as client:
await client.query("Continue where we left off.")
async for message in client.receive_response():
print(message)
Add JSONL logging to one Agent SDK call in your project, with partial messages on. Crash the process mid-stream. Restart with resume set to the stored session id and confirm you can continue the conversation. Verify the JSONL holds every delta the crashed turn streamed before it died.
Hint
If your downstream store and the JSONL disagree, trust the JSONL — purge and replay. That is the discipline.
Progress
Progress is local-only — sign in to sync across devices.