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

  1. Use the interrupt object exactly as returned by unwrapCodeModeResult/getCodeModeInterrupt; never edit its fields.
  2. Re-obtain a fresh interrupt by re-running the code-mode call if the stored one is suspect.
  3. 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

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


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/b48fa9a4ae739b04. Report an issue: GitHub.