Skip to content

Parallel streamText calls on one prompt kill each other's pending messages (failPendingSteps is unconditional) #294

Description

@kloudysky

Summary

saveInputMessages hardcodes failPendingSteps: !!args.promptMessageId (src/client/saveInputMessages.ts). failPendingSteps marks every status: "pending" message in the thread as failed (component messages.ts queries threadId_status_tool_order_stepOrder by thread + pending — not scoped to the prompt's order).

Consequence: two concurrent agent.streamText(...) calls with the same promptMessageId (fan-out: one user prompt → N models answering in parallel, e.g. a compare/arena feature) collide — the second call's startup marks the first call's in-flight pending message failed mid-stream. The stream keeps writing deltas, so you end up with a failed message containing the full response text. With saveStreamDeltas: true the UI shows the response aborting.

Repro (0.6.1)

const { messageId: promptMessageId } = await saveMessage(ctx, components.agent, { threadId, userId, prompt });
await Promise.all([
  agent.streamText(ctx, { threadId }, { promptMessageId, model: modelA }, { saveStreamDeltas: true }),
  agent.streamText(ctx, { threadId }, { promptMessageId, model: modelB }, { saveStreamDeltas: true }),
]);
// → whichever call starts second marks the first's pending message "failed".

Multiple concurrent streams per thread are otherwise well-supported (syncStreams merges them; streamingMessages rows are per order/stepOrder), so this bookkeeping collision appears to be the only blocker for parallel fan-out.

Ask

Expose an opt-out, e.g. storageOptions.failPendingSteps?: boolean (default current behavior) threaded through streamText/generateTextsaveInputMessagesaddMessages, so N-way fan-out on one prompt can run in parallel. Alternatively scope the pending-cleanup to messages not on the prompt's order.

Happy to send a PR if the opt-out shape sounds right.

Context: multi-model compare mode (same prompt → 2-3 models side-by-side, user picks a winner). Currently working around it by running the fan-out sequentially.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions