Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Use a Claude provider on a subscription (1-hour prompt cache) with auto-resume after usage limits turned on and "Compact idle threads" set to "Before the cache expires".
- Let a Claude thread with more than 200K tokens of context complete a turn and go idle.
- Before that thread's prompt cache expires, hit the provider's 5-hour usage limit, from this thread or from another one.
- Wait for idle compaction to fire on the idle thread, then wait for the limit reset.
Expected behavior
No compaction is sent to a thread while its provider reports an active usage limit. If a compaction does fail on a usage limit, usage-limit recovery does not send "Continue where you left off." to that idle thread. Or it retries the compaction instead of continuing the conversation.
Actual behavior
Idle compaction sends /compact to the idle thread about 2 minutes before its prompt cache expires, even though the usage limit is already hit. The compaction fails:
Error during compaction: You've hit your 5-hour limit · resets 7pm (Europe/London)
That failure marks the thread as failed with usage_limit, so usage-limit recovery arms it. At the reset, recovery sends "Continue where you left off." to a thread that had nothing left to do. The prompt cache expired long before, so the provider re-reads the whole conversation uncached. In our case the first request after the reset wrote 260,214 tokens to the cache and read none. The thread then found no pending work and stopped. This spends a large part of the new usage window on an idle thread. It happens to every idle Claude thread above 200K tokens whose compaction window falls inside the limit.
Code pointers, at commit 5a15495 (v0.1.10-nightly.20261114.4695):
apps/server/src/orchestration-v2/IdleCompaction.tsline 61 and lines 95-105: the compaction decision checks the run state and the prompt-cache expiry, but not the usage-limit state.apps/server/src/orchestration-v2/UsageLimitRecoveryWorker.tslines 23-31 and 65-73: any failed run classedusage_limitwith a future reset is armed and later resumed with a plain continuation, including a failed compaction run.
Related, not duplicates: #17184 (a compaction cancelled by the user sends a continuation, closed) and PR #27742 (an opt-in setting to compact before a usage limit is reached, which does not stop compaction from firing while the limit is active).
Suggested fix: skip idle compaction while the provider reports an active usage limit. Do not arm usage-limit recovery for a failed compaction run, or recover it by retrying the compaction instead of sending a continuation.
Impact
Major degradation or frequent failure
Version or commit
v0.1.10-nightly.20261113.4630 (observed). The same code is in v0.1.10-nightly.20261114.4695, commit 5a15495.
Environment
Linux desktop app (nightly), Claude Code provider on a Claude subscription, Claude Opus 5.5
Logs or stack traces
17:31:39 UTC idle compaction sends /compact to the idle thread
17:31:40 UTC Error during compaction: You've hit your 5-hour limit · resets 7pm (Europe/London)
19:00:07 UTC usage-limit recovery sends "Continue where you left off."
19:00:15 UTC first request: cache_creation (1h) 260,214 tokens, cache_read 0
19:00:21 UTC the thread reports nothing pending and stopsScreenshots, recordings, or supporting files
No response
Workaround
Turning off auto-resume after usage limits prevents it, but that is one global setting, so it also stops threads that were in the middle of real work. We avoid it by pacing usage so the 5-hour limit is not reached.
Activity
juliusmarminge commentedon Nov 14, 2026
I tried to reproduce this on the current build and could not.
Commit ea05ea5 (origin/main). An end-to-end run needs a first-party Claude subscription session that actually hits its 5-hour limit; no worker VM has first-party Claude OAuth, so the live flow could not be driven. Instead I reproduced the mechanism at the projection/recovery layer: a new test in apps/server/src/orchestration-v2/ProjectionStore.test.ts ('does not resume a thread whose idle compaction hit a usage limit') applies thread.created, a user message '/compact' with idleCompaction 'beforeCacheExpires' created by agent/server, a run for it, a failed error turn item with failure.class 'usage_limit' and resetAt one hour later, and run.updated status failed. Then it reads getLimitRecoveryCandidates({autoResume:true}) and calls limitRecoveryCommand. On main the thread is a candidate and the command is {type:'thread.metadata.update', commandId:'limit-arm:thread:idle-compact-limit:run:idle-compact-limit:3600000', limitRecovery:{autoResume:true,...}}, which the worker turns into the 'Continue where you left off.' dispatch at the reset. Run with: vp test run src/orchestration-v2/ProjectionStore.test.ts -t "idle compaction hit a usage limit" (fails 1/1). Code reading confirms the first half: idleCompactionWindow (IdleCompaction.ts) and IdleCompactionService.compactWhenDue check no usage-limit state, and the server keeps no per-instance active-limit state, only per-thread usageLimitResetAt after a failure.
Closing as not planned. If it still happens for you, reply with the exact steps and the version you are on and it will be looked at again.
fodurrr commentedon Nov 14, 2026
Thanks for checking. Your new test is the reproduction: it fails on main (1/1) because the thread whose idle compaction hit
usage_limitgets armed for "Continue where you left off." That is the bug.The live flow is the one in the report, seen on v0.1.10-nightly.20261113.4630 (Linux desktop, Claude subscription). The idle
/compactfired at 17:31:39 UTC, failed with the 5-hour limit at 17:31:40, and recovery resumed the idle thread at 19:00:07. The first request wrote 260,214 tokens to the cache and read 0, then the thread found nothing to do. The same code is in v0.1.10-nightly.20261114.4695.Could you reopen it?