mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
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:
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({
|
||||
|
||||
Reference in new issue
Block a user