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 d66084329dc..4c221b55189 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 @@ -70,12 +70,20 @@ describe("Codex ACPX runtime adapter", () => { const admittedFirst = port.startTurn({ text: "First", requestId: "turn-1", onExtensionRequest: request, onExtensionNotification: notification }); const signal = new AbortController().signal; await expect(created.onExtensionRequest!("cursor/ask_question", { sessionId: "backend-1" }, { requestId: 0, signal })).resolves.toEqual({ requestId: 0 }); + const sessionless = { toolCallId: "native-cursor-plan", plan: "Review the complete plan" }; + await expect(created.onExtensionRequest!("cursor/create_plan", sessionless, { requestId: "plan", signal })).resolves.toEqual({ requestId: "plan" }); + expect(request).toHaveBeenLastCalledWith("cursor/create_plan", { ...sessionless, sessionId: "backend-1" }, expect.objectContaining({ requestId: "plan" })); + expect(sessionless).not.toHaveProperty("sessionId"); await expect(created.onExtensionRequest!("pi/steer", {}, { requestId: 1, signal })).rejects.toThrow("admitted active turn"); await expect(created.onExtensionRequest!("cursor/ask_question", { sessionId: "other" }, { requestId: 1, signal })).rejects.toThrow("admitted active turn"); created.onExtensionNotification!("cursor/task", { sessionId: "backend-1", taskId: "t" }); created.onExtensionNotification!("cursor/task", { sessionId: "other" }); created.onExtensionNotification!("cursor/ask_question", { sessionId: "backend-1" }); expect(notification).toHaveBeenCalledExactlyOnceWith("cursor/task", { sessionId: "backend-1", taskId: "t" }); + created.onExtensionNotification!("cursor/task", { toolCallId: "native-child" }); + expect(notification).toHaveBeenLastCalledWith("cursor/task", { sessionId: "backend-1", toolCallId: "native-child" }); + created.onExtensionNotification!("cursor/task", { sessionId: "agent-1" }); + expect(notification).toHaveBeenCalledTimes(2); request.mockImplementationOnce(() => new Promise(() => undefined)); const pending = created.onExtensionRequest!("cursor/ask_question", {}, { requestId: 2, signal }); const rejected = expect(pending).rejects.toThrow("expired"); 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 77d540520e0..73d0bbfbf07 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -84,6 +84,7 @@ interface AcpxRuntimeGoalState { interface AcpxRuntimeExtensionTurn { requestId: string; + sessionId: string; controller: AbortController; signal: AbortSignal; promptStarted: Promise; @@ -320,7 +321,10 @@ export async function openQualifiedAcpxRuntime( throw new Error("ACPX extension request does not own an admitted active turn"); } const signal = AbortSignal.any([active.signal, context.signal]); - const response = await abortableExtensionResult(active.onRequest(method, params, { requestId: context.requestId, signal }), signal); + // Cursor's native extensions omit sessionId. Only after admission may + // that omission inherit this exact prompt's ACP wire session identity. + const boundParams = params.sessionId === undefined ? { ...params, sessionId: active.sessionId } : params; + const response = await abortableExtensionResult(active.onRequest(method, boundParams, { requestId: context.requestId, signal }), signal); signal.throwIfAborted(); if (!ownsExtensionTurn(active, params)) throw new Error("ACPX extension request turn expired"); return response; @@ -328,7 +332,7 @@ export async function openQualifiedAcpxRuntime( onExtensionNotification: (method, params) => { const active = extensionBoundary.active; if (!extensionNotifications.has(method) || !active?.onNotification || !ownsExtensionTurn(active, params)) return; - active.onNotification(method, params); + active.onNotification(method, params.sessionId === undefined ? { ...params, sessionId: active.sessionId } : params); }, nonInteractivePermissions: "fail", permissionPolicy: { @@ -1316,7 +1320,7 @@ function runtimePort( const approval = new AbortController(); const controller = new AbortController(); const extensionTurn: AcpxRuntimeExtensionTurn = { - requestId: input.requestId, controller, + requestId: input.requestId, sessionId: identity.backendSessionId, controller, signal: input.signal ? AbortSignal.any([controller.signal, input.signal, approval.signal]) : AbortSignal.any([controller.signal, approval.signal]), promptStarted: Promise.resolve(), onRequest: input.onExtensionRequest, onNotification: input.onExtensionNotification,