Skip to content

think: runTurn({ mode: "wait" }) resolves with message: undefined when a second turn is queued on the same agent #2243

Closed

Description

@MatthewHager

Summary

runTurn({ mode: "wait" }) reports the finished turn's assistant message by reading the session's newest message after the turn queue has released its slot. When a second turn is already queued on the same agent, that queued turn can append its user message first, so the completed turn resolves with status: "completed" and message: undefined.

@cloudflare/think@0.17.0, dist/think.js 3758–3768:

async _enrichTurnResult(result, continuation) {
  let message;
  if (result.status === "completed") {
    const leaf = await this.session.getLatestLeaf();
    if (leaf?.role === "assistant") message = leaf;
  }
  return { ...result, continuation, message };
}

_runTurnWait awaits _runProgrammaticMessagesTurn (which holds the TurnQueue slot) and only then calls _enrichTurnResult, so the lookup happens outside the serialized region:

const result = await this._runProgrammaticMessagesTurn(crypto.randomUUID(), input, { ... });
return this._enrichTurnResult(result, false);

TurnQueue.enqueue (agents/dist/chat/index.js 164–189) releases in a finally before the caller's continuation runs, so the next turn's execute — which begins by appending its user message — is free to interleave.

The turn itself is fine: both turns run, both messages persist, and the session is correct afterwards. Only the reported message is wrong, which makes a correct turn look like a failure to the caller.

Environment

  • @cloudflare/think@0.17.0, agents@0.22.0 (current npm latest)
  • Cloudflare Workers / workerd, Think subclass in a Durable Object
  • Same code on main: packages/think/src/think.ts _enrichTurnResult

Reproduction

Two RPC calls on one agent instance, each calling runTurn({ mode: "wait" }) with a function input:

await Promise.all([
  stub.appendTurn({ q: "any 2 bedrooms?" }),
  stub.appendTurn({ q: "book a tour" }),
]);

Probe inside the first call's continuation:

{"status":"completed","leafRole":"user"}

Expected leafRole: "assistant" for the turn that just completed; the leaf is the other turn's question. Reproduces reliably on a cold install and intermittently otherwise — it is a timing race, so it surfaces as a flaky test or an occasional 5xx in production rather than a hard failure.

Suggested fix

Resolve the assistant message from the run that produced it rather than from session-global state — either capture the leaf inside the admitted turn body (still holding the queue slot) and pass it out with the result, or look it up by the turn's requestId. Reading getLatestLeaf() after the slot is released is only correct when exactly one turn ever exists.

Workaround

Callers can record the id of the user message they submit and take the first assistant message after it in getMessages(), which is stable regardless of what else is queued.

Activity

  1. L4XB commented on Sep 11, 2026

    @L4XB

    A fix for this is on https://github.com/L4XB/agents/tree/fix/think-run-turn-wait-message (the queued turn reads its assistant leaf while it still holds the turn-queue slot and carries it in its result; _enrichTurnResult uses that instead of the post-release lookup; changeset + tests included). GitHub refuses to let me open the pull request against this repository (CreatePullRequest permission error, both from a fresh fork and a renamed one), so if a maintainer can open it or lift the restriction, it is ready.

  2. self-assigned this
    on Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions