pydantic/monty · error · Error

${what} expected event ${kind}, got ${event.tag}

Error message

${what} expected event ${kind}, got ${event.tag}

What it means

After sending a control request, `control()` verifies the reply's event tag matches the expected kind (e.g. `create-result`, `dump-result`, `finish-result`). A mismatch means the worker answered, but with the wrong kind of event — a protocol/schema inconsistency between the JS transport and the compiled wasm component. The message includes both the expected and actual tag for diagnosis.

Source

Thrown at crates/monty-js/ts/worker/transport.ts:314

        turn = aborted ? this.toTurn(aborted) : crashed('worker exited without a turn-ending event')
        // the component answers an abort with an error, never a suspension;
        // servicing one would let a compromised worker call the host past
        // the budget, so it ends the worker instead
        if (turn.kind !== 'error' && turn.kind !== 'crashed') {
          this.dead = true
          turn = { kind: 'protocol', message: `worker answered abort-feed with ${turn.kind}` }
        }
      }
    }
    if (turn.kind === 'crashed') this.dead = true
    return turn
  }

  /** Sends a control request and verifies its expected event kind. */
  private async control(request: ComponentRequest, kind: ComponentEvent['tag'], what: string): Promise<ComponentEvent> {
    const event = await this.run(request, undefined)
    if (!event) throw new Error(`${what} produced no turn-ending event (worker crashed)`)
    if (event.tag !== kind) throw new Error(`${what} expected event ${kind}, got ${event.tag}`)
    return event
  }

  /** Runs one turn, forwarding buffered prints and retaining its terminator. */
  private async run(request: ComponentRequest, onPrint: OnPrint | undefined): Promise<ComponentEvent | null> {
    let events: ComponentEvent[]
    try {
      const result = await this.dispatcher(request)
      if (result.status === 'shutdown') this.dead = true
      // the component reports the limit in force (the configured one, else
      // the 1000 default; a dump's on load), so it is adopted from the reply
      if (request.tag === 'configure' || request.tag === 'load') {
        this.suspensionLimit = result.maxSuspensions
        this.suspensionsSeen = 0n
      }
      events = result.events
    } catch {
      return null

View on GitHub (pinned to adc986b362)

Solutions

  1. Rebuild and redeploy the wasm component together with the TS layer (`make build-wasm`, then rebuild the JS bundle) so all protocol artifacts come from one source.
  2. Never hand-edit `crates/monty-js/ts/worker/component/` files — they are generated; regenerate them with the build.
  3. Ensure each session processes turns to completion (await each `feedRun`/`dump`/`finish`) so no stale events remain queued before the next control request.
  4. Pin matching versions of `@pydantic/monty` and the compiled component in your deployment.

Example fix

// before
// stale component + new TS bundle -> tag mismatch
const bytes = await session.dump()

// after
make build-wasm   # regenerate component + WIT-derived declarations
make build-js     # rebuild TS layer against them
const bytes = await session.dump()
Defensive patterns

Strategy: try-catch

Try / catch

try {
  result = await session.finish()
} catch (err) {
  if (err instanceof Error && /expected event .*, got /.test(err.message)) {
    // version skew: rebuild component + bindings, then retry on a fresh session
    session = await pool.checkout()
  } else throw err
}

Prevention

When it happens

Trigger: A control request receives an event whose `tag` differs from `kind` — typically caused by version skew between the checked-in WIT declarations, the compiled component, and the TS `ts/worker` layer, or by a worker emitting a terminator event from a previous unfinished turn.

Common situations: Hand-editing or regenerating only part of the component bindings; deploying a cached old wasm module behind an updated JS bundle; reusing a session whose previous turn ended abnormally so the next control call consumes a stale event.

Related errors


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