From 2e12baa01b61142d9647c396c60c619023121865 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:00:42 +1100 Subject: [PATCH] fix(server): waking or pinning a usage-limit-snoozed thread no longer re-snoozes it on Resume at reset Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/Orchestrator.ts | 2 ++ .../src/orchestration-v2/runtimeLayer.test.ts | 35 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 75df815127f39..c9e17d05d92d2 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -3514,6 +3514,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio snoozedUntil: null, snoozedAt: null, lastSnoozeWakeAt: alreadyAwake ? thread.lastSnoozeWakeAt : now, + limitRecovery: thread.limitRecovery ? { ...thread.limitRecovery, snooze: false } : null, updatedAt: alreadyAwake ? thread.updatedAt : now, }; } @@ -3540,6 +3541,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio snoozedUntil: null, snoozedAt: null, lastSnoozeWakeAt: thread.snoozedUntil == null ? thread.lastSnoozeWakeAt : now, + limitRecovery: thread.limitRecovery ? { ...thread.limitRecovery, snooze: false } : null, updatedAt: alreadyPinned && !promotes ? thread.updatedAt : now, }; } diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index 2bb9f5ed63165..b2918040695a2 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -7876,6 +7876,8 @@ it.layer(layerTest)("usage-limit recovery", (it) => { "independent-patches", "expired-snooze", "wake", + "wake-then-resume", + "pin-then-resume", "stop", "stop-before-arm", "stop-before-arm-mismatched-recovery", @@ -8025,8 +8027,10 @@ it.layer(layerTest)("usage-limit recovery", (it) => { assert.isNull(current.thread.snoozedUntil); return; } - const snooze = scenario === "wake" || scenario === "cancel-resume-keep-snooze"; - const autoResume = scenario !== "wake"; + const wakesBeforeResume = scenario === "wake-then-resume" || scenario === "pin-then-resume"; + const snooze = + scenario === "wake" || scenario === "cancel-resume-keep-snooze" || wakesBeforeResume; + const autoResume = scenario !== "wake" && !wakesBeforeResume; const arm = limitRecoveryCommand(shell, autoResume, DateTime.toEpochMillis(now), snooze); assert(arm?.type === "thread.metadata.update" && arm.limitRecovery != null); const automaticArm = { @@ -8199,6 +8203,33 @@ it.layer(layerTest)("usage-limit recovery", (it) => { }); assert.isNull((yield* orchestrator.getThreadProjection(threadId)).thread.snoozedUntil); } + if (wakesBeforeResume) { + yield* orchestrator.dispatch( + scenario === "pin-then-resume" + ? { + type: "thread.pin", + commandId: CommandId.make(`recovery:pin:${scenario}`), + threadId, + } + : { + type: "thread.unsnooze", + commandId: CommandId.make(`recovery:sidebar-wake:${scenario}`), + threadId, + reason: "user", + }, + ); + assert.isNull((yield* orchestrator.getThreadProjection(threadId)).thread.snoozedUntil); + // "Resume at reset" omits snooze, so it inherits the stored flag. + yield* orchestrator.dispatch({ + type: "thread.metadata.update", + commandId: CommandId.make(`recovery:resume-after-wake:${scenario}`), + threadId, + limitRecovery: { runId: run.id, resetAt, autoResume: true }, + }); + const current = yield* orchestrator.getThreadProjection(threadId); + assert.isTrue(current.thread.limitRecovery!.autoResume); + assert.isNull(current.thread.snoozedUntil); + } assert.isNull(limitRecoveryCommand(armedShell, true, DateTime.toEpochMillis(now))); yield* orchestrator.dispatch({ type: "message.dispatch",