Skip to content

Idle compaction never runs after a delegated-task completion: a late run.updated for the parent run replaces the timer #28099

Closed

Description

@Meorir

What happened

"Compact idle threads" is set to "Before the cache expires (beta)" globally and on every Claude instance, but a long Claude thread (500K+ tokens) never got an idle /compact. It sat idle three times for 2 to 12 hours, and each time the prompt cache expired uncompacted. Every one of those idle spells started right after a run that T3 started for a "Delegated task ... completed" notification (a Codex child launched with delegate_task, mode async). Other threads on the same server do get idle compactions.

Diagnosis

IdleCompactionService.start arms a timer for every run.updated event with status: "completed", keyed by thread in a FiberMap, so any later such event for the thread replaces the waiting timer.

When a delegated-task notification run N completes, T3 then finalizes the delegated completion on the earlier run N-1 (the run that called delegate_task; the subagent's completionDelivery becomes delivered). That emits run.updated for run N-1, which is already completed, about 0.1 to 1 s after run N completed. FiberMap.run replaces the timer for run N with compactWhenDue(thread, N-1). That fiber computes the window for the latest run (N), hits if (window?.runId !== runId) return; and exits. Nothing re-arms a timer for run N, so the thread never compacts.

The listener is unchanged on main (apps/server/src/orchestration-v2/IdleCompactionService.ts, same run.updated/completed filter and thread-keyed FiberMap after #23929).

Possible fixes: ignore run.updated for a run that is not the thread's latest (or older than the run whose timer is waiting); or react only to the transition into completed; or, when window.runId is newer than runId, keep or re-arm the timer for window.runId instead of returning.

Steps to reproduce

  1. Settings: Compact idle threads = Before the cache expires (beta), on a Claude instance (subscription, 1 h cache).
  2. In a Claude thread above the compaction minimum (well over 100K tokens), have the agent call delegate_task (async, any provider, e.g. Codex) and end its turn.
  3. Wait for the child to finish. T3 posts "Delegated task ... completed" and the parent runs one more turn (run N) and ends it.
  4. Leave the thread idle for over an hour.

Expected: /compact about 2 minutes before the cache of run N expires.
Actual: no compaction. In orchestration_events, the last two run.updated events of the thread are run N completed, then run N-1 completed with delegatedCompletion set, a fraction of a second later.

Version

T3 Code Desktop 0.1.5 with its embedded local server 0.1.5 (commit 6d450a4). Same listener code on main as of 2026-10-31.

Environment

Windows 11 x64 (10.0.26200), desktop app with the local server; three Claude provider instances (claudeAgent driver, subscription accounts) with idleCompaction: beforeCacheExpires; children run on Codex through delegate_task. The server ran without restart for the whole period (started 2026-10-30 08:29Z), so this is not #27915.

Evidence

Events of the affected thread (UTC), from statev2.sqlite orchestration_events:

07:08:21.239 run.updated run 164 completed   (notification run N)
07:08:21.980 subagent.updated  delegated task (completionDelivery: delivered)
07:08:21.980 run.updated run 163 completed   delegatedCompletion {disposition: open, nextGeneration: 2}
(no further events until the user returned at 10:32; no server:idle-compact receipt)

Same pattern before the other two idle spells: run 151 completed 15:24:52.224 then run 150 run.updated completed 15:24:53.214; run 153 completed 04:02:13.147 then run 152 run.updated completed 04:02:13.295.

Across all Claude threads on this server since its start, idle spells over an hour after a run triggered by a server-created message (delegated-task notifications, scheduled tasks): 0 compacted, 21 not. After runs triggered by MCP messages from other threads: 21 compacted, 22 not; after user messages: 12 compacted, 8 not. Only the delegated-task case was traced to the event level; the other misses may have the same or a different cause.

For the run that preceded the last idle spell, every condition in idleCompactionWindow() holds in the projections (not settled, archived or snoozed; message found by run id; no pending requests, merge-backs or background work; same provider thread and instance; 570K used tokens; promptCacheExpiresAt 1 h after completion).

Related issues

#27915 (idle compaction lost on server restart: different cause, same symptom). #21682 introduced the service, #23037 the per-instance setting, #23929 the cheaper eligibility check.

Fix applied or workaround

None applied. Workaround for users: send a message or a manual /compact after the last delegated-task notification before leaving the thread, or use delegate_task with mode: "wait" so the last run is not a notification run.

Filed by

Prepared with Claude Opus 5.5 via Claude Code in T3 Code (triage session); submitted by the owner.

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