pydantic/monty · error · Error

snapshot has already been resumed

Error message

snapshot has already been resumed

What it means

The `SingleUse` class marks each snapshot as resumable at most once. `claim()` throws this Error when code attempts to resume a snapshot that has already been consumed, preventing double-resume of a paused worker turn. Snapshots are single-use by design: the underlying worker state can only advance once per captured point.

Source

Thrown at crates/monty-js/ts/session.ts:923

      (await this.native.resumeNameLookup(null, { value: prepare(value, this.instances) }, this.onPrint)) as NativeTurn,
    )
  }

  async resolveFutures(results: NativeFutureResult[]): Promise<Snapshot> {
    return this.advance((await this.native.resolveFutures(results, this.onPrint)) as NativeTurn)
  }

  async dump(): Promise<Buffer> {
    return bufferFrom(await this.native.dump())
  }
}

/** Marks a snapshot single-use: each may be resumed at most once. */
class SingleUse {
  private used = false
  protected claim(): void {
    if (this.used) {
      throw new Error('snapshot has already been resumed')
    }
    this.used = true
  }
}

/**
 * A paused execution waiting for an external function or OS call result. For
 * OS calls `isOsFunction` is `true`; resume with a value, an error, or
 * `resumeNotHandled()`.
 */
export class FunctionSnapshot extends SingleUse {
  readonly functionName: string
  /** Positional arguments, already converted to JS values. */
  readonly args: unknown[]
  /** Keyword arguments (null-prototype record; string keys only). */
  readonly kwargs: Record<string, unknown>
  readonly callId: number
  readonly isOsFunction: boolean

View on GitHub (pinned to adc986b362)

Solutions

  1. Resume each Snapshot exactly once; capture a fresh snapshot for every attempt by re-running or re-loading.
  2. Track claimed state in your own code (e.g. a Set of consumed snapshots) before calling resume.
  3. If you need to retry with a different resume value, reload the snapshot via `session.loadSnapshot` to obtain a fresh, unclaimed instance if supported, or re-feed the code.

Example fix

// before
const snap = await session.resumeNotHandled()
await snap.resumeReturn(value)
await snap.resumeReturn(value) // throws

// after
const snap = await session.resumeNotHandled()
if (!consumed.has(snap)) {
  consumed.add(snap)
  await snap.resumeReturn(value)
}
Defensive patterns

Strategy: validation

Validate before calling

function canResume(snap: Snapshot, consumed: WeakSet<Snapshot>): boolean {
  return !consumed.has(snap)
}

Try / catch

try {
  await snap.resumeReturn(value)
} catch (err) {
  if (err instanceof Error && err.message === 'snapshot has already been resumed') {
    // obtain a fresh snapshot or skip
  } else {
    throw err
  }
}

Prevention

When it happens

Trigger: Calling resumeReturn/resumeError/resumeNotHandled/resumeFuture (or any Snapshot resume method) twice on the same Snapshot object; storing a snapshot and re-resuming it after a previous resume already claimed it.

Common situations: Retry logic that captures a snapshot, resumes it, catches an error in the host callback, then tries to resume the same snapshot again with a different answer; sharing a snapshot across concurrent async tasks that both attempt resume.

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/eeed097da81f1742. Report an issue: GitHub.