From 6cba271e65a95c4d4d5d58bdd6aaa4916131bc03 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 2 Sep 2026 21:37:34 -0500 Subject: [PATCH] fix(runner): restore ACPX backend identity fallback --- .../acpx/codex-runtime-adapter.test.ts | 51 ++++++++++++++----- .../src/drivers/acpx/codex-runtime-adapter.ts | 32 +++++++----- 2 files changed, 56 insertions(+), 27 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 c3a6418251..02839c31b9 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 @@ -1428,26 +1428,45 @@ describe("Codex ACPX runtime adapter", () => { }, ); - it("fails closed and closes the session when ACPX omits recovery identity", async () => { + it("uses the real ACP session when no second agent identity is advertised", async () => { const runtime = fakeRuntime({ ...HANDLE, agentSessionId: undefined }); - await expect( - openCodexAcpxRuntime(openOptions(fakeCommand()), { - createRegistry: () => registry(), - createStore: () => store(), - createRuntime: () => runtime, - }), - ).rejects.toThrow("ACPX runtime omitted agentSessionId"); - expect(runtime.close).toHaveBeenCalledWith({ - handle: { ...HANDLE, agentSessionId: undefined }, - reason: "ACPX runtime identity validation failed", - discardPersistentState: false, + const durableStore: AcpSessionStore = { + load: vi.fn(async () => + structuredClone({ + acpxRecordId: "record-1", + acpSessionId: "backend-1", + acpx: { current_model_id: "gpt-5.6-sol" }, + } as never), + ), + save: vi.fn(), + }; + const port = await openCodexAcpxRuntime(openOptions(fakeCommand()), { + createRegistry: () => registry(), + createStore: () => durableStore, + createRuntime: () => runtime, }); + + await expect(port.identity()).resolves.toEqual({ + acpxRecordId: "record-1", + backendSessionId: "backend-1", + agentSessionId: "backend-1", + }); + await expect(port.getStatus()).resolves.toMatchObject({ + backendSessionId: "backend-1", + agentSessionId: "backend-1", + models: { currentModelId: "gpt-5.6-sol" }, + }); + expect(runtime.close).not.toHaveBeenCalled(); }); it("bounds invalid-identity cleanup before terminating the provider", async () => { vi.useFakeTimers(); try { - const runtime = fakeRuntime({ ...HANDLE, agentSessionId: undefined }); + const runtime = fakeRuntime({ + ...HANDLE, + backendSessionId: undefined, + agentSessionId: undefined, + }); vi.mocked(runtime.close).mockImplementation( () => new Promise(() => undefined), ); @@ -1466,7 +1485,11 @@ describe("Codex ACPX runtime adapter", () => { args: ["--stdio"], options: {}, }); - return { ...HANDLE, agentSessionId: undefined }; + return { + ...HANDLE, + backendSessionId: undefined, + agentSessionId: undefined, + }; }), }), }); 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 8ccdb20bbb..9502df5eb7 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -1048,10 +1048,12 @@ async function persistedRuntimeStatus( { code: "ACPX_PERSISTED_SESSION_MISSING" }, ); } + const persistedAgentSessionId = + nonEmptyRuntimeIdentity(record.agentSessionId) ?? record.acpSessionId; if ( record.acpxRecordId !== identity.acpxRecordId || record.acpSessionId !== identity.backendSessionId || - record.agentSessionId !== identity.agentSessionId + persistedAgentSessionId !== identity.agentSessionId ) { throw Object.assign( new Error("The persisted ACPX session identity changed after admission"), @@ -1064,12 +1066,12 @@ async function persistedRuntimeStatus( summary: [ `session=${record.acpxRecordId}`, `backendSessionId=${record.acpSessionId}`, - `agentSessionId=${record.agentSessionId}`, + `agentSessionId=${persistedAgentSessionId}`, record.closed === true ? "closed" : "open", ].join(" "), acpxRecordId: record.acpxRecordId, backendSessionId: record.acpSessionId, - agentSessionId: record.agentSessionId, + agentSessionId: persistedAgentSessionId, ...(currentModelId === undefined && !availableModelIds?.length ? {} : { @@ -1491,17 +1493,21 @@ function pushUnique(errors: unknown[], error: unknown): void { } function requireIdentity(handle: AcpRuntimeHandle): AcpxRuntimePortIdentity { - const identity = { - acpxRecordId: handle.acpxRecordId, - backendSessionId: handle.backendSessionId, - agentSessionId: handle.agentSessionId, - }; - for (const [name, value] of Object.entries(identity)) { - if (typeof value !== "string" || value.length === 0) { - throw new Error(`ACPX runtime omitted ${name}`); - } + const acpxRecordId = nonEmptyRuntimeIdentity(handle.acpxRecordId); + if (!acpxRecordId) throw new Error("ACPX runtime omitted acpxRecordId"); + const backendSessionId = nonEmptyRuntimeIdentity(handle.backendSessionId); + if (!backendSessionId) { + throw new Error("ACPX runtime omitted backendSessionId"); } - return identity as AcpxRuntimePortIdentity; + return { + acpxRecordId, + backendSessionId, + // ACPX agents do not all advertise a distinct native thread identity. + // In that case the backend ID is the real ACP protocol session, so retain + // it explicitly rather than inventing a Paperclip-owned identifier. + agentSessionId: + nonEmptyRuntimeIdentity(handle.agentSessionId) ?? backendSessionId, + }; } function definedEnvironment(