fix: resume saved user messages after execution recovery (#13270)

Preserve verified native process-stop evidence and retry saved user messages through normal continuation admission after recovery cleanup. Show the current wait reason and serialize delivery so a saved message starts one fresh turn.

Validated with 410 focused tests, typecheck, build, token gates, all PR CI checks, and Greptile 5/5.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-09-11 17:12:55 -05:00
1 parent 250deab910
commit 51b0e01ead
17 files changed
+471 -95

No files matched your search

@@ -1,3 +1,4 @@
import { recordNativeLocalProcessStop } from "../native-local-process-stop.js";
import { randomUUID } from "node:crypto";
import { and, desc, eq, inArray, isNull, lte, or, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
@@ -909,6 +910,10 @@ export async function claimNativeRestartRecoveries(input: {
} as const;
}
if (claimKind !== "reattach_existing_runner") {
await recordNativeLocalProcessStop(tx as unknown as Db, row.run);
}
await tx
.update(heartbeatRuns)
.set({
@@ -1,3 +1,4 @@
import { hasNativeLocalProcessStop } from "../native-local-process-stop.js";
import { randomUUID } from "node:crypto";
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
@@ -580,6 +581,10 @@ describeEmbeddedPostgres("native runner restart recovery with real processes", (
if (!claim || claim.kind !== "resume_dead_runner") {
throw new Error("Expected dead-runner recovery claim");
}
expect(await hasNativeLocalProcessStop(fixture.db, companyId, fixture.runId)).toBe(true);
const [stoppedRun] = await fixture.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
expect(stoppedRun.processPid).toBeNull();
expect(stoppedRun.processGroupId).toBeNull();
restored = createRunnerdCodexTransport({
...options,
@@ -4119,7 +4119,7 @@ function leaseDb(
returning: () => Promise<Array<{ runId: string }>>;
};
result.returning = () =>
Promise.resolve([{ runId: coordinator.runId }]);
Promise.resolve([{ runId: coordinator.runId, nextEventSeq: 2 }]);
return result;
},
};
@@ -4162,12 +4162,20 @@ function leaseDb(
return query;
},
});
const insert = (table: unknown) => ({
values: (values: Record<string, unknown>) => {
updates.push({ table, values });
return { returning: async () => [values] };
},
});
const tx = {
insert,
execute: async () => [],
select,
update,
};
return {
insert,
select,
transaction: async (operation: (transaction: Db) => Promise<unknown>) =>
operation(tx as unknown as Db),
@@ -6463,13 +6471,16 @@ describe("native process ownership", () => {
highestContiguousSourceSeq: 1,
};
});
state.createBackend.mockImplementationOnce((_input, options) => ({
kind: "test",
onSpawn: options.onSpawn,
}));
const updates: Array<{ table: unknown; values: Record<string, unknown> }> = [];
state.createBackend.mockImplementationOnce((_input, options) => {
expect(updates).toContainEqual({ table: heartbeatRunEvents, values: expect.objectContaining({
eventType: "native.process_start_requested", runId: execution.binding.runId,
}) });
return { kind: "test", onSpawn: options.onSpawn };
});
await executePaperclipNativeSession({
db: leaseDb(),
db: leaseDb(execution, {}, {}, updates),
execution,
runnerInstanceId: "runner",
onSpawn,
@@ -1,3 +1,4 @@
import { PROCESS_START_REQUESTED } from "../native-local-process-stop.js";
import { remoteLeaseCleanupScope } from "../remote-execution-termination.js";
import { resolveConnectorAssignments, isConnectorSkill } from "../connector-runtime.js";
import {
@@ -7598,6 +7599,17 @@ async function executePaperclipNativeSessionWithinScope(
input.db,
input.execution.binding,
);
// Invalidate prior stop evidence before a backend can spawn. A crash between
// spawn and the PID callback must not make an old receipt authorize a turn.
await appendHeartbeatRunEvent(input.db, {
companyId: input.execution.binding.companyId,
runId: input.execution.binding.runId,
agentId: input.execution.binding.agentId,
eventType: PROCESS_START_REQUESTED,
stream: "system",
level: "info",
message: "Native execution requested; prior local stop evidence no longer applies.",
});
const runnerdBackend =
input.useRunnerd && input.backend === undefined
? await createRunnerdBackend({