From e26bae5b2af407320e27f25f0803cc468ef0f277 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 14 Aug 2026 21:10:10 -0700 Subject: [PATCH] fix(xai): fence cancelled x_search cache writes (#123996) Prevent a completed standalone x_search request from building or caching a result after caller cancellation. Preserve the exact abort reason and force identical retries back through the provider request path.\n\nFixes #123964 --- extensions/xai/x-search.test.ts | 23 +++++++++++++++++++++++ extensions/xai/x-search.ts | 1 + 2 files changed, 24 insertions(+) diff --git a/extensions/xai/x-search.test.ts b/extensions/xai/x-search.test.ts index 476576d9f91c..adea96d5a51f 100644 --- a/extensions/xai/x-search.test.ts +++ b/extensions/xai/x-search.test.ts @@ -276,6 +276,29 @@ describe("xai x_search tool", () => { expect(mockFetch).not.toHaveBeenCalled(); }); + it("does not cache an X search result completed after caller cancellation", async () => { + const controller = new AbortController(); + const reason = new Error("operator cancelled X search after response"); + const mockFetch = vi + .fn() + .mockImplementationOnce(async () => { + controller.abort(reason); + return jsonResponse({ output_text: "Cancelled X answer", citations: [] }); + }) + .mockResolvedValueOnce(jsonResponse({ output_text: "Recovered X answer", citations: [] })); + global.fetch = withFetchPreconnect(mockFetch); + const tool = createConfiguredXSearchTool(); + const query = "unique standalone x_search late-cancel cache regression"; + + await expect(tool.execute("xai-late-cancel", { query }, controller.signal)).rejects.toBe( + reason, + ); + const recovered = await tool.execute("xai-late-cancel-retry", { query }); + + expect(mockFetch).toHaveBeenCalledTimes(2); + expect((recovered.details as { content?: string }).content).toContain("Recovered X answer"); + }); + it("uses the xAI Responses x_search tool with structured filters", async () => { const mockFetch = installXSearchFetch(); const tool = createConfiguredXSearchTool({ xSearch: { maxTurns: 2 } }); diff --git a/extensions/xai/x-search.ts b/extensions/xai/x-search.ts index 4d911550cb97..e3e34fccee7a 100644 --- a/extensions/xai/x-search.ts +++ b/extensions/xai/x-search.ts @@ -242,6 +242,7 @@ export function createXSearchTool(options?: { options: xSearchOptions, ...(signal ? { signal } : {}), }); + signal?.throwIfAborted(); const payload = buildXaiXSearchPayload({ query, model,