mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix: preserve successor compaction state
This commit is contained in:
1 parent
d4236f5bd2
commit
df05193e4f
2 files changed
+92
-4
No files matched your search
@@ -788,7 +788,12 @@ export class ClaudeCompactionEnforcer {
|
||||
}),
|
||||
);
|
||||
if (!prep.ok) {
|
||||
return this.recordManualFailure(input.sessionName, prep.reason ?? "send_failed");
|
||||
return this.recordManualFailure(
|
||||
input.sessionName,
|
||||
prep.reason ?? "send_failed",
|
||||
undefined,
|
||||
startingInvalidationEpoch,
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 2 — WAIT for the prep turn to complete (seat idle), THEN send
|
||||
@@ -856,9 +861,15 @@ export class ClaudeCompactionEnforcer {
|
||||
input.sessionName,
|
||||
preCompactCheck.reason,
|
||||
preCompactCheck.decisionId,
|
||||
startingInvalidationEpoch,
|
||||
);
|
||||
}
|
||||
return this.recordManualFailure(input.sessionName, compact.reason ?? "send_failed");
|
||||
return this.recordManualFailure(
|
||||
input.sessionName,
|
||||
compact.reason ?? "send_failed",
|
||||
undefined,
|
||||
startingInvalidationEpoch,
|
||||
);
|
||||
}
|
||||
|
||||
// Seed the EXISTING post-compact back-half (turn_boundary → restore_prompt
|
||||
@@ -919,8 +930,18 @@ export class ClaudeCompactionEnforcer {
|
||||
this.manualCompactionState.set(sessionName, { stage, reason, updatedAt: Date.now(), operatorInitiated });
|
||||
}
|
||||
|
||||
private recordManualFailure(sessionName: string, reason: string, decisionId?: string): ManualCompactionOutcome {
|
||||
this.setManualStage(sessionName, "skipped-or-failed", reason);
|
||||
private recordManualFailure(
|
||||
sessionName: string,
|
||||
reason: string,
|
||||
decisionId?: string,
|
||||
expectedInvalidationEpoch?: number,
|
||||
): ManualCompactionOutcome {
|
||||
if (
|
||||
expectedInvalidationEpoch === undefined
|
||||
|| (this.occupantInvalidationEpoch.get(sessionName) ?? 0) === expectedInvalidationEpoch
|
||||
) {
|
||||
this.setManualStage(sessionName, "skipped-or-failed", reason);
|
||||
}
|
||||
return {
|
||||
triggered: false,
|
||||
stage: "skipped-or-failed",
|
||||
|
||||
@@ -990,6 +990,73 @@ describe("ClaudeCompactionEnforcer", () => {
|
||||
await first;
|
||||
});
|
||||
|
||||
it("a retiring invocation refusal does not clobber its same-name successor's preparing marker", async () => {
|
||||
const settings = makeSettingsStore(POLICY_ENABLED_AT_80);
|
||||
let reachRetireeBoundary!: () => void;
|
||||
let releaseRetiree!: () => void;
|
||||
let reachSuccessorBoundary!: () => void;
|
||||
let releaseSuccessor!: () => void;
|
||||
const retireeAtBoundary = new Promise<void>((resolve) => { reachRetireeBoundary = resolve; });
|
||||
const retireeReleased = new Promise<void>((resolve) => { releaseRetiree = resolve; });
|
||||
const successorAtBoundary = new Promise<void>((resolve) => { reachSuccessorBoundary = resolve; });
|
||||
const successorReleased = new Promise<void>((resolve) => { releaseSuccessor = resolve; });
|
||||
let compactAttempt = 0;
|
||||
const send = vi.fn(async (
|
||||
_sessionName: string,
|
||||
message: string,
|
||||
opts?: { beforeSend?: () => { reason: string; error?: string } | null },
|
||||
) => {
|
||||
if (!message.startsWith("/compact")) return { ok: true as const };
|
||||
compactAttempt += 1;
|
||||
if (compactAttempt === 1) {
|
||||
reachRetireeBoundary();
|
||||
await retireeReleased;
|
||||
} else if (compactAttempt === 2) {
|
||||
reachSuccessorBoundary();
|
||||
await successorReleased;
|
||||
} else {
|
||||
return { ok: false as const, reason: "unexpected_duplicate" };
|
||||
}
|
||||
const refusal = opts?.beforeSend?.();
|
||||
return refusal
|
||||
? { ok: false as const, sent: false, sessionName: SEAT, ...refusal }
|
||||
: { ok: true as const };
|
||||
});
|
||||
const enforcer = new ClaudeCompactionEnforcer(
|
||||
settings,
|
||||
{ send } as unknown as SessionTransport,
|
||||
{ openrigHome: HOME },
|
||||
);
|
||||
|
||||
const retiree = enforcer.triggerManualCompact({ sessionName: SEAT, runtime: "claude-code", usedPercentage: 20 });
|
||||
await retireeAtBoundary;
|
||||
enforcer.invalidateOccupant(SEAT);
|
||||
|
||||
const successor = enforcer.triggerManualCompact({ sessionName: SEAT, runtime: "claude-code", usedPercentage: 20 });
|
||||
await successorAtBoundary;
|
||||
expect(enforcer.getManualCompactionState(SEAT)?.stage).toBe("preparing");
|
||||
|
||||
releaseRetiree();
|
||||
const retireeOutcome = await retiree;
|
||||
const stateAfterRetiree = enforcer.getManualCompactionState(SEAT)?.stage;
|
||||
const sendsBeforeThird = send.mock.calls.length;
|
||||
const third = await enforcer.triggerManualCompact({ sessionName: SEAT, runtime: "claude-code", usedPercentage: 20 });
|
||||
const sendsAfterThird = send.mock.calls.length;
|
||||
|
||||
releaseSuccessor();
|
||||
const successorOutcome = await successor;
|
||||
|
||||
expect(retireeOutcome).toEqual({
|
||||
triggered: false,
|
||||
stage: "skipped-or-failed",
|
||||
reason: "occupant_generation_changed",
|
||||
});
|
||||
expect(stateAfterRetiree).toBe("preparing");
|
||||
expect(third).toEqual({ triggered: false, stage: "skipped-or-failed", reason: "already_in_progress" });
|
||||
expect(sendsAfterThird).toBe(sendsBeforeThird);
|
||||
expect(successorOutcome).toEqual({ triggered: true, stage: "compact-sent" });
|
||||
});
|
||||
|
||||
it("sequential call after compact-sent but before the back-half drains: no second prep, no second /compact", async () => {
|
||||
const settings = makeSettingsStore(POLICY_ENABLED_AT_80);
|
||||
const { transport, send } = makeSessionTransport();
|
||||
|
||||
Reference in new issue
Block a user