ADR 0002: Claude Agent SDK over subprocess + regex¶
Status¶
Accepted. Implemented in M1 (domain-level typed signals); adapter implementation planned for M2.
Context¶
claude_autoresume.py drives Claude Code by shelling out to
claude -p --output-format stream-json --verbose and regex-matching the raw
stream for phrases like "usage limit", "try again later", and
"rejected" — explicitly not matching the whole stream, because it
routinely contains allowed_warning events and repo file contents that
mention "rate limit" in ordinary prose, which would false-positive.
Research during planning established that claude-agent-sdk (the Python
package wrapping Claude Code as a library) yields typed events instead:
a RateLimitEvent with a status field (allowed / allowed_warning /
rejected), a ResultMessage.api_error_status, and an
AssistantMessage.error enum. It also exposes a supported session-discovery
API (list_sessions()) replacing a glob over ~/.claude/projects/ that the
Claude Code docs explicitly warn against parsing directly, since the
transcript format changes between releases.
Decision¶
Replace subprocess + regex with claude-agent-sdk. The domain layer
(domain/classify.py) consumes a TurnSignals dataclass populated from the
SDK's typed fields rather than parsing text.
Consequences¶
- The
allowed_warningfalse positive becomes a singleifbranch (status == "allowed_warning"→ not a rejection) instead of a carefully scoped regex applied only to "trusted" text surfaces. - Session discovery moves from a hand-rolled JSONL parser to
list_sessions()/get_session_info(), which use cheap stat + head/tail reads and are the documented, version-stable way to do this. - One new risk accepted deliberately: the Claude Code binary contains the
string
[sdkMessageAdapter] Ignoring rate_limit_event message, suggestingRateLimitEventis dropped on some adapter paths.classify()therefore never depends onRateLimitEventalone — it also readsResultMessage.api_error_statusandAssistantMessage.erroras independent corroborating signals. See../domain-model.md. ClaudeSDKClient(streaming-input mode) stays alive across error results, where single-shotquery()raises a plainExceptionafter yielding the error and exits the process. This collapses the legacy respawn-and-resume loop into repeated sends on one live process — planned for the M2 agent gateway adapter.