vercel/ai · error · CodeModeProtocolError
CODE_MODE_PROTOCOL_ERROR
CODE_MODE_PROTOCOL_ERROR
Error message
Code-mode interrupt outer tool call id does not match its continuation.
What it means
assertInterruptMatchesLedger verifies that a CodeModeInterrupt object is internally consistent before it is accepted (in isCodeModeInterrupt) or continued (in continueCodeModeInterrupt). The interrupt carries an outerToolCallId at top level and inside its signed continuation ledger; when these differ the object is considered tampered, corrupt, or not a genuine code-mode interrupt, and a CodeModeProtocolError is thrown. In isCodeModeInterrupt the throw is caught and merely makes the guard return false.
Source
Thrown at packages/code-mode/src/interrupt-continuation.ts:142
value: unknown,
continuationSecurity: CodeModeContinuationSecurityOptions,
): CodeModeInterrupt | undefined {
if (isCodeModeInterrupt(value, continuationSecurity)) {
return value;
}
if (
isRecord(value) &&
(value.type === 'json' || value.type === 'text') &&
'value' in value
) {
return readInterruptValue(value.value, continuationSecurity);
}
return undefined;
}
function assertInterruptMatchesLedger(interrupt: CodeModeInterrupt): void {
if (interrupt.continuation.outerToolCallId !== interrupt.outerToolCallId) {
throw new CodeModeProtocolError(
'Code-mode interrupt outer tool call id does not match its continuation.',
);
}
const pending =
interrupt.continuation.pendingInterruptions[
interrupt.continuation.resolutions.length
];
if (
pending === undefined ||
pending.interruptId !== interrupt.interruptId ||
pending.toolCallId !== interrupt.toolCallId ||
pending.toolName !== interrupt.toolName ||
!jsonEqual(pending.input, interrupt.input) ||
!jsonEqual(pending.payload, interrupt.payload)
) {
throw new CodeModeProtocolError(
'Code-mode interrupt metadata does not match the signed continuation ledger.',
{ interruptId: interrupt.interruptId },View on GitHub (pinned to 69428b1f8b)
Solutions
- Use the interrupt object exactly as returned by unwrapCodeModeResult/getCodeModeInterrupt; never edit its fields.
- Re-obtain a fresh interrupt by re-running the code-mode call if the stored one is suspect.
- Verify your persistence layer serializes/deserializes the whole interrupt object losslessly (no partial updates).
Defensive patterns
Strategy: type-guard
Validate before calling
// Before resuming:
const valid = isCodeModeInterrupt(interrupt, continuationSecurity);
if (!valid) throw new Error('interrupt is not a genuine code-mode interrupt'); Type guard
function isConsistentInterrupt(i: CodeModeInterrupt): boolean {
return i.continuation.outerToolCallId === i.outerToolCallId;
} Try / catch
try {
await continueCodeModeInterrupt({ interrupt, resolution, tools });
} catch (error) {
if (CodeModeProtocolError.isInstance(error)) {
// discard tampered/corrupt interrupt, re-run the program
} else throw error;
} Prevention
- Treat CodeModeInterrupt objects as opaque; never mutate any field.
- Persist interrupts with lossless JSON serialization only.
- Always re-validate with isCodeModeInterrupt after loading from storage.
When it happens
Trigger: Calling continueCodeModeInterrupt with an interrupt whose outerToolCallId was manually edited, or an interrupt deserialized/transported incorrectly so the top-level id and the id embedded in the signed continuation token disagree.
Common situations: Persisting interrupts to a database and partially mutating them on replay; hand-constructing a CodeModeInterrupt instead of using the one returned by getCodeModeInterrupt/unwrapCodeModeResult; version mismatches between serialized interrupt formats.
Related errors
- CODE_MODE_PROTOCOL_ERROR
- Continuation contains a non-JSON-serializable value.
- CODE_MODE_PROTOCOL_ERROR
- CODE_MODE_PROTOCOL_ERROR
- Code mode interrupt payload must be an object.
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/b48fa9a4ae739b04.
Report an issue: GitHub.