fix(agents): await block-reply flush before tool execution starts

handleToolExecutionStart() flushed pending block replies and then called
onBlockReplyFlush() as fire-and-forget (`void`). This created a race where
fast tool results (especially media on Telegram) could be delivered before
the text block that preceded the tool call.

Await onBlockReplyFlush() so the block pipeline finishes before tool
execution continues, preserving delivery order.

Fixes #25267

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
SidQin-cyber
2026-02-24 21:13:34 +08:00
committed by Peter Steinberger
parent 4d124e4a9b
commit 99d854db82
2 changed files with 32 additions and 1 deletions

View File

@@ -88,6 +88,37 @@ describe("handleToolExecutionStart read path checks", () => {
expect(warn).toHaveBeenCalledTimes(1);
expect(String(warn.mock.calls[0]?.[0] ?? "")).toContain("read tool called without path");
});
it("awaits onBlockReplyFlush before continuing tool start processing", async () => {
const { ctx, onBlockReplyFlush } = createTestContext();
let releaseFlush: (() => void) | undefined;
onBlockReplyFlush.mockImplementation(
() =>
new Promise<void>((resolve) => {
releaseFlush = resolve;
}),
);
const evt: ToolExecutionStartEvent = {
type: "tool_execution_start",
toolName: "exec",
toolCallId: "tool-await-flush",
args: { command: "echo hi" },
};
const pending = handleToolExecutionStart(ctx, evt);
// Let the async function reach the awaited flush Promise.
await Promise.resolve();
// If flush isn't awaited, tool metadata would already be recorded here.
expect(ctx.state.toolMetaById.has("tool-await-flush")).toBe(false);
expect(releaseFlush).toBeTypeOf("function");
releaseFlush?.();
await pending;
expect(ctx.state.toolMetaById.has("tool-await-flush")).toBe(true);
});
});
describe("handleToolExecutionEnd cron.add commitment tracking", () => {

View File

@@ -174,7 +174,7 @@ export async function handleToolExecutionStart(
// Flush pending block replies to preserve message boundaries before tool execution.
ctx.flushBlockReplyBuffer();
if (ctx.params.onBlockReplyFlush) {
void ctx.params.onBlockReplyFlush();
await ctx.params.onBlockReplyFlush();
}
const rawToolName = String(evt.toolName);