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:
DottaandPaperclip committed 2026-09-08 16:41:32 -05:00
1 parent ffe3a32b2d
commit b8d003baff
3 files changed
+48 -11

No files matched your search

@@ -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" }),
]);
});
+22 -10
View File
@@ -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);