From cb425d35bcd1f5d9a3eaff8e8a714c09d915586d Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 11 Sep 2026 14:37:05 -0500 Subject: [PATCH] Observe native provider admission failures without crashing the sidecar --- .../acpx/codex-runtime-adapter.test.ts | 26 +++++++++++++ .../src/drivers/acpx/codex-runtime-adapter.ts | 13 ++++++- .../native-session-executor.test.ts | 39 ++++++++++++------- 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts index 4b0584264e..a2570ee484 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts @@ -1311,6 +1311,32 @@ describe("Codex ACPX runtime adapter", () => { }); }); + it("observes prompt admission rejection when the sidecar consumes only events and the result", async () => { + const runtime = fakeRuntime(); + const failure = new Error("Recovered provider could not start the prompt"); + vi.mocked(runtime.startTurn).mockImplementation(() => ({ + requestId: "turn-recovered-failure", + promptStarted: Promise.reject(failure), + events: { async *[Symbol.asyncIterator]() { throw failure; } }, + result: Promise.reject(failure), + cancel: vi.fn(), + closeStream: vi.fn(), + })); + const port = await openCodexAcpxRuntime(openOptions(fakeCommand()), { + createRegistry: () => registry(), createStore: () => store(), createRuntime: () => runtime, + }); + const turn = port.startTurn({ text: "Resume", requestId: "turn-recovered-failure" }); + const eventDrain = (async () => { for await (const _event of turn.events) { /* drain */ } })(); + await expect(eventDrain).rejects.toBe(failure); + // The sidecar does not await promptStarted. Leave it unconsumed across a + // full event-loop turn so an unobserved derived rejection fails this test. + await new Promise((resolve) => setImmediate(resolve)); + // Observing internally must not replace failure with successful admission. + await expect(turn.result).rejects.toBe(failure); + await expect(turn.promptStarted).rejects.toBe(failure); + await port.close({ reason: "test complete" }); + }); + it("verifies a lazy recovered provider spawned by model selection before returning", async () => { const runtime = fakeRuntime(); const command = fakeCommand(); diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts index a499b407ec..8c1ad01c2b 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -1221,11 +1221,20 @@ function turnWithVerifiedLifetimeOwnership( finishOwnershipAdmission(), ); void ownershipVerified.catch(() => undefined); + const promptStarted = ownershipVerified.then(() => turn.promptStarted); + const result = ownershipVerified.then(() => turn.result); + // Some consumers (including the sidecar) drain events and await the result + // without awaiting this optional admission signal. Observe its rejection + // immediately so a failed cold start cannot terminate the host process as an + // unhandled rejection. Keep the original rejected promise for consumers. + void promptStarted.catch(() => undefined); + // Event drains can fail before their caller reaches the result promise. + void result.catch(() => undefined); return { requestId: turn.requestId, - promptStarted: ownershipVerified.then(() => turn.promptStarted), + promptStarted, events: eventsAfterLifetimeOwnership(turn.events, ownershipVerified), - result: ownershipVerified.then(() => turn.result), + result, cancel: (input) => turn.cancel(input), closeStream: (input) => turn.closeStream(input), }; diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 8cf2974425..4300ae87e7 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -3135,19 +3135,32 @@ describe("native session same-turn steering", () => { }); describe("native warm session supervision", () => { - it.each([true, false])("preserves chat reply grace for per-turn providers: chat=%s", async (conversationMode) => { - state.execute.mockReset().mockImplementationOnce(async (options) => { - expect(options.semanticResultTerminalGraceMs).toBe(conversationMode ? 30_000 : undefined); - return { - result: { summary: "Reply completed" }, - terminal: { runTerminalState: "succeeded" }, - turnId: "turn-grace", normalizedSessionId: execution.session.normalizedSessionId, - providerSessionId: "provider-grace", driverKind: "test", driverVersion: "1", - nativeEventCount: 1, highestContiguousSourceSeq: 1, - }; - }); - await executePaperclipNativeSession({ db: leaseDb(), execution, runnerInstanceId: "runner", conversationMode }); - }); + it.each([true, false])( + "preserves chat reply grace for per-turn providers: chat=%s", + async (conversationMode) => { + state.execute.mockReset().mockImplementationOnce(async (options) => { + expect(options.semanticResultTerminalGraceMs).toBe(conversationMode ? 30_000 : undefined); + return { + result: { summary: "Reply completed" }, + terminal: { runTerminalState: "succeeded" }, + turnId: "turn-grace", + normalizedSessionId: execution.session.normalizedSessionId, + providerSessionId: "provider-grace", + driverKind: "test", + driverVersion: "1", + nativeEventCount: 1, + highestContiguousSourceSeq: 1, + }; + }); + await executePaperclipNativeSession({ + db: leaseDb(), + execution, + runnerInstanceId: "runner", + conversationMode, + }); + }, + ); + it("persists agent-created goal continuity before a per-turn runner settles", async () => { const goalCheckpoint = { identity: { runId: execution.binding.runId, sessionId: "session" },