Repository navigation
A /compact cut by a server restart is silently dropped, and the message queued behind it stays held #19233
Description
What happened
A Claude thread was running /compact, and I had queued a follow-up message to send after it. Then I ran Update everything from the desktop app, which restarted the server the thread runs on. After the restart, my other interrupted thread was continued automatically ("T3 Code restarted and resumed this turn"). This one wasn't. The compaction never happened, nothing said so, and I assumed it was still compacting. My follow-up sat in the queue. To get unstuck I removed the queued message, sent /compact again (which worked), and then re-sent the message.
Diagnosis
Two deliberate parts of restart recovery combine into a silent dead end for /compact:
- A cut
/compactis never continued, and nothing says so.continueRestartedRunreturns early for native maintenance commands (RestartContinuation.ts#L229, test "does not continue a cut /compact or /logout turn", from fix(server): restarts keep delegated tasks, queued threads, and stops intact #15323). That makes sense for "Continue where you left off.", but nothing takes its place. The run endscancelledwith only the user's/compactmessage: no compaction row, no error, no restart notice. - The message queued behind it is held. Startup reconciliation marks every queued run
queueHeld: true(ProviderRuntimeRecoveryService.ts#L309-L326), and onlyqueue.resumereleases a restart hold. For an ordinary cut turn, the continuation's notice tells you what happened. Here nothing does.
On web/desktop, the only sign of the hold is that the composer's send button becomes a play icon ("Resume thread"), and only while the composer is empty. Mobile at least says "Queue held after restart"; #14617 (open) would add a "Queue paused" header on web. Pressing that button also skips the compaction. The cut run is cancelled, not interrupted, so resumableRunId is null (ChatView.tsx#L2153) and the button dispatches queue.resume directly (ChatView.tsx#L8770). The held follow-up then runs on the uncompacted context. Either way, the order the user asked for (compact, then send) is lost without notice.
Possible directions, for maintainers to decide: when continuations are on, dispatch /compact again as the continuation of a cut /compact (compaction has no side effects to duplicate). Or at least post the usual restart notice saying compaction was stopped, so the user knows to compact before resuming the queue.
Steps to reproduce
Reproduced deterministically on main @ 11fd54f. I used a scripted WebSocket client modeled on apps/server/scripts/verify-background-live.ts: a fresh T3 home, the real Claude CLI, claude-sonnet-5-5, and Continue threads after restarts on (the default).
- Thread A: run one turn that reads a few large source files, so there is context to compact. Wait for it to complete.
- Thread A: send
/compact(dispatchMode: start_immediately), then immediately send a follow-up withdispatchMode: queue_after_active. - Thread B (control): start a turn that runs
sleep 45. - While A's compaction provider turn and B's turn are both
running, send SIGTERM to the server, as an update restart does. Start it again on the same database. - Wait for B's restart continuation to complete, then 15 more seconds, and read both thread projections.
- Dispatch
queue.resumeon A. This is what the web play button sends here.
Result:
- B (control): run 1
cancelled; run 2completed, withrestartContinuationOfRunIdset to run 1 and a "T3 Code restarted and resumed this turn" notification. As expected. - A: run 2 (
/compact) iscancelled, and its only turn item is theuser_message: no compaction item, no notification. Run 3 (the follow-up) isqueuedwithqueueHeld: trueand stays that way. - A after
queue.resume: run 3 runs and completes. The thread never gets a compaction item.
Version
Seen while updating the server from 0.0.46-nightly.20261013.3066 to 0.0.46-nightly.20261013.3080 with Update everything. Reproduced on main @ 11fd54f.
Environment
Server: Ubuntu 26.04, background service. Client: T3 Code (Nightly) desktop app 0.0.46-nightly.20261013.3080 on macOS 26, connected over T3 Connect. Provider: Claude (claude-opus-5-5, xhigh) in the original incident; claude-sonnet-5-5 in the repro.
Evidence
From the original incident's projections and server trace (UTC, IDs shortened):
02:36:58.190 run 3 "/compact" requested; started 02:36:58.226
02:36:58.306 run 4 (follow-up) queued behind it
02:37:00 update downloads the new runtime; old server stops (tunnel: connection refused 02:37:16)
02:37:19.251 another thread's cut turn -> cancelled
02:37:19.267 run 3 -> cancelled; no compaction item, no notice
02:37:25 new server recovery: the other thread gets message:restart-continuation:<run> and completes;
this thread gets nothing; run 4 -> queued, queueHeld: true
02:43:12 thread.settle rejected: "Thread <id> has a queued message. Send it or remove it from the queue before settling."
02:48:27 user removes run 4 (queued-run.cancel)
02:48:31 user sends /compact again -> compaction item completed 02:49:30
02:48:49 user re-sends the follow-up -> delivered after compaction, 02:49:30Related
- fix(server): restarts keep delegated tasks, queued threads, and stops intact #15323: added the
/compactskip and the restart queue hold - fix(server): cap automatic thread continuations and turn them on by default #18461: continuations on by default
- feat(web): resume a paused queue from the queue itself #14617 (open): web "Queue paused" header with Resume queue
- fix(server): a held queue no longer strands later queued messages #18792 (open): a held queue strands later queued messages (different trigger)
- Snooze fails with "has a queued run" when a held queue never drains after restart #15862 (closed): a held queue that never drains after restart blocks snooze
Investigated, reproduced, and filed by Claude Opus 5.5 (Claude Code, running in T3 Code).
Activity