{"record":{"id":"ae1231d6a92e626e","repo":"vercel/ai","slug":"sandboxchannel-cannot-open-a-closed-channel","errorCode":null,"errorMessage":"SandboxChannel: cannot open a closed channel.","messagePattern":"SandboxChannel: cannot open a closed channel\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/harness/src/utils/sandbox-channel.ts","lineNumber":207,"sourceCode":"   */\n  get lastSeenEventId(): number {\n    return this._lastSeenEventId;\n  }\n\n  /**\n   * Establish the initial connection. A single attempt — startup failures\n   * reject so the caller can fail `doStart` cleanly. Reconnect retries apply\n   * only to drops after a successful open.\n   *\n   * Pass `{ resume: true }` to attach to a bridge that is already mid-session:\n   * after the socket opens, the channel sends `{ type: 'resume', lastSeenEventId }`\n   * so the bridge replays everything past the seeded cursor. This is the\n   * cross-process attach handshake — identical to what a transient reconnect\n   * does, but triggered by the initial open from a new process.\n   */\n  async open(opts?: { resume?: boolean }): Promise<void> {\n    if (this.terminal) {\n      throw new Error('SandboxChannel: cannot open a closed channel.');\n    }\n    const ws = await this.connectThunk();\n    this.wire(ws);\n    this.ws = ws;\n    this.connected = true;\n    if (opts?.resume) {\n      this.rawSend(\n        JSON.stringify({\n          type: 'resume',\n          lastSeenEventId: this._lastSeenEventId,\n        }),\n      );\n    }\n  }\n\n  on<T extends EventTypeOf<TOut>>(\n    type: T,\n    listener: Listener<TOut, T>,","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/harness/src/utils/sandbox-channel.ts#L189-L225","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nlet channel = createChannel();\nawait channel.close();\nawait channel.open(); // throws: cannot open a closed channel\n// after\nlet channel = createChannel();\nawait channel.close();\nchannel = createChannel();\nawait channel.open();","handlingStrategy":"fallback","validationCode":"// Check terminal state before opening (adapt to exposed API)\nif (channel.isTerminal?.()) {\n  channel = createChannel(); // recreate instead of reopening\n}\nawait channel.open();","typeGuard":"function isReopenable(channel: { isTerminal?: () => boolean }): boolean {\n  return !(channel.isTerminal?.() ?? false);\n}","tryCatchPattern":"try {\n  await channel.open({ resume: true });\n} catch (error) {\n  if (/cannot open a closed channel/.test((error as Error).message)) {\n    channel = createChannel();\n    await channel.open();\n  } else throw error;\n}","preventionTips":["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."],"tags":["sandbox","channel","lifecycle","websocket"],"backgroundTag":"channel-already-closed","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}