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

  1. Check out a new session with `pool.checkout()`
  2. If the thrown error is the stored crash/protocol error, treat the worker as dead — the pool replaces it on next checkout
  3. 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

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


AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13). Data as JSON: /api/errors/86d5a2d4e478a26c. Report an issue: GitHub.