fix: serialize tool result delivery to preserve message ordering (#21231)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 68adbf58c8
Co-authored-by: ahdernasr <44983175+ahdernasr@users.noreply.github.com>
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
Reviewed-by: @joshavant
This commit is contained in:
ahdernasr
2026-02-20 01:23:23 +00:00
committed by GitHub
parent d871ee91d0
commit e321f21daa
3 changed files with 83 additions and 21 deletions

View File

@@ -533,6 +533,61 @@ describe("runReplyAgent typing (heartbeat)", () => {
vi.useRealTimers();
});
it("delivers tool results in order even when dispatched concurrently", async () => {
const deliveryOrder: string[] = [];
const onToolResult = vi.fn(async (payload: { text?: string }) => {
// Simulate variable network latency: first result is slower than second
const delay = payload.text === "first" ? 50 : 10;
await new Promise((r) => setTimeout(r, delay));
deliveryOrder.push(payload.text ?? "");
});
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => {
// Fire two tool results without awaiting — simulates concurrent tool completion
void params.onToolResult?.({ text: "first", mediaUrls: [] });
void params.onToolResult?.({ text: "second", mediaUrls: [] });
// Small delay to let the chain settle before returning
await new Promise((r) => setTimeout(r, 150));
return { payloads: [{ text: "final" }], meta: {} };
});
const { run } = createMinimalRun({
typingMode: "message",
opts: { onToolResult },
});
await run();
expect(onToolResult).toHaveBeenCalledTimes(2);
// Despite "first" having higher latency, it must be delivered before "second"
expect(deliveryOrder).toEqual(["first", "second"]);
});
it("continues delivering later tool results after an earlier tool result fails", async () => {
const delivered: string[] = [];
const onToolResult = vi.fn(async (payload: { text?: string }) => {
if (payload.text === "first") {
throw new Error("simulated delivery failure");
}
delivered.push(payload.text ?? "");
});
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => {
void params.onToolResult?.({ text: "first", mediaUrls: [] });
void params.onToolResult?.({ text: "second", mediaUrls: [] });
await new Promise((r) => setTimeout(r, 50));
return { payloads: [{ text: "final" }], meta: {} };
});
const { run } = createMinimalRun({
typingMode: "message",
opts: { onToolResult },
});
await run();
expect(onToolResult).toHaveBeenCalledTimes(2);
expect(delivered).toEqual(["second"]);
});
it("announces auto-compaction in verbose mode and tracks count", async () => {
await withTempStateDir(async (stateDir) => {
const storePath = path.join(stateDir, "sessions", "sessions.json");