Add resume_session_at / resume_drops_turn for truncating resume - #1198
Conversation
The CLI's headless lane can resume a session truncated at an earlier transcript entry (`--resume-session-at=<uuid>`) and, since 2.1.223, validate that truncation against a declared discarded turn (`--resume-drops-turn=<prompt uuid>`): every entry past the fork point must be attributable to that turn, otherwise the resume is refused with an `error_during_execution` result whose message starts with `Resume rejected by --resume-drops-turn:` -- so a caller rewinding to "before my last prompt" cannot silently discard a queued message or task notification the session absorbed mid-turn that the caller never saw. The TypeScript SDK exposes the pair as resumeSessionAt / resumeDropsTurn; the Python SDK had neither. Add both options to ClaudeAgentOptions and forward them in equals form with the same Windows cmd metacharacter rejection as resume/session_id. resume_drops_turn is forwarded whenever it is not None (an empty string reaches the CLI and is rejected as malformed rather than being dropped here and silently disarming the guard), matching the TypeScript transport. A refused resume is reported by the CLI as an error result on stdout followed by exit 1, before it answers the SDK's `initialize` request. The read loop already replaced the bare "exit code 1" ProcessError with the result's error text for the message stream, but pending control requests -- including that in-flight initialize -- still received the raw exception, so callers saw "Command failed with exit code 1" with the actual reason discarded. Pending control requests now get a ProcessError carrying the same "Claude Code returned an error result: ..." text. Unit tests cover flag emission (incl. the empty-string case and Windows rejection) and the pending-initialize error text. E2E tests build a two-turn session, fork it at the end of turn 1 with a matching resume_drops_turn (succeeds; forked transcript is turn 1 + the new turn) and with a non-matching one (refused; ProcessError carries the prefix; source session untouched).
|
@claude please review this PR for correctness, edge cases, and test coverage. |
|
Claude finished @qing-ant's task in 2m 13s —— View job Code Review: Add
|
No-Verification-Needed: docstring-only change
chrislloyd
left a comment
There was a problem hiding this comment.
Stamp via #claude-code-shadow-stamps
Port of Python SDK commit be2d0df (anthropics/claude-agent-sdk-python#1198). Fixes #586
) Port of Python SDK commit be2d0df (anthropics/claude-agent-sdk-python#1198). Fixes #586 Co-authored-by: Claude <noreply@anthropic.com>
Summary
TypeScript-parity gap (
resumeSessionAt/resumeDropsTurn, the latter added in@anthropic-ai/claude-agent-sdk0.3.223). The CLI's headless lane can resume a session truncated at an earlier transcript entry (--resume-session-at=<uuid>) and validate that truncation against a declared discarded turn (--resume-drops-turn=<prompt uuid>): every entry past the fork point must be attributable to that turn, otherwise the resume is refused with anerror_during_executionresult whose message starts withResume rejected by --resume-drops-turn:. That lets a caller rewind to "before my last prompt" without silently discarding a queued message or task notification the session absorbed mid-turn that the caller never observed. The Python SDK had neither option.ClaudeAgentOptions.resume_session_at/resume_drops_turn, forwarded in equals form with the same Windows cmd-metacharacter rejection asresume/session_id.resume_drops_turnis forwarded whenever it is notNone— an empty string reaches the CLI and is rejected as malformed rather than being dropped by the SDK and silently disarming the guard (same as the TS transport)._internal/query.py: a refused resume is reported by the CLI as an error result on stdout followed by exit 1, before it answers the SDK'sinitializerequest. The read loop already replaced the bare "exit code 1"ProcessErrorwith the result's error text for the message stream, but pending control requests — including that in-flightinitialize— still received the raw exception, so callers sawCommand failed with exit code 1with the actual reason discarded. Pending control requests now get aProcessErrorcarrying the sameClaude Code returned an error result: ...text. (This also improves e.g.resumeof a nonexistent session, which takes the same path.)No SDK-side validation of the option combination is added; like the TS SDK this defers to the CLI.
Test plan
tests/test_transport.py: equals-form emission of both flags,resume_drops_turnomitted whenNoneand forwarded when"", Windows metacharacter rejection for both.tests/test_query.py: an error result followed by aProcessErrorwhileinitialize()is pending raisesProcessErrorwith the result text and exit code.e2e-tests/test_truncating_resume.py(runs in CI against the real CLI): build a two-turn session; fork at the last assistant entry of turn 1 withresume_drops_turn= turn 2's prompt UUID → succeeds, forked transcript is[turn 1, new turn], source untouched; same fork with a randomresume_drops_turn→ProcessErrorcontainingResume rejected by --resume-drops-turn:, nothing written. Passed locally against CLI 2.1.227.ruff check,ruff format --check,mypy src/, fullpytest tests/green locally.