vercel/ai · error · Error
SandboxChannel: cannot send ${message.type} — channel is clo
Error message
SandboxChannel: cannot send ${message.type} — channel is closed. What it means
Thrown by SandboxChannel.send in packages/harness/src/utils/sandbox-channel.ts when a message is sent on a channel that has reached terminal (closed) state. Once a channel is terminal no further messages can be serialized or transmitted; every send attempt fails immediately rather than silently dropping the message. A new channel must be established to continue communicating.
Source
Thrown at packages/harness/src/utils/sandbox-channel.ts:260
return () => {
set!.delete(listener as unknown as Listener<TOut, EventTypeOf<TOut>>);
};
}
onClose(handler: (code: number, reason: string) => void): void {
this.onCloseHandlers.add(handler);
}
onReconnect(handler: () => void): () => void {
this.onReconnectHandlers.add(handler);
return () => {
this.onReconnectHandlers.delete(handler);
};
}
send(message: TIn): void {
if (this.terminal) {
throw new Error(
`SandboxChannel: cannot send ${message.type} — channel is closed.`,
);
}
this.rawSend(JSON.stringify(message));
}
/**
* Mark that the host is tearing the session down. The next socket close is
* then treated as terminal rather than triggering a reconnect. Call before
* sending a `stop` / `destroy` message whose completion closes the bridge
* socket.
*/
beginClose(): void {
this.closing = true;
}
close(): void {
if (this.terminal) return;View on GitHub (pinned to 69428b1f8b)
Solutions
- Check the channel's connection/terminal state before calling send, and recreate the channel when it is terminal.
- Wrap sends in error handling that triggers channel recreation and message replay.
- Stop producers (queues/callbacks) when the channel's terminal/close callback fires.
- Use the channel's reconnect handlers to detect permanent closure versus transient disconnect.
Example fix
// before
await channel.close();
channel.send({ type: 'run' }); // throws
// after
await channel.close();
channel = createChannel();
await channel.open();
channel.send({ type: 'run' }); Defensive patterns
Strategy: try-catch
Validate before calling
if (channel.isTerminal?.()) {
throw new Error('Channel closed — recreate before sending ' + message.type);
} Type guard
function canSend(channel: { isTerminal?: () => boolean }): boolean {
return !(channel.isTerminal?.() ?? true);
} Try / catch
try {
channel.send(message);
} catch (error) {
if (/channel is closed/.test((error as Error).message)) {
await recreateAndReplay([message]); // new channel, replay unsent messages
} else throw error;
} Prevention
- Pause message producers when the terminal/close callback fires.
- Queue messages with replay support so a closed channel loses nothing.
- Bind send calls to a live-connection check.
- Handle sandbox process exit events eagerly instead of waiting for send to throw.
When it happens
Trigger: Calling `channel.send(message)` after the channel entered its terminal state — e.g. the sandbox process exited, the WebSocket closed fatally, or `close()` was invoked — and application code still holds the stale channel reference.
Common situations: Fire-and-forget command queues continuing to push messages after sandbox teardown; long-running workers holding a channel across sandbox restarts; race where the sandbox dies mid-task and the next `send` throws.
Related errors
- SandboxChannel: cannot open a closed channel.
- Invalid argument for parameter batch: batch must be a suppor
- Invalid argument for parameter requests: requests must not b
- Invalid argument for parameter requests: request IDs must no
- Invalid argument for parameter requests: request IDs must be
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/8c5fe9ebb061758f.
Report an issue: GitHub.