fix: keep prior saves visible after interrupted sandbox runs
Report incomplete terminal saves without hiding the latest successful checkpoint, including same-sandbox continuation and replacement failures. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
ffe3a32b2d
commit
b8d003baff
|
|
@ -228,7 +228,9 @@ Native continuation validates the full control-plane journal with the same
|
|||
2 MiB without invalidating its identity. Oversized, malformed, foreign-session,
|
||||
and unverifiable state still fail closed and remain recoverable in quarantine.
|
||||
The cached-file routes initialize storage once per router after authorization;
|
||||
every request still checks current owner access.
|
||||
every request still checks current owner access. If a run ends while its save is
|
||||
starting or in progress, the inspector reports the interruption and keeps the
|
||||
previous successful save time visible, including across sandbox replacement.
|
||||
|
||||
Passing acceptance does not authorize a merge or mainline release. Both require
|
||||
the user's explicit sign-off.
|
||||
|
|
|
|||
|
|
@ -40,3 +40,26 @@ it("initializes storage only after access succeeds and reuses it while checking
|
|||
expect(mocks.access).toHaveBeenCalledTimes(4);
|
||||
expect(mocks.list).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it.each(["same-sandbox", "replacement-sandbox"])("keeps the last successful save visible after an interrupted run on %s", async (sandboxKey) => {
|
||||
const oldSave = {
|
||||
folderRun: { runId: "saved-run", manifest: { agentId: "agent", sandboxKey: "same-sandbox" },
|
||||
state: "saved", lastSavedAt: new Date("2026-09-08T12:00:00Z"), error: null, refreshRequested: false },
|
||||
status: "succeeded",
|
||||
};
|
||||
const interrupted = {
|
||||
folderRun: { runId: "interrupted-run", manifest: { agentId: "agent", sandboxKey },
|
||||
state: "starting", lastSavedAt: null, error: null, refreshRequested: false },
|
||||
status: "failed",
|
||||
};
|
||||
const query = { from: () => query, innerJoin: () => query, where: () => query,
|
||||
orderBy: () => query, limit: async () => [interrupted, oldSave] };
|
||||
const app = express();
|
||||
app.use("/api", workFolderRoutes({ select: () => query } as unknown as Db));
|
||||
const response = await request(app).get("/api/companies/11111111-1111-4111-8111-111111111111/work-folders/task/22222222-2222-4222-8222-222222222222/sync").expect(200);
|
||||
expect(response.body).toEqual([
|
||||
expect.objectContaining({ runId: "interrupted-run", state: "failed", active: false,
|
||||
error: "Run ended before its final file save completed.", lastSavedAt: null }),
|
||||
expect.objectContaining({ runId: "saved-run", state: "saved", lastSavedAt: "2026-09-08T12:00:00.000Z" }),
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -86,19 +86,31 @@ export function workFolderRoutes(db: Db, provider?: StorageProvider) {
|
|||
.where(and(eq(workFolderRuns.companyId, owner.companyId),
|
||||
sql`${workFolderRuns.manifest}->'folders'->>${owner.scope} = ${folder.id}`))
|
||||
.orderBy(desc(workFolderRuns.updatedAt)).limit(100);
|
||||
const isActive = (status: string) => status === "running" || status === "queued";
|
||||
const latestCompletedSave = rows.filter(({ folderRun, status }) =>
|
||||
!isActive(status) && folderRun.state === "saved" && folderRun.lastSavedAt !== null)
|
||||
.sort((a, b) => b.folderRun.lastSavedAt!.getTime() - a.folderRun.lastSavedAt!.getTime())[0];
|
||||
const projectStatus = ({ folderRun: row, status }: typeof rows[number]) => {
|
||||
const active = isActive(status);
|
||||
const interrupted = !active && (row.state === "starting" || row.state === "saving");
|
||||
return { runId: row.runId, agentId: row.manifest.agentId, state: interrupted ? "failed" : row.state,
|
||||
lastSavedAt: row.lastSavedAt, error: interrupted ? row.error ?? "Run ended before its final file save completed." : row.error,
|
||||
refreshRequested: row.refreshRequested, active };
|
||||
};
|
||||
const leases = new Set<string>();
|
||||
let includedCompletedSave = false;
|
||||
res.json(rows.flatMap(({ folderRun: row, status }) => {
|
||||
const statuses = rows.flatMap((entry) => {
|
||||
const row = entry.folderRun;
|
||||
if (leases.has(row.manifest.sandboxKey)) return [];
|
||||
leases.add(row.manifest.sandboxKey);
|
||||
const active = status === "running" || status === "queued";
|
||||
if (!active && row.state !== "failed") {
|
||||
if (includedCompletedSave) return [];
|
||||
includedCompletedSave = true;
|
||||
}
|
||||
return [{ runId: row.runId, agentId: row.manifest.agentId, state: row.state, lastSavedAt: row.lastSavedAt, error: row.error,
|
||||
refreshRequested: row.refreshRequested, active }];
|
||||
}));
|
||||
if (!isActive(entry.status) && row.state === "saved" && row.runId !== latestCompletedSave?.folderRun.runId) return [];
|
||||
return [projectStatus(entry)];
|
||||
});
|
||||
// A failed replacement or later run must not erase the last successful
|
||||
// checkpoint, including when both runs share the same physical sandbox.
|
||||
if (latestCompletedSave && !statuses.some((status) => status.runId === latestCompletedSave.folderRun.runId)) {
|
||||
statuses.push(projectStatus(latestCompletedSave));
|
||||
}
|
||||
res.json(statuses);
|
||||
});
|
||||
router.post(`${base}/refresh`, async (req, res) => {
|
||||
const owner = ownerSchema.parse(req.params);
|
||||
|
|
|
|||
Loading…
Reference in New Issue