mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
fix(runtime): validate provider process identity
Persist the operating-system start timestamp for the detached Codex provider and require an exact identity match before signaling its PID or process group. Clear stale ownership metadata without killing a replacement process, with focused PID-reuse coverage. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
1 parent
bbc0413956
commit
bfed0ca90d
3 files changed
+47
-5
No files matched your search
@@ -108,6 +108,7 @@ import {
|
||||
} from "../services/heartbeat.ts";
|
||||
import {
|
||||
readHotRestartIntent,
|
||||
readProcessStartedAt,
|
||||
resolveLegacyHotRestartIntentPath,
|
||||
resolveHotRestartReportPath,
|
||||
writeHotRestartIntent,
|
||||
@@ -490,6 +491,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
|
||||
processGroupId?: number | null;
|
||||
providerProcessPid?: number | null;
|
||||
providerProcessGroupId?: number | null;
|
||||
providerProcessStartedAt?: Date | null;
|
||||
processLossRetryCount?: number;
|
||||
includeIssue?: boolean;
|
||||
runErrorCode?: string | null;
|
||||
@@ -552,7 +554,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
|
||||
processGroupId: input?.processGroupId ?? null,
|
||||
providerProcessPid: input?.providerProcessPid ?? null,
|
||||
providerProcessGroupId: input?.providerProcessGroupId ?? null,
|
||||
providerProcessStartedAt: input?.providerProcessPid ? now : null,
|
||||
providerProcessStartedAt: input?.providerProcessStartedAt
|
||||
?? (input?.providerProcessPid ? now : null),
|
||||
processLossRetryCount: input?.processLossRetryCount ?? 0,
|
||||
errorCode: input?.runErrorCode ?? null,
|
||||
error: input?.runError ?? null,
|
||||
@@ -1250,6 +1253,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
|
||||
const { runId } = await seedRunFixture({
|
||||
processPid: 999_999_999,
|
||||
providerProcessPid: provider.pid ?? null,
|
||||
providerProcessStartedAt: new Date(await readProcessStartedAt(provider.pid ?? 0) ?? 0),
|
||||
includeIssue: false,
|
||||
});
|
||||
const heartbeat = heartbeatService(db);
|
||||
@@ -1267,6 +1271,32 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not signal a reused provider pid with a different start time", async () => {
|
||||
const replacement = spawnAliveProcess();
|
||||
childProcesses.add(replacement);
|
||||
expect(replacement.pid).toBeTypeOf("number");
|
||||
|
||||
const { runId } = await seedRunFixture({
|
||||
processPid: 999_999_999,
|
||||
providerProcessPid: replacement.pid ?? null,
|
||||
providerProcessStartedAt: new Date("2020-01-01T00:00:00.000Z"),
|
||||
includeIssue: false,
|
||||
});
|
||||
const heartbeat = heartbeatService(db);
|
||||
|
||||
const result = await heartbeat.reapOrphanedRuns();
|
||||
|
||||
expect(result.reaped).toBe(1);
|
||||
expect(isPidAlive(replacement.pid)).toBe(true);
|
||||
const run = await heartbeat.getRun(runId);
|
||||
expect(run).toMatchObject({
|
||||
status: "failed",
|
||||
providerProcessPid: null,
|
||||
providerProcessGroupId: null,
|
||||
providerProcessStartedAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips generic timer wakes without invoking an adapter when no assigned work is actionable", async () => {
|
||||
const { companyId, agentId } = await seedIdleTimerAgentFixture();
|
||||
const heartbeat = heartbeatService(db);
|
||||
|
||||
@@ -313,6 +313,7 @@ import {
|
||||
import {
|
||||
findMissingHotRestartSnapshotRunIds,
|
||||
readHotRestartIntent,
|
||||
readProcessStartedAt,
|
||||
removeHotRestartIntent,
|
||||
shouldHonorHotRestartIntentForProcess,
|
||||
writeHotRestartReport,
|
||||
@@ -9941,19 +9942,25 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
async function terminateRunProviderProcess(
|
||||
run: Pick<
|
||||
typeof heartbeatRuns.$inferSelect,
|
||||
"id" | "providerProcessPid" | "providerProcessGroupId"
|
||||
"id" | "providerProcessPid" | "providerProcessGroupId" | "providerProcessStartedAt"
|
||||
>,
|
||||
): Promise<boolean> {
|
||||
const pid = run.providerProcessPid;
|
||||
const processGroupId = run.providerProcessGroupId;
|
||||
const alive = isProcessAlive(pid) || isProcessGroupAlive(processGroupId);
|
||||
if (alive) {
|
||||
let ownsLiveProcess = false;
|
||||
if (alive && pid && run.providerProcessStartedAt) {
|
||||
const observedStartedAt = await readProcessStartedAt(pid).catch(() => null);
|
||||
ownsLiveProcess = observedStartedAt !== null
|
||||
&& Date.parse(observedStartedAt) === run.providerProcessStartedAt.getTime();
|
||||
}
|
||||
if (ownsLiveProcess) {
|
||||
await terminateHeartbeatRunProcess({ pid, processGroupId });
|
||||
}
|
||||
if (pid || processGroupId) {
|
||||
await clearRunProviderProcessMetadata(run.id);
|
||||
}
|
||||
return alive;
|
||||
return ownsLiveProcess;
|
||||
}
|
||||
|
||||
async function clearDetachedRunWarning(runId: string) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { AdapterExecutionResult } from "@paperclipai/adapter-utils";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
|
||||
import { resolvePaperclipInstanceRoot } from "../../home-paths.js";
|
||||
import { readProcessStartedAt } from "../hot-restart.js";
|
||||
import { runnerPrpCoordinator } from "./runner-prp-coordinator.js";
|
||||
|
||||
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -286,10 +287,14 @@ export async function executeNativeCodexRunner(input: {
|
||||
stderr = `${stderr}${chunk}`.slice(-16_384);
|
||||
});
|
||||
if (!sharedCodexServer.pid) throw new Error("codex_app_server_process_not_started");
|
||||
const providerStartedAt = await readProcessStartedAt(sharedCodexServer.pid);
|
||||
if (!providerStartedAt) {
|
||||
throw new Error("codex_app_server_process_identity_unavailable");
|
||||
}
|
||||
await input.onProviderSpawn?.({
|
||||
pid: sharedCodexServer.pid,
|
||||
processGroupId: process.platform === "win32" ? null : sharedCodexServer.pid,
|
||||
startedAt: new Date().toISOString(),
|
||||
startedAt: providerStartedAt,
|
||||
});
|
||||
const startupDeadline = Date.now() + 10_000;
|
||||
while (
|
||||
|
||||
Reference in new issue
Block a user