{"record":{"id":"40d1800baecdd1f1","repo":"stablyai/orca","slug":"attach-canceled-for-session-sessionid","errorCode":null,"errorMessage":"Attach canceled for session ${sessionId}","messagePattern":"Attach canceled for session (.+?)","errorType":"exception","errorClass":"TerminalAttachCanceledError","httpStatus":null,"severity":"info","filePath":"src/main/daemon/daemon-server.ts","lineNumber":902,"sourceCode":"    timer.unref()\n    socket.once('close', start)\n    socket.once('error', start)\n    this.pendingShutdownReplies.set(key, { start })\n  }\n\n  private async preparePtySpawnUnlessCanceled(sessionId: string, clientId: string): Promise<void> {\n    const preparation: PendingPtySpawnPreparation = {\n      canceled: false,\n      clientId\n    }\n    const pending = this.pendingPtySpawnPreparations.get(sessionId) ?? new Set()\n    pending.add(preparation)\n    this.pendingPtySpawnPreparations.set(sessionId, pending)\n    try {\n      // Why: register before the async probe so a concurrent close can cancel this creation before a subprocess exists.\n      await this.preparePtySpawn()\n      if (preparation.canceled) {\n        throw new TerminalAttachCanceledError(sessionId)\n      }\n    } finally {\n      pending.delete(preparation)\n      if (pending.size === 0) {\n        this.pendingPtySpawnPreparations.delete(sessionId)\n      }\n    }\n  }\n\n  private cancelPendingPtySpawnPreparations(sessionId: string): boolean {\n    const pending = this.pendingPtySpawnPreparations.get(sessionId)\n    if (!pending) {\n      return false\n    }\n    for (const preparation of pending) {\n      preparation.canceled = true\n    }\n    return true","sourceCodeStart":884,"sourceCodeEnd":920,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/daemon/daemon-server.ts#L884-L920","documentation":"preparePtySpawnUnlessCanceled registers a per-session preparation record before the async preparePtySpawn() probe, so a concurrent close can flip preparation.canceled before a subprocess is created. If canceled is true after the probe resolves, it throws TerminalAttachCanceledError(sessionId) — the create was aborted to avoid spawning a PTY the user already closed.","triggerScenarios":"createOrAttach (attachOnly=false) running while the renderer closes the same session id; close arrives between probe registration and the spawn; rapid open/close thrash on a terminal tab.","commonSituations":"User opens then immediately closes a terminal; a reconnect race that reissues create then cancels; UI remount churn driving attach/cancel cycles.","solutions":["Treat TerminalAttachCanceledError as benign — the session was closed by the user/caller before spawn; do not surface as a crash.","Avoid issuing createOrAttach for a sessionId the UI has already torn down; track in-flight tabs.","If it fires constantly, audit the renderer for open/close thrash on the same id."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"import { TerminalAttachCanceledError } from './daemon-errors'\n\nfunction isAttachCanceled(e: unknown): e is TerminalAttachCanceledError {\n  return e instanceof TerminalAttachCanceledError\n}","tryCatchPattern":"try {\n  await host.createOrAttach({ sessionId, attachOnly: false, ... })\n} catch (e) {\n  if (isAttachCanceled(e)) {\n    // benign — closed before spawn; suppress\n  } else throw e\n}","preventionTips":["Track in-flight terminal tabs and do not issue createOrAttach for ids the UI has torn down.","Treat TerminalAttachCanceledError as benign in telemetry — do not count as a crash.","Debounce renderer remount churn to avoid open/close thrash on the same id."],"tags":["daemon","pty","spawn","cancel","race","session-lifecycle"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}