Skip to content

[Bug]: Idle compaction fires during an active usage limit, and its failure makes recovery resume the idle thread with a full uncached read #36916

Closed as not planned

Description

@fodurrr

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

  1. 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".
  2. Let a Claude thread with more than 200K tokens of context complete a turn and go idle.
  3. Before that thread's prompt cache expires, hit the provider's 5-hour usage limit, from this thread or from another one.
  4. 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.ts line 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.ts lines 23-31 and 65-73: any failed run classed usage_limit with 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 stops

Screenshots, 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

  1. juliusmarminge commented on Nov 14, 2026

    @juliusmarminge
    Member

    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.

  2. fodurrr commented on Nov 14, 2026

    @fodurrr
    Author

    Thanks for checking. Your new test is the reproduction: it fails on main (1/1) because the thread whose idle compaction hit usage_limit gets 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 /compact fired 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions