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();