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/generateText → saveInputMessages → addMessages, 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.
Summary
saveInputMessageshardcodesfailPendingSteps: !!args.promptMessageId(src/client/saveInputMessages.ts).failPendingStepsmarks everystatus: "pending"message in the thread asfailed(componentmessages.tsqueriesthreadId_status_tool_order_stepOrderby thread + pending — not scoped to the prompt's order).Consequence: two concurrent
agent.streamText(...)calls with the samepromptMessageId(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 messagefailedmid-stream. The stream keeps writing deltas, so you end up with afailedmessage containing the full response text. WithsaveStreamDeltas: truethe UI shows the response aborting.Repro (0.6.1)
Multiple concurrent streams per thread are otherwise well-supported (
syncStreamsmerges them;streamingMessagesrows 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 throughstreamText/generateText→saveInputMessages→addMessages, 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.