From 4a4b28e5e694c1cc7fa50595f103b028543b3df1 Mon Sep 17 00:00:00 2001 From: Epinephrine <27862058+luvs01@users.noreply.github.com> Date: Sun, 27 Sep 2026 07:51:03 +0000 Subject: [PATCH] fix(search): retain sidecar probe until error body settlement --- src/server/responses/core.ts | 1 - src/server/responses/sidecar-execution.ts | 3 ++ structure/transports/responses.md | 4 +- .../responses-run-turn-web-search.test.ts | 48 +++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 0f231ad86e3..a1ad4a15b37 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -140,7 +140,6 @@ async function handleResponsesInner( responseEffects, sendBudgetState, ); - if (sidecarPlans instanceof Response && !sidecarPlans.ok) sidecarState.openAiSidecar?.releaseProbeLease?.(); if (sidecarPlans instanceof Response) return sidecarPlans; const completionPolicy = createResponsesCompletionPolicy(requestContext, sidecarState); if (transportState.adapter.runTurn) return await executeResponsesRunTurn( diff --git a/src/server/responses/sidecar-execution.ts b/src/server/responses/sidecar-execution.ts index a0cb6ac07d5..ccdc4ecb55a 100644 --- a/src/server/responses/sidecar-execution.ts +++ b/src/server/responses/sidecar-execution.ts @@ -144,6 +144,8 @@ export async function executeResponsesSidecars( || (message as { toolCallId: string }).toolCallId.length === 0), ); if (unpaired) { + // Local validation has no running sidecar body to own this lease. + releaseSearchProbeLease(); // Never interpolate the tool output: this message reaches the client and the logs. return formatErrorResponse( 400, @@ -341,6 +343,7 @@ export async function executeResponsesSidecars( // the bridge entirely so enabling the feature doesn't break ordinary non-streaming traffic. if (!parsed.stream) { if (imgPlan) { + releaseSearchProbeLease(); return formatErrorResponse(400, "invalid_request_error", "image bridge requires stream=true"); } // Video-only: skip bridge for non-streaming requests diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 8320d296bac..13146527d14 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -61,6 +61,8 @@ the code let a provider-scoped transport past it is what #4992 recorded, and it regression for this policy has to enter through `handleResponses` rather than through a hand-written override that cooperates by calling the executor it was handed. +`src/server/responses/sidecar-execution.ts` owns search probe settlement: local validation releases immediately, bodyless responses release before returning, and upstream bodies retain the lease through completion, error or cancellation regardless of HTTP status. The core dispatcher does not infer body completion from a non-success status. + ### Semantic progress ownership The Responses proxy does not treat transcript growth as repository progress. It can observe request @@ -72,9 +74,7 @@ retention limits, and the stall watchdog is a silence limit. None is a cumulativ semantic no-progress budget. > Decision record: [ADR-0031](../decisions/ADR-0031-responses-http-sse.md) - > Decision record: [ADR-0032](../decisions/ADR-0032-responses-http-sse.md) - > Decision record: [ADR-0033](../decisions/ADR-0033-responses-http-sse.md) > Decision record: [ADR-0034](../decisions/ADR-0034-responses-http-sse.md) diff --git a/tests/responses/responses-run-turn-web-search.test.ts b/tests/responses/responses-run-turn-web-search.test.ts index afbf1d50c49..6bf4d968f37 100644 --- a/tests/responses/responses-run-turn-web-search.test.ts +++ b/tests/responses/responses-run-turn-web-search.test.ts @@ -68,11 +68,19 @@ mock.module("../../src/server/responses/request-sidecar-auth", () => ({ ...sidec } as Awaited>; }, })); +let fixtureSidecarResponse: Response | undefined; +const webSearchModule = await import("../../src/web-search"); +const executeWebSearch = webSearchModule.runWithWebSearch; +mock.module("../../src/web-search", () => ({ ...webSearchModule, + runWithWebSearch: async (...args: Parameters) => + fixtureSidecarResponse ?? executeWebSearch(...args), +})); const { handleResponses } = await import("../../src/server/responses"); const originalHome = process.env.OPENCODEX_HOME; let home = ""; let release: (() => void) | undefined; beforeEach(async () => { + fixtureSidecarResponse = undefined; home = mkdtempSync(join(tmpdir(), "ocx-runturn-search-")); process.env.OPENCODEX_HOME = home; release = acquireOwnedSpendHome(); @@ -319,3 +327,43 @@ test("superseded 429 attempt cannot restore stale route state", async () => { expect(owner(attempts[1])).not.toBe(owner(attempts[0])); expect(owner(attempts[1])).not.toBe("stale-superseded"); }); + +for (const settlement of ["complete", "cancel", "error"] as const) { + test(`a non-success sidecar body retains its probe until ${settlement}`, async () => { + releasedFixtureProbe = false; + let controller!: ReadableStreamDefaultController; + fixtureSidecarResponse = new Response(new ReadableStream({ + start(value) { controller = value; }, + }), { status: 503, headers: { "content-type": "text/event-stream" } }); + const config = { + port: 0, defaultProvider: "fetchonly", + webSearchSidecar: { backend: "exa", exaApiKey: "fixture-search-key" }, + providers: { fetchonly: { adapter: "fetchonly", baseUrl: "https://fetchonly.test/v1", + apiKey: "fixture-key", models: ["model"] } }, + } as OcxConfig; + let response: Response | undefined; + try { + response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json", "x-fixture-probe": "held" }, + body: JSON.stringify({ model: "fetchonly/model", input: "search this", stream: true, + tools: [{ type: "web_search" }] }), + }), config, { model: "", provider: "" }); + expect(response.status).toBe(503); + expect(releasedFixtureProbe).toBe(false); + if (settlement === "complete") { + controller.enqueue(new TextEncoder().encode("data: fixture-error\n\n")); + controller.close(); + expect(await response.text()).toContain("fixture-error"); + } else if (settlement === "cancel") { + await response.body!.cancel(); + } else { + controller.error(new Error("fixture body failure")); + await expect(response.text()).rejects.toThrow("fixture body failure"); + } + expect(releasedFixtureProbe).toBe(true); + } finally { + if (response?.body && !response.bodyUsed) await response.body.cancel(); + fixtureSidecarResponse = undefined; + } + }); +}