fix(restore): invalidate stale native identity snapshots

This commit is contained in:
v-openrig-build committed 2026-09-02 17:47:39 +00:00
1 parent aa5e187982
commit 92a2b17dc9
2 files changed
+50 -7

No files matched your search

@@ -11,10 +11,10 @@ export interface CurrentStateRehydrateEligibility {
* A crash snapshot remains trustworthy only while it names every occupant that
* durable current state still identifies as active. Boot reconciliation turns
* rows detached after abrupt substrate loss, so detached rows must participate:
* a different session id is positive evidence that the snapshot belongs to an
* older occupant. Ambiguous current state is also not permission to revive a
* historical occupant; current-state capture preserves the ambiguity and
* restore refuses it explicitly.
* a different session id or native resume identity is positive evidence that
* the snapshot belongs to an older occupant. Ambiguous current state is also
* not permission to revive a historical occupant; current-state capture
* preserves the ambiguity and restore refuses it explicitly.
*/
export function snapshotMatchesCurrentOccupants(
db: Database.Database,
@@ -23,11 +23,18 @@ export function snapshotMatchesCurrentOccupants(
): boolean {
if (rig.nodes.length === 0) return true;
const rows = db.prepare(
`SELECT s.id, s.node_id AS nodeId, s.status
`SELECT s.id, s.node_id AS nodeId, s.status,
s.resume_type AS resumeType, s.resume_token AS resumeToken
FROM sessions s
JOIN nodes n ON n.id = s.node_id
WHERE n.rig_id = ? AND s.status NOT IN ('superseded', 'exited')`
).all(rig.rig.id) as Array<{ id: string; nodeId: string; status: string }>;
).all(rig.rig.id) as Array<{
id: string;
nodeId: string;
status: string;
resumeType: string | null;
resumeToken: string | null;
}>;
for (const node of rig.nodes) {
const current = resolveActiveOccupantRow(rows, undefined, node.id);
@@ -38,7 +45,12 @@ export function snapshotMatchesCurrentOccupants(
snapshot.data.activeSessionIdByNode,
node.id,
);
if (captured.kind !== "resolved" || captured.session.id !== current.session.id) {
if (
captured.kind !== "resolved"
|| captured.session.id !== current.session.id
|| (captured.session.resumeType ?? null) !== current.session.resumeType
|| (captured.session.resumeToken ?? null) !== current.session.resumeToken
) {
return false;
}
}
+31
View File
@@ -264,6 +264,37 @@ describe("Up API route", () => {
}),
]);
});
it("plans from current state when the same occupant row has a newer native resume identity", async () => {
const rig = rigRepo.createRig("stale-native-identity-rig");
const node = rigRepo.addNode(rig.id, "dev.impl", { runtime: "claude-code" });
insertStartupContextRow(db, node.id);
const session = sessionRegistry.registerSession(node.id, "dev-impl@stale-native-identity-rig");
db.prepare("UPDATE sessions SET resume_type = ?, resume_token = ?, status = ? WHERE id = ?")
.run("claude_id", "11111111-1111-4111-8111-111111111111", "running", session.id);
snapshotCapture.captureSnapshot(rig.id, "auto-periodic");
db.prepare("UPDATE sessions SET resume_type = ?, resume_token = ?, status = ? WHERE id = ?")
.run("claude_id", "22222222-2222-4222-8222-222222222222", "detached", session.id);
const res = await app.request("/api/up", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sourceRef: "stale-native-identity-rig", plan: true }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.snapshot).toBeNull();
expect(body.wouldCaptureCurrentState).toBe(true);
expect(body.nodes).toEqual([
expect.objectContaining({
logicalId: "dev.impl",
intendedAction: "resume-original",
}),
]);
});
});
// OPR.0.3.4.9 — Option Y: auto-periodic co-equal with auto-pre-down in restore selection.