Add ConversationResetMessage for the CLI's conversation_reset frame - #1196
Conversation
In streaming-input mode one connection carries many user turns, and a
`/clear` (or any other flow that discards the transcript mid-session)
resets the conversation and zeroes the running totals on subsequent
result messages. The CLI announces this with a top-level
`{"type":"conversation_reset","new_conversation_id","uuid","session_id"}`
frame, which the TypeScript SDK surfaces as SDKConversationResetMessage.
The Python parser only recognized its known message types and dropped
this one through the forward-compat fallthrough, so Python apps never
saw resets at all.
Model it as a `ConversationResetMessage` dataclass alongside the other
top-level non-system frames (RateLimitEvent, StreamEvent), add it to the
`Message` union, parse and export it, and cover it with unit tests plus
an e2e test that sends `/clear` mid-session and asserts the frame
arrives stamped with the outgoing session id.
There was a problem hiding this comment.
LGTM — small, self-contained addition that mirrors the existing RateLimitEvent pattern for a new top-level CLI frame.
What was reviewed:
- The new
conversation_resetparser case inmessage_parser.py— matches sibling frames (required-field KeyError → MessageParseError), and unknown types still fall through to the forward-compat skip. - Checked
client.pyinternals for exhaustive Message-type handling — onlyResultMessageis special-cased, so the new frame flows throughreceive_messages/receive_responsewithout changes. - Public-surface wiring (
types.pyunion,__init__.pyexport) and test coverage (unit happy path + missing-field, plus an e2e test against the real CLI).
Extended reasoning...
Overview
This PR adds a ConversationResetMessage dataclass surfacing the CLI's conversation_reset frame, which was previously dropped by the parser's forward-compat fallthrough. It touches four source locations — a new dataclass in types.py added to the Message union, a new match case in message_parser.py, an export in __init__.py — plus unit tests and a new e2e test. The change is a TypeScript-parity gap fix and is structurally identical to the prior RateLimitEvent addition (#648).
Security risks
None. The change parses three string fields from an already-trusted subprocess stream into a plain dataclass; there is no auth, permission, subprocess-spawning, or injection surface involved.
Level of scrutiny
Low-to-moderate. This is a mechanical, pattern-following addition to the message parser — the riskiest aspect is the public-API widening of the Message union, which the PR description explicitly calls out and which has direct precedent in RateLimitEvent. Code that exhaustively matches on Message may need a type-level update, but runtime behavior for existing consumers is unchanged (the frame was previously dropped silently). I verified that the SDK's own internals (client.py, query.py) only special-case ResultMessage, so the new type flows through the streaming iterators without further changes.
Other factors
Test coverage is solid: unit tests for the happy path and missing-field error mirror the sibling frames' tests, and a new e2e test exercises the real CLI flow (/clear mid-session → reset frame stamped with the outgoing session_id → fresh session_id on the next result). The bug hunting system found no bugs, and the PR timeline has no outstanding reviewer comments. The one behavioral caveat — a malformed conversation_reset frame now raises MessageParseError instead of being dropped — is the same contract every sibling frame follows and is validated by the e2e test against the bundled CLI version.
chrislloyd
left a comment
There was a problem hiding this comment.
Stamp via #claude-code-shadow-stamps
Port of Python SDK commit 54dd3b4 (anthropics/claude-agent-sdk-python#1196). Fixes #582
#589) Port of Python SDK commit 54dd3b4 (anthropics/claude-agent-sdk-python#1196). Fixes #582 Co-authored-by: Claude <noreply@anthropic.com>
Summary
TypeScript-parity gap. In streaming-input mode one connection carries many user turns, and a
/clear(or any other flow that discards the transcript mid-session) resets the conversation and zeroes the running totals reported on subsequent result messages. The CLI announces this with a top-level frame:{"type": "conversation_reset", "new_conversation_id": "<uuid>", "uuid": "<uuid>", "session_id": "<outgoing session id>"}The TypeScript SDK surfaces it as
SDKConversationResetMessage. The Python parser only recognized its known message types and dropped this one through the forward-compat fallthrough inmessage_parser.py, so Python apps never saw resets — including ones they didn't initiate — and had no signal to snapshot totals before they zero.types.py: newConversationResetMessagedataclass (new_conversation_id,uuid,session_id), added to theMessageunion next to the other top-level non-system frames (RateLimitEvent,StreamEvent).message_parser.py: parsetype == "conversation_reset"; missing required field →MessageParseError, same as siblings.__init__.py: export.Compatibility note: like
RateLimitEvent(#648), this widens the publicMessageunion. Code that exhaustively matches onMessagewithassert_neverwill get a new type-check error, and code that raises on unrecognized message classes will now see a frame that was previously dropped silently.Test plan
tests/test_message_parser.py: parse happy path + missing-field error.e2e-tests/test_conversation_reset.py(runs in CI against the real CLI): open aClaudeSDKClient, run one turn, send/clear, assert aConversationResetMessagearrives before the/clearturn's result, stamped with the outgoingsession_id, and that the following result carries a newsession_id. Passed locally against CLI 2.1.227.ruff check,ruff format --check,mypy src/, fullpytest tests/green locally.