vercel/ai · error · Error
SandboxChannel: cannot open a closed channel.
Error message
SandboxChannel: cannot open a closed channel.
What it means
Thrown by SandboxChannel.open in packages/harness/src/utils/sandbox-channel.ts when the channel has reached a terminal (irreversibly closed) state and an attempt is made to open it again. A terminal channel cannot be revived — even the cross-process attach/resume handshake — because its underlying transport is permanently gone. Callers must create a fresh channel instead of reopening the closed one.
Source
Thrown at packages/harness/src/utils/sandbox-channel.ts:207
*/
get lastSeenEventId(): number {
return this._lastSeenEventId;
}
/**
* Establish the initial connection. A single attempt — startup failures
* reject so the caller can fail `doStart` cleanly. Reconnect retries apply
* only to drops after a successful open.
*
* Pass `{ resume: true }` to attach to a bridge that is already mid-session:
* after the socket opens, the channel sends `{ type: 'resume', lastSeenEventId }`
* so the bridge replays everything past the seeded cursor. This is the
* cross-process attach handshake — identical to what a transient reconnect
* does, but triggered by the initial open from a new process.
*/
async open(opts?: { resume?: boolean }): Promise<void> {
if (this.terminal) {
throw new Error('SandboxChannel: cannot open a closed channel.');
}
const ws = await this.connectThunk();
this.wire(ws);
this.ws = ws;
this.connected = true;
if (opts?.resume) {
this.rawSend(
JSON.stringify({
type: 'resume',
lastSeenEventId: this._lastSeenEventId,
}),
);
}
}
on<T extends EventTypeOf<TOut>>(
type: T,
listener: Listener<TOut, T>,View on GitHub (pinned to 69428b1f8b)
Solutions
- Create a new SandboxChannel instance instead of reusing the closed one.
- Track channel lifecycle and discard references once the terminal state is reached.
- Only use resume/reopen paths on channels that are disconnected but not terminal.
- Listen for the channel's close/terminal callback to invalidate cached references.
Example fix
// before let channel = createChannel(); await channel.close(); await channel.open(); // throws: cannot open a closed channel // after let channel = createChannel(); await channel.close(); channel = createChannel(); await channel.open();
Defensive patterns
Strategy: fallback
Validate before calling
// Check terminal state before opening (adapt to exposed API)
if (channel.isTerminal?.()) {
channel = createChannel(); // recreate instead of reopening
}
await channel.open(); Type guard
function isReopenable(channel: { isTerminal?: () => boolean }): boolean {
return !(channel.isTerminal?.() ?? false);
} Try / catch
try {
await channel.open({ resume: true });
} catch (error) {
if (/cannot open a closed channel/.test((error as Error).message)) {
channel = createChannel();
await channel.open();
} else throw error;
} Prevention
- Discard channel references as soon as the close/terminal callback fires.
- Never cache channels across sandbox process restarts.
- Distinguish transient disconnects (reconnect) from terminal closure (recreate).
- Centralize channel lifecycle management in one owner.
When it happens
Trigger: Calling `channel.open()` (with or without `{ resume: true }`) on a SandboxChannel that previously hit its terminal state (e.g. after close, fatal disconnect, or process teardown).
Common situations: Reusing a cached channel object after the sandbox process exited; attempting cross-process attach with a stale channel reference; retry logic re-opening a channel that closed fatally instead of recreating it.
Related errors
- SandboxChannel: cannot send ${message.type} — channel is clo
- Invalid argument for parameter batch: batch must be a suppor
- The Claude Code harness requires an explicit `portEndpoint`
- claude-code bridge did not complete WebSocket handshake with
- Invalid argument for parameter requests: requests must not b
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/ae1231d6a92e626e.
Report an issue: GitHub.