{"record":{"id":"84fdee96e318029b","repo":"pydantic/monty","slug":"what-expected-event-kind-got-event-tag","errorCode":null,"errorMessage":"${what} expected event ${kind}, got ${event.tag}","messagePattern":"(.+?) expected event (.+?), got (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/monty-js/ts/worker/transport.ts","lineNumber":314,"sourceCode":"        turn = aborted ? this.toTurn(aborted) : crashed('worker exited without a turn-ending event')\n        // the component answers an abort with an error, never a suspension;\n        // servicing one would let a compromised worker call the host past\n        // the budget, so it ends the worker instead\n        if (turn.kind !== 'error' && turn.kind !== 'crashed') {\n          this.dead = true\n          turn = { kind: 'protocol', message: `worker answered abort-feed with ${turn.kind}` }\n        }\n      }\n    }\n    if (turn.kind === 'crashed') this.dead = true\n    return turn\n  }\n\n  /** Sends a control request and verifies its expected event kind. */\n  private async control(request: ComponentRequest, kind: ComponentEvent['tag'], what: string): Promise<ComponentEvent> {\n    const event = await this.run(request, undefined)\n    if (!event) throw new Error(`${what} produced no turn-ending event (worker crashed)`)\n    if (event.tag !== kind) throw new Error(`${what} expected event ${kind}, got ${event.tag}`)\n    return event\n  }\n\n  /** Runs one turn, forwarding buffered prints and retaining its terminator. */\n  private async run(request: ComponentRequest, onPrint: OnPrint | undefined): Promise<ComponentEvent | null> {\n    let events: ComponentEvent[]\n    try {\n      const result = await this.dispatcher(request)\n      if (result.status === 'shutdown') this.dead = true\n      // the component reports the limit in force (the configured one, else\n      // the 1000 default; a dump's on load), so it is adopted from the reply\n      if (request.tag === 'configure' || request.tag === 'load') {\n        this.suspensionLimit = result.maxSuspensions\n        this.suspensionsSeen = 0n\n      }\n      events = result.events\n    } catch {\n      return null","sourceCodeStart":296,"sourceCodeEnd":332,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty-js/ts/worker/transport.ts#L296-L332","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Never hand-edit `crates/monty-js/ts/worker/component/` files — they are generated; regenerate them with the build.","Ensure each session processes turns to completion (await each `feedRun`/`dump`/`finish`) so no stale events remain queued before the next control request.","Pin matching versions of `@pydantic/monty` and the compiled component in your deployment."],"exampleFix":"// before\n// stale component + new TS bundle -> tag mismatch\nconst bytes = await session.dump()\n\n// after\nmake build-wasm   # regenerate component + WIT-derived declarations\nmake build-js     # rebuild TS layer against them\nconst bytes = await session.dump()","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  result = await session.finish()\n} catch (err) {\n  if (err instanceof Error && /expected event .*, got /.test(err.message)) {\n    // version skew: rebuild component + bindings, then retry on a fresh session\n    session = await pool.checkout()\n  } else throw err\n}","preventionTips":["Keep the wasm component, WIT declarations, and TS layer from one build.","Pin matching package + component versions in deployments.","Always await each turn to completion so no stale events queue.","Regenerate (never hand-edit) ts/worker/component files."],"tags":["protocol","versioning","wasm","wasm-worker"],"backgroundTag":"unexpected-response-shape","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}