mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix: cancel invalidated manual compaction
This commit is contained in:
1 parent
89fd15bf59
commit
d4236f5bd2
2 files changed
+58
No files matched your search
@@ -323,6 +323,10 @@ export class ClaudeCompactionEnforcer {
|
||||
// retired-generation stage → refuse. Injected resolver (atom-B's currentOccupantTenure by session).
|
||||
private readonly pendingStageGeneration = new Map<string, string | null>();
|
||||
private readonly pendingStageAuthorization = new Map<string, PendingStageAuthorization>();
|
||||
// Monotonic per-seat invalidation identity. Unlike the occupant state maps, this survives
|
||||
// invalidation so an already-awaiting manual trigger can detect a same-name handover even when
|
||||
// best-effort successor tenure minting failed and the durable generation still appears equal.
|
||||
private readonly occupantInvalidationEpoch = new Map<string, number>();
|
||||
private readonly resolveOccupantGeneration?: (sessionName: string) => string | null;
|
||||
private readonly decisionStore?: Pick<
|
||||
EnforcerDecisionStore,
|
||||
@@ -715,6 +719,7 @@ export class ClaudeCompactionEnforcer {
|
||||
resolveCurrentSessionName?: (sessionName: string) => string | null;
|
||||
} = {},
|
||||
): Promise<ManualCompactionOutcome> {
|
||||
const startingInvalidationEpoch = this.occupantInvalidationEpoch.get(input.sessionName) ?? 0;
|
||||
const startingGenerationUuid = this.resolveOccupantGeneration?.(input.sessionName) ?? null;
|
||||
const activeHold = this.findAndObserveHold(input.sessionName, startingGenerationUuid);
|
||||
if (activeHold) {
|
||||
@@ -834,6 +839,13 @@ export class ClaudeCompactionEnforcer {
|
||||
error: `Session '${input.sessionName}' changed occupant generation during manual compaction preparation.`,
|
||||
};
|
||||
}
|
||||
if ((this.occupantInvalidationEpoch.get(input.sessionName) ?? 0) !== startingInvalidationEpoch) {
|
||||
preCompactCheck.reason = "occupant_generation_changed";
|
||||
return {
|
||||
reason: preCompactCheck.reason,
|
||||
error: `Session '${input.sessionName}' was invalidated during manual compaction preparation.`,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
@@ -889,6 +901,10 @@ export class ClaudeCompactionEnforcer {
|
||||
* record. Occupant-scoped (no atom-B): the retiring occupant is gone, so a name match IS the ghost.
|
||||
*/
|
||||
invalidateOccupant(sessionName: string): void {
|
||||
this.occupantInvalidationEpoch.set(
|
||||
sessionName,
|
||||
(this.occupantInvalidationEpoch.get(sessionName) ?? 0) + 1,
|
||||
);
|
||||
this.lastAutoCompactAt.delete(sessionName);
|
||||
this.postCompactRestoreCooldownUntil.delete(sessionName);
|
||||
this.triggeredAboveThreshold.delete(sessionName);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
import { SessionRegistry } from "../src/domain/session-registry.js";
|
||||
import { ClaudeCompactionEnforcer } from "../src/domain/claude-compaction-enforcer.js";
|
||||
import { EnforcerDecisionStore } from "../src/domain/enforcer-decision-store.js";
|
||||
import { DefaultOccupantInvalidator } from "../src/domain/occupant-invalidator.js";
|
||||
import { compactionRoutes } from "../src/routes/compaction.js";
|
||||
|
||||
const IDENTITY = "orch-lead@rig";
|
||||
@@ -515,6 +516,47 @@ describe("W4 compaction-control routes", () => {
|
||||
expect(tx.delivered).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("the public manual trigger refuses a same-alias handover when successor tenure minting fails", async () => {
|
||||
const store = new EnforcerDecisionStore(db, {
|
||||
now: () => new Date("2026-08-08T18:00:00.000Z"),
|
||||
authorizeTtlMinutes: () => 15,
|
||||
});
|
||||
const startingGeneration = sessionRegistry.currentOccupantGenerationForSession(SEAT);
|
||||
let invalidator: DefaultOccupantInvalidator;
|
||||
const tx = makeBoundaryTransport(() => {
|
||||
const mint = vi.spyOn(sessionRegistry, "mintOccupantTenure").mockImplementationOnce(() => {
|
||||
throw new Error("forced successor tenure mint failure");
|
||||
});
|
||||
sessionRegistry.registerClaimedSession(nodeId, SEAT, "handover");
|
||||
mint.mockRestore();
|
||||
|
||||
expect(sessionRegistry.currentOccupantGenerationForSession(SEAT)).toBe(startingGeneration);
|
||||
expect(
|
||||
(db.prepare("SELECT session_name FROM sessions WHERE node_id = ? ORDER BY id DESC LIMIT 1")
|
||||
.get(nodeId) as { session_name: string }).session_name,
|
||||
).toBe(SEAT);
|
||||
invalidator.invalidateRetiringOccupant({
|
||||
retiringSessionName: SEAT,
|
||||
successorSessionName: SEAT,
|
||||
retiringGeneration: startingGeneration ?? undefined,
|
||||
});
|
||||
});
|
||||
installManualHarness(store, tx.transport);
|
||||
invalidator = new DefaultOccupantInvalidator({
|
||||
enforcer: manualDeps!.enforcer,
|
||||
contextUsage: { invalidateOccupantSidecar: vi.fn() },
|
||||
});
|
||||
|
||||
const response = await request("/api/compaction/trigger", { session: SEAT });
|
||||
expect(response.status).toBe(409);
|
||||
expect(await response.json()).toMatchObject({
|
||||
ok: false,
|
||||
reason: "occupant_generation_changed",
|
||||
});
|
||||
expect(tx.delivered).toHaveLength(1);
|
||||
expect(tx.delivered[0]).toContain("automatic compaction preparation");
|
||||
});
|
||||
|
||||
it("lists durable observation fields without requiring a write identity", async () => {
|
||||
const response = await request(`/api/compaction/control?session=${encodeURIComponent(SEAT)}`);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
Reference in new issue
Block a user