From da140226552c6983c7516139a125b7583f02fb8a Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 7 Nov 2026 12:06:36 -0600 Subject: [PATCH] fix(runner): name the local app-server process in Codex session start Local Codex and OpenCode (through the app-server proxy, which keeps opencode serve in its process group) run behind the Codex app-server driver. Its session.started carried no process identity, so the control plane's stop proof never checked that process. Emit its pid, process group and OS start time. Co-Authored-By: Claude Opus 5.5 --- .../codex/codex-app-server-driver.lifecycle.test.ts | 11 +++++++++++ .../src/drivers/codex/codex-session-state.ts | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts index 62e97dc75e4..4bc5843d8ce 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts @@ -311,6 +311,17 @@ describe("Codex app-server Codex driver", () => { expect(transport.calls[0]?.method).toBe("initialize"); }); + it("names the local app-server process in session.started for the control plane's stop proof", async () => { + const transport = new FakeCodexTransport(); + Object.assign(transport, { + processInfo: () => ({ pid: process.pid, processGroupId: null, startedAt: "2026-08-18T18:00:00.000Z", exited: false, exitCode: null, signal: null }), + }); + const session = await makeDriver([transport]).openSession({ runId: "run-identity", normalizedSessionId: "normalized-identity", workingDirectory: WORKSPACE }); + const started = await session.events()[Symbol.asyncIterator]().next(); + expect(started.value).toMatchObject({ eventType: "session.started", + payload: { processId: process.pid, processGroupId: null, processStartedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/) } }); + }); + it("persists process ownership after a lazy transport launches during session open", async () => { const transport = new FakeCodexTransport(); Object.assign(transport, { diff --git a/packages/paperclip-runner/src/drivers/codex/codex-session-state.ts b/packages/paperclip-runner/src/drivers/codex/codex-session-state.ts index 7e9e9cf862e..2f7c4cc366d 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-session-state.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-session-state.ts @@ -1,4 +1,5 @@ import { type CodexUsageBaseline, codexRunUsage } from "./codex-usage-baseline.js"; +import { readLocalProcessStartedAt } from "../../live/linux-process-start.js"; import type { ProviderRateLimitSnapshot } from "../provider-rate-limits.js"; import { workspaceRootsFor } from "../../tool-file-changes.js"; import type { @@ -626,9 +627,16 @@ export function initializeCodexSessionEvents( state: CodexSessionState, input: CodexSessionStateInput, ): void { + // Name the local app-server process (Codex, or the OpenCode proxy that owns `opencode serve` + // in its process group) so the control plane's stop proof can verify that exact process exited. + const processInfo = state.transport.processInfo?.(); + const processIdentity = processInfo && !processInfo.exited && processInfo.pid !== null + ? { processId: processInfo.pid, processGroupId: processInfo.processGroupId, processStartedAt: readLocalProcessStartedAt(processInfo.pid) } + : {}; state.emit(input.resumed ? "session.resumed" : "session.started", { driverSessionId: input.opened.threadId, providerSessionId: input.opened.providerSessionId, + ...processIdentity, context: input.opened.context, }); state.emitGoalCapabilities();