fix(runner): name the spawned opencode serve in OpenCode session start

The OpenCode server driver spawns `opencode serve` in its own process group but
its session.started/resumed carried no process identity. The server's stop proof
checks processId from these events, so it never verified that this provider
process had exited. Emit its pid, process group and OS start time, and share the
runner's local process start reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-07 11:48:28 -06:00
1 parent 51dc541273
commit af78a9c57f
4 files changed
+62 -44

No files matched your search

@@ -1188,6 +1188,12 @@ describe("OpenCodeServerDriver", () => {
expect(
events.filter((event) => event.eventType === "item.delta"),
).toHaveLength(1);
// The session names the exact spawned `opencode serve`, so cleanup can prove it is gone.
expect(events.find((event) => event.eventType === "session.started")?.payload).toMatchObject({
processId: spawns[0]!.pid,
processGroupId: spawns[0]!.processGroupId,
processStartedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
});
expect(
events.find((event) => event.eventType === "item.delta")?.payload.text,
).toBe("done [guide](guide.md)");
@@ -1,4 +1,5 @@
import { isOpenRouterUrl, OPENROUTER_ATTRIBUTION_HEADERS } from "../openrouter-attribution.js";
import { readLocalProcessStartedAt } from "../../live/linux-process-start.js";
import { spawn, type ChildProcess } from "node:child_process";
import { resolveCompletionContinuation } from "../completion-continuation.js";
import { createHash, randomBytes } from "node:crypto";
@@ -231,6 +232,8 @@ interface OpenCodeRuntime {
version: string;
permissionMode: "allow" | "ask" | "deny";
process: ChildProcess;
/** OS identity of the spawned `opencode serve`, so cleanup can prove that exact process is gone. */
processIdentity: { processId: number; processGroupId: number | null; processStartedAt: string | null } | null;
bridge: OpenCodeMcpBridge;
trace: ProviderTraceFileSink | null;
sensitiveValues: readonly string[];
@@ -624,6 +627,7 @@ class OpenCodeHarnessSession implements HarnessSession {
this.#emit(input.snapshot ? "session.resumed" : "session.started", {
driverSessionId: input.providerSessionId,
providerSessionId: input.providerSessionId,
...(input.runtime.processIdentity ?? {}),
context: {
protocolVersion: "http+sse/v1",
opencodeVersion: input.runtime.version,
@@ -2726,6 +2730,11 @@ async function startRuntime(input: {
providerChild.once("spawn", resolve);
providerChild.once("error", reject);
});
const processIdentity = child.pid ? {
processId: child.pid,
processGroupId: globalThis.process.platform === "win32" || !isolateProcessGroup ? null : child.pid,
processStartedAt: readLocalProcessStartedAt(child.pid),
} : null;
if (child.pid)
await input.options.onSpawn?.({
pid: child.pid,
@@ -2762,6 +2771,7 @@ async function startRuntime(input: {
version,
permissionMode: input.options.permissionMode ?? "allow",
process: child,
processIdentity,
bridge,
trace: input.trace,
sensitiveValues,
@@ -38,3 +38,48 @@ export function readLinuxProcessStartedAt(pid: number, options: LinuxProcessStar
}
return new Date(value).toISOString();
}
/** OS start time of a local process as an ISO string, or null when it cannot be read. */
export function readLocalProcessStartedAt(pid: number): string | null {
if (!Number.isInteger(pid) || pid <= 0) return null;
try {
if (process.platform === "linux") {
return readLinuxProcessStartedAt(pid);
}
if (
["darwin", "freebsd", "openbsd", "aix", "sunos"].includes(
process.platform,
)
) {
const raw = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
encoding: "utf8",
timeout: 1_500,
windowsHide: true,
}).trim();
const parsed = new Date(raw);
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
}
if (process.platform === "win32") {
const script = [
`$target = Get-Process -Id ${pid} -ErrorAction Stop`,
"$target.StartTime.ToUniversalTime().ToString('o')",
].join("; ");
for (const command of ["powershell.exe", "pwsh.exe"]) {
try {
const raw = execFileSync(
command,
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
{ encoding: "utf8", timeout: 1_500, windowsHide: true },
).trim();
const parsed = new Date(raw);
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
} catch {
// Try the other supported PowerShell host.
}
}
}
} catch {
// Process exit and restricted process metadata both produce no fingerprint.
}
return null;
}
@@ -25,7 +25,7 @@ import { waitForWarmAttachmentReadiness } from "./warm-attachment-readiness.js";
import { resolveCodexCommand } from "../drivers/codex/codex-command.js";
import { isCanonicalProviderEventType } from "../provider-events.js";
import { execFileSync } from "node:child_process";
import { readLinuxProcessStartedAt } from "./linux-process-start.js";
import { readLocalProcessStartedAt } from "./linux-process-start.js";
import { createHash, randomUUID } from "node:crypto";
import {
appendFileSync,
@@ -117,49 +117,6 @@ const RUNNER_BOOTSTRAP_TICKET_TTL_MS = 60_000;
const RUNNERD_MAX_OUTBOX_BYTES = 16 * 1024 * 1024;
const RUNNERD_P0_RESERVE_BYTES = 1024 * 1024;
function readLocalProcessStartedAt(pid: number): string | null {
if (!Number.isInteger(pid) || pid <= 0) return null;
try {
if (process.platform === "linux") {
return readLinuxProcessStartedAt(pid);
}
if (
["darwin", "freebsd", "openbsd", "aix", "sunos"].includes(
process.platform,
)
) {
const raw = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
encoding: "utf8",
timeout: 1_500,
windowsHide: true,
}).trim();
const parsed = new Date(raw);
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
}
if (process.platform === "win32") {
const script = [
`$target = Get-Process -Id ${pid} -ErrorAction Stop`,
"$target.StartTime.ToUniversalTime().ToString('o')",
].join("; ");
for (const command of ["powershell.exe", "pwsh.exe"]) {
try {
const raw = execFileSync(
command,
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
{ encoding: "utf8", timeout: 1_500, windowsHide: true },
).trim();
const parsed = new Date(raw);
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
} catch {
// Try the other supported PowerShell host.
}
}
}
} catch {
// Process exit and restricted process metadata both produce no fingerprint.
}
return null;
}
const CONTROL_PLANE_STATE_MAX_BYTES = 256 * 1024 * 1024;