{"record":{"id":"ff9d097ead6c0f80","repo":"pydantic/monty","slug":"worker-reported-unknown-pending-call-id-id","errorCode":null,"errorMessage":"worker reported unknown pending call id ${id}","messagePattern":"worker reported unknown pending call id (.+?)","errorType":"exception","errorClass":"ProtocolError","httpStatus":null,"severity":"error","filePath":"crates/monty-js/ts/session.ts","lineNumber":711,"sourceCode":"        future.outcome = { ok }\n      },\n      (err: unknown) => {\n        future.done = true\n        future.outcome = { err }\n      },\n    )\n    this.futures.set(callId, future)\n  }\n\n  /**\n   * Every sandbox task is blocked: wait until at least one pending future\n   * settles, then deliver everything that is ready.\n   */\n  private async answerResolveFutures(event: ResolveFuturesTurn, onPrint: PrintCallback): Promise<object> {\n    const pending = event.pendingCallIds.map((id) => {\n      const future = this.futures.get(id)\n      if (future === undefined) {\n        throw new ProtocolError(`worker reported unknown pending call id ${id}`)\n      }\n      return future\n    })\n    if (pending.length === 0) {\n      throw new ProtocolError('worker reported ResolveFutures with no pending call ids')\n    }\n    await Promise.race(pending.map((f) => f.settled))\n    const results: NativeFutureResult[] = pending\n      .filter((f) => f.done)\n      .map((f) => {\n        this.futures.delete(f.callId)\n        const outcome = f.outcome!\n        if ('ok' in outcome) {\n          // a prepare rejection is delivered as the future's error, matching\n          // how a sync return value's conversion failure surfaces\n          try {\n            return { callId: f.callId, ok: true, value: prepare(outcome.ok, this.instances) }\n          } catch (err) {","sourceCodeStart":693,"sourceCodeEnd":729,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty-js/ts/session.ts#L693-L729","documentation":"The worker sent a `ResolveFutures` turn listing a pending external-call id the host has no record of. This breaks the protocol contract (both sides must agree on outstanding future ids), so a `ProtocolError` is thrown — it indicates a worker/host desync or a bug, not user input.","triggerScenarios":"A `ResolveFutures` event arrives whose `pendingCallIds` contains an id never registered by a host-side external call, or an id already resolved and deleted from the host map; typically only reachable via a mismatched/buggy worker binary or protocol version skew.","commonSituations":"Version mismatch between the JS package and the monty worker binary, a compromised or buggy worker, replaying turns out of order.","solutions":["Ensure the monty worker binary version matches the @pydantic/monty package version","Retry the turn on a fresh session; the pool detects crashes and replaces workers","If reproducible, file a bug with the turn/event trace — this is an internal invariant violation"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await session.feedRun(code, { externalLookup })\n} catch (e) {\n  if (e instanceof ProtocolError && e.message.includes('unknown pending call id')) {\n    // worker desync: discard session, let the pool replace the worker\n    session = await pool.checkout()\n  } else throw e\n}","preventionTips":["Keep the monty worker binary version in lockstep with the JS package version","Never reuse future/call ids across sessions; let the library manage them","Treat ProtocolError as fatal to the session — do not retry on the same session"],"tags":["protocol","typescript","internal"],"backgroundTag":"internal-invariant-violation","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}