What happened
A model sent agent_swarm one malformed item (items[2] carried neither
subagent_id nor a legacy profile). That is a recoverable mistake, and the
runtime is built to treat it as one: formatToolArgsViolationText exists so the
refusal names the fields the call does take, and
packages/runtime/src/__tests__/tool-args-violation.test.ts asserts the refusal
comes back as a tool result the model can read and correct.
Instead the whole turn died. From the user's side the transcript simply stops
under a red Agent Swarm card:
Tool "agent_swarm" arguments failed validation: [
{
"code": "custom",
"message": "Provide exactly one of subagent_id or legacy profile.",
"path": [ "items", 2 ]
}
] agent_swarm takes `items`, `prompt_template`, `profile`, `subagent_id`, `resume_run_ids`, `max_concurrency`.
No retry, no further steps, composer idle. The model was never given the chance
to fix its third item.
What the durable state shows
The refused call left nothing in the ledger, and the run never reached a
terminal state:
runtime_events for the turn stops at event_seq 102 — the assistant text
that preceded the call. There is no function_call and no function_response
for agent_swarm anywhere in the session.tool_journal_events / tool_operations have no row for it either; the last
entries are the preceding Read operations, cleanly outcome_committed.core_agent_runs still holds "status": "running" for that run (it becomes
failureClass: "app_restarted" on the next launch), carrying:
"traceWriteError": "append runtime event: Tool ledger transition rejected: orphan_response at 8de09ddf-1b18-4b0c-964f-8d69be078d69"
So the append of the synthetic error result was rejected by the ledger, the
rejection threw out of the append, and the turn unwound with the run row stuck
mid-flight.
Root cause
packages/runtime/src/tool-runtime.ts:858-864 already names this exact failure
mode — but it only closes it for one of the refusal paths:
// Exclusive-step rejection is preflight: it must remain on the generic// call/response lane instead of claiming the T1 dispatch protocol. If the// call carried an operationId here, AgentRun would (correctly) skip its// generic projection assuming commitToolPrepared already persisted it;// the synthetic response would then become an orphan.constoperationId=this.input.runtimeCommitSink&&invocationId&&!admissionFailure
? buildToolOperationId({ invocationId,providerToolCallId: toolUseId})
: undefined;admissionFailure is the only pre-dispatch refusal excluded. Every other one
returns beforeprepareDurableToolAttempt (:1107), which is what calls
commitToolPrepared and actually persists the call on the T1 lane:
| refusal | site |
|---|
| arguments failed schema validation | :950 |
| loop-gate / repeated failing call | :1005, :1026, :1049 |
| permission denial, sandbox boundary | :1066, :1077 |
| subagent tool limit | :1102 |
For all of those the tool_start event has already been pushed with an
operationId (id: ${operationId}_call), so AgentRun skips its generic
projection of the call and waits for a commitToolPrepared that never comes.
writeSyntheticToolResult (:728) then finds no durable attempt for the call:
constdurableAttempt=this.durableToolAttempts.get(durableAttemptKey(turnId,toolUseId));constdurableOutcome=awaitdurableAttempt?.commitOutcome(content,true);…queue.push({type: 'tool_result',id: durableOutcome?.id??this.input.newId(),// ← UUID, not `${operationId}_response`…
...(durableOutcome ? {operationId: durableOutcome.operationId} : {}),// ← omitted— and writes the result onto the generic lane with a fresh UUID and no
operationId. tool-ledger-scanner.ts:351-372 matches responses to calls by
(invocationId, toolCallId), finds no call, and raises orphan_response. The
append throws, and the turn dies.
The UUID in the recorded traceWriteError (rather than a
toolop_<hash>_response id) is the fingerprint of that fallback path.
Scope
Observed via the argument-validation path. The other rows in the table above
share the same shape — pushed tool_start with an operationId, returned before
prepareDurableToolAttempt, synthetic result on the generic lane — so they
should be lethal in the same way; that part is read off the code, not yet
reproduced. If so, the class is "every recoverable pre-dispatch refusal is fatal
whenever runtimeCommitSink is active", i.e. in the real Desktop runtime.
Two secondary observations, separable from the fix:
agent_swarm's refusal text says Provide exactly one of subagent_id or legacy profile. without listing the valid profile enum values or pointing
at agent_list. The field list appended by formatToolArgsViolationText is
the top-level one, which is not where the violation was
(path: ["items", 2]). Once the turn survives the refusal, this is what
decides whether the model actually repairs the call.- A single rejected event append killing the turn silently — and leaving the
run row at status: "running" with no terminal event — is its own weakness,
independent of what caused the rejection.
How to reproduce
- Desktop, a real session (durable
runtimeCommitSink active). - Get any tool call whose arguments the tool's own zod schema rejects. A
cross-field superRefine rule is the reliable way to reach ToolRuntime's
validation, e.g. an agent_swarm item with neither subagent_id nor
profile. - The refusal renders under the tool's card and the turn ends there. Afterwards
the session's runtime_events hold no call/response pair for it, and the run
row in core_agent_runs carries traceWriteError: … orphan_response … while
still reading status: "running".
Environment
- Maka commit:
cef8c44d8 (locally packaged unsigned build, app version 0.1.5) - OS: macOS 26.6 (arm64)
- Surface: Desktop
- Node.js: v22.17.0
- Model:
deepseek-v4-pro over an openai-compatible connection
What happened
A model sent
agent_swarmone malformed item (items[2]carried neithersubagent_idnor a legacyprofile). That is a recoverable mistake, and theruntime is built to treat it as one:
formatToolArgsViolationTextexists so therefusal names the fields the call does take, and
packages/runtime/src/__tests__/tool-args-violation.test.tsasserts the refusalcomes back as a tool result the model can read and correct.
Instead the whole turn died. From the user's side the transcript simply stops
under a red
Agent Swarmcard:No retry, no further steps, composer idle. The model was never given the chance
to fix its third item.
What the durable state shows
The refused call left nothing in the ledger, and the run never reached a
terminal state:
runtime_eventsfor the turn stops atevent_seq 102— the assistant textthat preceded the call. There is no
function_calland nofunction_responsefor
agent_swarmanywhere in the session.tool_journal_events/tool_operationshave no row for it either; the lastentries are the preceding
Readoperations, cleanlyoutcome_committed.core_agent_runsstill holds"status": "running"for that run (it becomesfailureClass: "app_restarted"on the next launch), carrying:So the append of the synthetic error result was rejected by the ledger, the
rejection threw out of the append, and the turn unwound with the run row stuck
mid-flight.
Root cause
packages/runtime/src/tool-runtime.ts:858-864already names this exact failuremode — but it only closes it for one of the refusal paths:
admissionFailureis the only pre-dispatch refusal excluded. Every other onereturns before
prepareDurableToolAttempt(:1107), which is what callscommitToolPreparedand actually persists the call on the T1 lane::950:1005,:1026,:1049:1066,:1077:1102For all of those the
tool_startevent has already been pushed with anoperationId(id: ${operationId}_call), so AgentRun skips its genericprojection of the call and waits for a
commitToolPreparedthat never comes.writeSyntheticToolResult(:728) then finds no durable attempt for the call:— and writes the result onto the generic lane with a fresh UUID and no
operationId.tool-ledger-scanner.ts:351-372matches responses to calls by(invocationId, toolCallId), finds no call, and raisesorphan_response. Theappend throws, and the turn dies.
The UUID in the recorded
traceWriteError(rather than atoolop_<hash>_responseid) is the fingerprint of that fallback path.Scope
Observed via the argument-validation path. The other rows in the table above
share the same shape — pushed
tool_startwith anoperationId, returned beforeprepareDurableToolAttempt, synthetic result on the generic lane — so theyshould be lethal in the same way; that part is read off the code, not yet
reproduced. If so, the class is "every recoverable pre-dispatch refusal is fatal
whenever
runtimeCommitSinkis active", i.e. in the real Desktop runtime.Two secondary observations, separable from the fix:
agent_swarm's refusal text saysProvide exactly one of subagent_id or legacy profile.without listing the validprofileenum values or pointingat
agent_list. The field list appended byformatToolArgsViolationTextisthe top-level one, which is not where the violation was
(
path: ["items", 2]). Once the turn survives the refusal, this is whatdecides whether the model actually repairs the call.
run row at
status: "running"with no terminal event — is its own weakness,independent of what caused the rejection.
How to reproduce
runtimeCommitSinkactive).cross-field
superRefinerule is the reliable way to reach ToolRuntime'svalidation, e.g. an
agent_swarmitem with neithersubagent_idnorprofile.the session's
runtime_eventshold no call/response pair for it, and the runrow in
core_agent_runscarriestraceWriteError: … orphan_response …whilestill reading
status: "running".Environment
cef8c44d8(locally packaged unsigned build, app version 0.1.5)deepseek-v4-proover an openai-compatible connection