Finding
The claude-code driver classifies a phase as successful on resultMessage.subtype === "success" (src/lib/workflow/drivers/claude-code.ts:230) — but the SDK emits subtype: "success" with is_error: true when the turn ends on an API error. The SDK's own docs on SDKResultMessage state it explicitly: "subtype 'success' carries the final assistant text in result — or, with is_error true, the error text when the turn ended on an API error" (sdk.d.ts:4608). The driver never reads is_error, so an API-errored turn is reported as a successful phase with zero work done.
Reproduction (deterministic)
Probed 2026-08-28 on Claude Code 2.1.251:
$ claude -p "Reply with exactly: OK" --model nonexistent-model-xyz --output-format json
[claude-code:unrecognized_model] {"model":"nonexistent-model-xyz","query_source":"sdk"}
{... "subtype":"success", "is_error":true, "api_error_status":404,
"terminal_reason":"api_error", "num_turns":1, "total_cost_usd":0,
"modelUsage":{},
"result":"There's an issue with the selected model (nonexistent-model-xyz). It may not exist or you may not have access to it. ..."}
$ echo $?
0
Three properties worth noting: no fallback model is substituted (good — nothing runs on the wrong model), the process exits 0, and the only failure signals are is_error/terminal_reason/api_error_status inside the result message the driver already holds.
Why it matters now
#914 made per-phase model strings reachable from settings.run.phases.<phase>.model and --models — free strings passed verbatim to the SDK (claude-code.ts:144, deliberately, per the AC-3 key-presence test). A typo'd or roster-stale model name therefore produces a phase that reports success, writes no commits, and posts no comment — the failure surfaces only downstream as confusing state (e.g. an "exec succeeded" with an empty diff entering QA). The same silent-success path presumably covers any other API-error termination the SDK folds into a success-subtype result. This is also a prerequisite for the model-roles indirection work: loud dispatch-time failure on a bad model string is what makes a thin role map safe without its own validation layer.
Suggested fix
In the subtype === "success" branch, treat is_error: true as a phase failure: success: false, error from the result text (which carries the API error message), preserving sessionId/resume handling. terminal_reason/api_error_status belong in the structured error context (#732 pattern).
Acceptance Criteria
Environment
Claude Code 2.1.251 / @anthropic-ai/claude-agent-sdk (repo-pinned), macOS. Found during the model-string contract spike for the model-roles design (vid-gen dogfood follow-up, 2026-08-28).
Finding
The claude-code driver classifies a phase as successful on
resultMessage.subtype === "success"(src/lib/workflow/drivers/claude-code.ts:230) — but the SDK emitssubtype: "success"withis_error: truewhen the turn ends on an API error. The SDK's own docs onSDKResultMessagestate it explicitly: "subtype 'success' carries the final assistant text in result — or, with is_error true, the error text when the turn ended on an API error" (sdk.d.ts:4608). The driver never readsis_error, so an API-errored turn is reported as a successful phase with zero work done.Reproduction (deterministic)
Probed 2026-08-28 on Claude Code 2.1.251:
Three properties worth noting: no fallback model is substituted (good — nothing runs on the wrong model), the process exits 0, and the only failure signals are
is_error/terminal_reason/api_error_statusinside the result message the driver already holds.Why it matters now
#914 made per-phase
modelstrings reachable fromsettings.run.phases.<phase>.modeland--models— free strings passed verbatim to the SDK (claude-code.ts:144, deliberately, per the AC-3 key-presence test). A typo'd or roster-stale model name therefore produces a phase that reports success, writes no commits, and posts no comment — the failure surfaces only downstream as confusing state (e.g. an "exec succeeded" with an empty diff entering QA). The same silent-success path presumably covers any other API-error termination the SDK folds into asuccess-subtype result. This is also a prerequisite for the model-roles indirection work: loud dispatch-time failure on a bad model string is what makes a thin role map safe without its own validation layer.Suggested fix
In the
subtype === "success"branch, treatis_error: trueas a phase failure:success: false,errorfrom the result text (which carries the API error message), preservingsessionId/resume handling.terminal_reason/api_error_statusbelong in the structured error context (#732 pattern).Acceptance Criteria
subtype: "success"andis_error: trueyieldssuccess: falsewith the result text as the error, verified by a driver unit test using the reproduction shape above (api_error_status: 404, emptymodelUsage)is_error: falseor absent) is unaffected, verified by the existing success-path tests staying greenterminal_reasonandapi_error_statuswhen present, verified by a unit test asserting both fieldsEnvironment
Claude Code 2.1.251 / @anthropic-ai/claude-agent-sdk (repo-pinned), macOS. Found during the model-string contract spike for the model-roles design (vid-gen dogfood follow-up, 2026-08-28).