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

  1. Create a new SandboxChannel instance instead of reusing the closed one.
  2. Track channel lifecycle and discard references once the terminal state is reached.
  3. Only use resume/reopen paths on channels that are disconnected but not terminal.
  4. 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

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


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