-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Problem
The Claude Agent SDK's v2 session API (unstable_v2_createSession / unstable_v2_resumeSession) does not reliably persist sessions. When session.close() is called after streaming completes, it sends SIGTERM to the subprocess immediately, killing it before it can write session data to disk. This means unstable_v2_resumeSession() either fails outright or starts a new session with no context from the previous conversation.
Upstream issue: anthropics/claude-agent-sdk-typescript#177
Evidence
Two prototype scripts (prototypes/test-v1-session.ts and prototypes/test-v2-session.ts) that test session persistence with each API using the same flow:
- Send a prompt containing a secret code ("PINEAPPLE-42")
- Capture the session ID
- Wait 3 seconds, then resume the session
- Ask the model to recall the secret code
v1 query() API — works
--- v1 query() [new] ---
Response: PINEAPPLE-42 acknowledged.
Session ID: 4ff709d2-5111-4bd1-a8f7-0be073839e51
--- v1 query() [resume:4ff709d2-...] ---
Response: The secret code you shared earlier was **PINEAPPLE-42**.
Session ID: 4ff709d2-5111-4bd1-a8f7-0be073839e51 (SAME)
Session persisted: YES
The v1 query() returns an AsyncGenerator. When iteration completes, the subprocess exits naturally, giving it time to flush session data to disk before the process ends.
v2 session API — broken
--- v2 createSession [new] ---
Response: PINEAPPLE-42 acknowledged.
Session ID: 24d643eb-425a-4167-b388-3f537e0602e4
--- v2 resumeSession [24d643eb-...] ---
Response: (empty, 0 chars)
Session ID: 30bf9ac1-a436-425a-9fbe-b80969afa7ba (DIFFERENT!)
Result subtype: error_during_execution
Session persisted: NO
session.close() sends SIGTERM immediately. The subprocess is killed before it can persist the session. On resume, a different session ID is returned and the model has no context from the first interaction.
Root cause
The v2 API requires calling session.close() to clean up resources after streaming completes. However, close() sends SIGTERM to the Claude Code subprocess, which kills it before the internal session persistence logic (writing to ~/.claude/projects/) can finish.
The v1 query() API doesn't have this problem because the subprocess lifecycle is tied to the AsyncGenerator — it exits naturally when the query is done, after persisting session data.
Additional notes
- Setting
persistSession: truedoes not help with the v2 API (theSDKSessionOptionstype doesn't even include this field; it must be spread in as an escape hatch) - The v2 API is marked as
@alpha/UNSTABLE, so this may be a known limitation - SDK version tested:
@anthropic-ai/[email protected]
Resolution
Switched src/claude-code.ts from the v2 API to the v1 query() API:
- Replace
unstable_v2_createSession/unstable_v2_resumeSessionwithquery() - Use
Options.resumeto resume existing sessions (instead ofunstable_v2_resumeSession) - Use
Options.abortControllerfor cancellation (instead ofsession.close()) - Only call
q.close()on timeout to force-terminate; otherwise the subprocess exits naturally
This gives us reliable session persistence — the subprocess has time to write session data before exiting.