vercel/ai · error · CodeModeProtocolError
CODE_MODE_PROTOCOL_ERROR
CODE_MODE_PROTOCOL_ERROR
Error message
Code mode continuation envelope is malformed.
What it means
verifyCodeModeContinuation validates the structural envelope of a CodeModeContinuation before checking its auth metadata and HMAC signature. It throws this CodeModeProtocolError when required fields are missing or mistyped: version !== 2, non-string js/outerToolCallId/token, empty token, missing/empty pendingInterruptions array, or a non-array resolutions field. This guards against replaying corrupted, truncated, or hand-forged continuation objects.
Source
Thrown at packages/code-mode/src/continuation-capability.ts:99
export function verifyCodeModeContinuation(
continuation: CodeModeContinuation,
security: CodeModeContinuationSecurityOptions = {},
): void {
if (
typeof continuation !== 'object' ||
continuation === null ||
continuation.version !== 2 ||
typeof continuation.js !== 'string' ||
typeof continuation.outerToolCallId !== 'string' ||
!Array.isArray(continuation.toolNames) ||
!continuation.toolNames.every(name => typeof name === 'string') ||
typeof continuation.token !== 'string' ||
continuation.token.length === 0 ||
!Array.isArray(continuation.pendingInterruptions) ||
continuation.pendingInterruptions.length === 0 ||
!Array.isArray(continuation.resolutions)
) {
throw new CodeModeProtocolError(
'Code mode continuation envelope is malformed.',
);
}
assertAuthShape(continuation.auth);
const now = Date.now();
if (continuation.auth.expiresAtMs < now) {
throw new CodeModeProtocolError('Code mode continuation has expired.', {
expiresAtMs: continuation.auth.expiresAtMs,
now,
});
}
if (continuation.auth.issuedAtMs > now + 60_000) {
throw new CodeModeProtocolError(
'Code mode continuation was issued in the future.',
{ issuedAtMs: continuation.auth.issuedAtMs, now },
);
}
View on GitHub (pinned to 69428b1f8b)
Solutions
- Keep and reuse the exact CodeModeContinuation object returned by the interrupt, without modifications
- Check the object has version: 2, non-empty string token, and non-empty pendingInterruptions array before calling
- Re-serialize with a lossless format and confirm no field stripping/redaction happens before verification
- If upgrading from an older schema, re-create the continuation rather than passing legacy objects
- Use hasValidCodeModeContinuationCapability() to test validity without throwing
Example fix
// before
const stored = JSON.parse(await redis.get(id));
await continueCodeModeInterrupt(stored.continuation); // token stripped for size
// after
const continuation = JSON.parse(await redis.get(id));
if (
continuation?.version !== 2 ||
typeof continuation.token !== 'string' ||
!continuation.pendingInterruptions?.length
) {
throw new Error('continuation payload incomplete');
}
await continueCodeModeInterrupt(continuation); Defensive patterns
Strategy: type-guard
Validate before calling
function looksLikeContinuation(v: unknown): boolean {
const c = v as any;
return (
typeof c === 'object' && c !== null &&
c.version === 2 &&
typeof c.js === 'string' &&
typeof c.outerToolCallId === 'string' &&
Array.isArray(c.toolNames) &&
typeof c.token === 'string' && c.token.length > 0 &&
Array.isArray(c.pendingInterruptions) && c.pendingInterruptions.length > 0 &&
Array.isArray(c.resolutions)
);
}
if (!looksLikeContinuation(continuation)) throw new Error('continuation envelope incomplete'); Type guard
function isWellFormedContinuation(v: unknown): v is CodeModeContinuation {
const c = v as any;
return (
typeof v === 'object' && v !== null &&
c.version === 2 &&
typeof c.js === 'string' &&
typeof c.outerToolCallId === 'string' &&
Array.isArray(c.toolNames) &&
typeof c.token === 'string' && c.token.length > 0 &&
Array.isArray(c.pendingInterruptions) && c.pendingInterruptions.length > 0 &&
Array.isArray(c.resolutions)
);
} Try / catch
import { hasValidCodeModeContinuationCapability } from '.../continuation-capability.js';
if (!hasValidCodeModeContinuationCapability(continuation)) {
throw new Error('continuation is malformed, expired, or has an invalid signature');
}
await continueCodeModeInterrupt(continuation); Prevention
- Persist and reuse the continuation object exactly as returned; never redact or strip fields
- Check version === 2 when loading persisted continuations across SDK upgrades
- Serialize losslessly (full JSON) when transferring between services
- Use hasValidCodeModeContinuationCapability() as a cheap pre-check before continuing
When it happens
Trigger: Calling continueCodeModeInterrupt / prepareContinuation with a continuation that was JSON-round-tripped with fields dropped; constructing the object manually with missing pendingInterruptions or an empty array; persisting continuations and loading old-schema objects (version !== 2); passing null/undefined or a redacted object where token or pendingInterruptions were stripped for logging.
Common situations: Storing continuations in a DB/Redis and a schema migration (v1 vs v2) changed field names; serializers that drop arrays or undefined fields; redacting `token` in logs and then reusing the redacted copy; middleware transforming the payload between services.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- CODE_MODE_PROTOCOL_ERROR
- Continuation maxAgeMs must be a positive integer.
- Continuation contains a non-JSON-serializable value.
- Code mode interrupt payload must be an object.
- Code mode interrupt payload must include a string kind.
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/bbc4041a7203fb03.
Report an issue: GitHub.