{"record":{"id":"86d5a2d4e478a26c","repo":"pydantic/monty","slug":"the-session-is-closed-check-out-a-new-one","errorCode":null,"errorMessage":"the session is closed — check out a new one","messagePattern":"the session is closed — check out a new one","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/monty-js/ts/session.ts","lineNumber":446,"sourceCode":"      return\n    }\n    this.closed = true\n    await this.native.finish()\n  }\n\n  async [Symbol.asyncDispose](): Promise<void> {\n    await this.close()\n  }\n\n  /** Poisons the session over a worker death or protocol violation. */\n  private poison(err: Error): Error {\n    this.broken = err\n    return err\n  }\n\n  private ensureUsable(): void {\n    if (this.closed) {\n      throw new Error('the session is closed — check out a new one')\n    }\n    if (this.broken !== null) {\n      throw this.broken\n    }\n  }\n}\n\n/**\n * Answers one suspension turn from a captured `externalLookup` / `os`, tracking\n * promise-returning externals as pending futures. Shared by\n * [`MontySession.feedRun`]'s drive loop and [`SnapshotDriver`]'s `resumeAuto`\n * so both resolve suspensions identically. Built fresh per feed / per snapshot\n * chain — its `futures` map is scoped to that run, never leaking across feeds.\n *\n * `answer` deliberately does **not** catch: a handler that throws leaves the\n * worker suspended, so the caller poisons the session and rethrows.\n */\nclass TurnAnswerer {","sourceCodeStart":428,"sourceCodeEnd":464,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty-js/ts/session.ts#L428-L464","documentation":"The session is no longer usable: it was closed (`session.close()` / `await using` exit), or it is broken (poisoned by a crash/protocol error rethrown by `ensureUsable`). Any driving call — `feedRun`, `feedStart`, `loadSession`, `dump`, `installDependencies` — is rejected.","triggerScenarios":"Calling any session method after `close()` or after the `await using session` block; also after a `MontyCrashedError` poisoned the session, in which case the stored broken error is rethrown instead.","commonSituations":"Background tasks holding a session past its disposal scope, a worker crash earlier in the turn followed by more calls, accidentally closing then retrying on the same object.","solutions":["Check out a new session with `pool.checkout()`","If the thrown error is the stored crash/protocol error, treat the worker as dead — the pool replaces it on next checkout","Keep all session usage inside the `await using` scope"],"exampleFix":"// before\nawait using session = await pool.checkout()\nawait session.feedRun('1+1')\n// after disposal:\nawait session.feedRun('2+2') // throws\n// after\nconst fresh = await pool.checkout()\nawait fresh.feedRun('2+2')","handlingStrategy":"try-catch","validationCode":"if (sessionClosed || sessionBroken) {\n  session = await pool.checkout()\n}","typeGuard":"function isUsable(session: MontySession): boolean {\n  return !session.closed // and no prior crash poisoned it\n}","tryCatchPattern":"try {\n  await session.feedRun(code)\n} catch (e) {\n  if (e instanceof Error && e.message.includes('the session is closed') || e instanceof MontyCrashedError) {\n    session = await pool.checkout()\n    await session.feedRun(code)\n  } else throw e\n}","preventionTips":["Keep session usage inside the `await using session` scope","Cancel background jobs referencing the session before closing it","After any MontyCrashedError, always check out a fresh session — the old one is poisoned"],"tags":["typescript","lifecycle","session"],"backgroundTag":"invalid-state-transition","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}