pydantic/monty · error · Error
the session is closed — check out a new one
Error message
the session is closed — check out a new one
What it means
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.
Source
Thrown at crates/monty-js/ts/session.ts:446
return
}
this.closed = true
await this.native.finish()
}
async [Symbol.asyncDispose](): Promise<void> {
await this.close()
}
/** Poisons the session over a worker death or protocol violation. */
private poison(err: Error): Error {
this.broken = err
return err
}
private ensureUsable(): void {
if (this.closed) {
throw new Error('the session is closed — check out a new one')
}
if (this.broken !== null) {
throw this.broken
}
}
}
/**
* Answers one suspension turn from a captured `externalLookup` / `os`, tracking
* promise-returning externals as pending futures. Shared by
* [`MontySession.feedRun`]'s drive loop and [`SnapshotDriver`]'s `resumeAuto`
* so both resolve suspensions identically. Built fresh per feed / per snapshot
* chain — its `futures` map is scoped to that run, never leaking across feeds.
*
* `answer` deliberately does **not** catch: a handler that throws leaves the
* worker suspended, so the caller poisons the session and rethrows.
*/
class TurnAnswerer {View on GitHub (pinned to adc986b362)
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
Example fix
// before
await using session = await pool.checkout()
await session.feedRun('1+1')
// after disposal:
await session.feedRun('2+2') // throws
// after
const fresh = await pool.checkout()
await fresh.feedRun('2+2') Defensive patterns
Strategy: try-catch
Validate before calling
if (sessionClosed || sessionBroken) {
session = await pool.checkout()
} Type guard
function isUsable(session: MontySession): boolean {
return !session.closed // and no prior crash poisoned it
} Try / catch
try {
await session.feedRun(code)
} catch (e) {
if (e instanceof Error && e.message.includes('the session is closed') || e instanceof MontyCrashedError) {
session = await pool.checkout()
await session.feedRun(code)
} else throw e
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- the pool is closed — create a new Monty pool
- loadSession / loadSnapshot is only valid on a fresh session,
- ClassInstance expects an object instance
- notCallableMessage(method)
- ClassInstance expects an instance of a class, not a null-pro
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/86d5a2d4e478a26c.
Report an issue: GitHub.