Skip to content

Think: TurnResult.message is the session leaf, not the message this turn produced #2264

Closed

Description

@MatthewHager

Summary

TurnResult.message is documented as part of the wait-mode result, but _enrichTurnResult fills it from session.getLatestLeaf() after the turn queue has released. That is the newest message in the conversation, not the assistant message this turn produced.

Two consequences:

  1. A queued follow-up can already have appended its user message, so a completed wait turn resolves with message: undefined (see think: runTurn({ mode: "wait" }) resolves with message: undefined when a second turn is queued on the same agent #2243).
  2. After submitMessages + inspectSubmission, there is no message at all: ThinkSubmissionInspection has submissionId, status, error, timestamps — no message. A workflow that admitted the turn by submission id cannot tell which assistant message that turn wrote.

Environment

  • @cloudflare/think@0.17.0
  • runTurn({ mode: "wait" }) and submitMessages / inspectSubmission

What the types allow

type TurnResult = SaveMessagesResult & {
  message?: SessionMessage;
  continuation: boolean;
};

type ThinkSubmissionInspection = {
  submissionId: string;
  idempotencyKey?: string;
  requestId?: string;
  status: ThinkSubmissionStatus;
  error?: string;
  metadata?: Record<string, unknown>;
  createdAt: number;
  startedAt?: number;
  completedAt?: number;
};

Reproduction (wait)

Two runTurn({ mode: "wait" }) calls on the same DO, overlapping:

await Promise.all([
  stub.runTurn({ mode: "wait", input: "q1" }),
  stub.runTurn({ mode: "wait", input: "q2" }),
]);

The first result can be { status: "completed", message: undefined } because the leaf is already the second turn's user message. #2243 has the _enrichTurnResult / TurnQueue trace.

Reproduction (submit)

const { submissionId } = await agent.submitMessages([userMessage], {
  idempotencyKey: "import:instance:capture:homes",
});
const inspection = await agent.inspectSubmission(submissionId);
// inspection.status === "completed"
// inspection.message is not a field

The caller has to walk getMessages() and guess which assistant row belongs to this submission.

Workaround

Record the user message id at admit time and take the first assistant after it in getMessages(). That is stable for both wait and submit. For wait, do not trust result.message.

Suggested fix

Attach the assistant message produced by this turn to the result, captured while the queue slot is still held (or looked up by requestId). For submissions, put that message (or its id) on ThinkSubmissionInspection once status === "completed".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    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