Skip to content

Idle compaction is skipped when the server restarts, including for a desktop update, during the wait #27915

Open

Description

@shiptomorrow

What happened

"Before the cache expires (beta)" is turned on, but a Claude thread with a 316K-token context never compacted during its idle hour. The user sent their next message 35 minutes after the prompt cache had expired. The composer's compact-before-send fallback then ran /compact first (marked idleCompaction: "onReturn"), so the compaction re-read the whole context uncached.

Diagnosis

IdleCompactionService only sets a compaction timer when it sees a run.updated event with status: "completed" while the server is running. The timer lives in a FiberMap in memory. If the server restarts at any point between a run finishing and its compaction window, the timer is lost. On startup the service doesn't scan idle threads whose cache is still warm, so it never sets the timer again. The code already says so (apps/server/src/orchestration-v2/IdleCompactionService.ts):

Timers live in memory, so a server restart skips compactions that were still waiting.

That comment and the timer logic are the same on main, which also has #23929's cheaper eligibility check.

Installing a desktop update is a common trigger. The update installs while threads sit idle, which is exactly when their compactions are waiting. Here the user clicked Install 22 minutes into a 58-minute wait, and the embedded server stopped right away.

Every condition in idleCompactionWindow() passes in the current projections:

  • The global setting is idleCompaction: beforeCacheExpires, with no per-instance override.
  • The thread is not archived, settled, snoozed, or a subagent.
  • Runtime requests are all resolved and context transfers are all consumed.
  • The latest run (run 32) completed and was not a /compact.
  • The active provider thread is run 32's, on the claudeAgent driver and instance.
  • Run 32 reported usedTokens = 316,180 (above the 200K minimum) and a 1-hour promptCacheTtlMs, with the cache expiring at 23:51:32Z.

The thread's event log has been compacted, so its state at 22:51:32Z can't be replayed directly. Pending background work is still ruled out, for two reasons:

  • Run 32 was itself the notification that the thread's last background command had finished.
  • No turn item of the thread was updated between 22:51:32Z and 00:26:17Z.

Suggested fix: when the service starts, find idle threads that still have a warm cache and call compactWhenDue for each one's latest completed run. idleCompactionWindow() already returns null once the cache has expired. Points to watch:

Steps to reproduce

  1. Set idle compaction to "Before the cache expires (beta)" for a Claude instance (subscription auth, so the cache TTL is 1 hour).
  2. In a thread, finish a turn with more than 200K tokens of context.
  3. Before the compaction window opens (~58 minutes after the turn), restart the server. Installing a desktop update through the in-app updater, or quitting and reopening the desktop app, both do it.
  4. Leave the thread idle past the cache expiry, then send a message. No server /compact turn appears during the idle hour. Instead, the composer's compact-before-send runs after the cache is already cold.

Version

The thread finished its last turn on the desktop build installed before 0.1.5 (exact version unknown, because those traces have rotated out). The update to 0.1.5 caused the restart. The timer logic is unchanged on main.

Environment

T3 Code Desktop (Beta) 0.1.5 on macOS (darwin arm64, Darwin 27.0.0), Node v24.22.1 (Electron-bundled), Claude Code 2.1.314, model claude-opus-5-5

Evidence

# Times are UTC. Run ordinals throughout.

# orchestration_v2_projection_provider_turns, run 32's turn
run 32  completed 22:51:32Z  usedTokens 316180  promptCacheTtlMs 3600000  promptCacheExpiresAt 23:51:32Z
# -> compaction window would start 23:49:32Z (LEAD_MS = 2 min)

# desktop.trace.ndjson
22:41:18      desktop.updates.applyUpdateAvailable  version 0.1.5
23:13:40.611  desktop.updates.install               # user clicked Install
23:13:40.617  desktop.backendInstance.stop          # embedded server stopped (3.6 s)
23:13:44.214  ElectronUpdater.quitAndInstall
23:13:50.778  desktop.backendInstance.start         # new process
23:13:57.108  desktop.backendInstance.onReady       # server-runtime.json: serverVersion 0.1.5

# DB: no server-created compaction message (idle-compact:… / idleCompaction "beforeCacheExpires") exists for the thread

# Thread messages
run 33  00:26:17.755Z  "/compact"  createdBy user, idleCompaction "onReturn"   # composer fallback, cache already cold
run 34  00:26:17.776Z  the user's actual message, queued behind it

Related issues

Fix applied or workaround

Nothing was changed on the machine. Workaround: after a restart or update, send /compact to any thread that went idle within the last hour and has a large context.

Filed by

Claude Code (claude-opus-5-5) via T3 Code triage, reviewed by Claude Fable 5.1

Activity

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