Skip to content
C.W.K.
Stream
Lesson 07 of 08 · published

Sessions, Resume, and JSONL Truth

~16 min · sessions, resume, jsonl, persistence

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

Sessions are the SDK's memory

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)

External links

Exercise

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.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.