{"record":{"id":"e5c0352e0e46733b","repo":"pydantic/monty","slug":"worker-reported-resolvefutures-with-no-pending-call-ids","errorCode":null,"errorMessage":"worker reported ResolveFutures with no pending call ids","messagePattern":"worker reported ResolveFutures with no pending call ids","errorType":"exception","errorClass":"ProtocolError","httpStatus":null,"severity":"error","filePath":"crates/monty-js/ts/session.ts","lineNumber":716,"sourceCode":"      },\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) {\n            const { excType, message } = jsErrorParts(err)\n            return { callId: f.callId, ok: false, excType, message }\n          }\n        }\n        const { excType, message } = jsErrorParts(outcome.err)","sourceCodeStart":698,"sourceCodeEnd":734,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty-js/ts/session.ts#L698-L734","documentation":"The worker sent a `ResolveFutures` turn with an empty `pendingCallIds` list, which is meaningless — the turn exists only to report pending futures. The host throws `ProtocolError` because the protocol requires at least one pending call id; this signals a worker bug or version mismatch.","triggerScenarios":"A worker (wrong version, buggy, or compromised) emitting `ResolveFutures` with no ids after an external-call turn; host and worker disagreeing about whether futures are outstanding.","commonSituations":"Mixed worker/binding versions after an upgrade, custom worker builds, or protocol regressions.","solutions":["Align the monty worker binary with the installed @pydantic/monty package version","Re-run the workload on a fresh session; the pool replaces misbehaving workers","Report a bug with reproduction if a matched version still produces it"],"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('no pending call ids')) {\n    session = await pool.checkout() // worker desync; get a fresh one\n  } else throw e\n}","preventionTips":["Use a matched worker binary and JS package version","Report occurrences with the turn trace — an empty ResolveFutures is always a worker bug","Discard the session after ProtocolError; the pool's crash replacement handles worker health"],"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"}