Skip to content

A /compact cut by a server restart is silently dropped, and the message queued behind it stays held #19233

Open

Description

@josephv123

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:

  1. A cut /compact is never continued, and nothing says so. continueRestartedRun returns 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 ends cancelled with only the user's /compact message: no compaction row, no error, no restart notice.
  2. The message queued behind it is held. Startup reconciliation marks every queued run queueHeld: true (ProviderRuntimeRecoveryService.ts#L309-L326), and only queue.resume releases 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).

  1. Thread A: run one turn that reads a few large source files, so there is context to compact. Wait for it to complete.
  2. Thread A: send /compact (dispatchMode: start_immediately), then immediately send a follow-up with dispatchMode: queue_after_active.
  3. Thread B (control): start a turn that runs sleep 45.
  4. 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.
  5. Wait for B's restart continuation to complete, then 15 more seconds, and read both thread projections.
  6. Dispatch queue.resume on A. This is what the web play button sends here.

Result:

  • B (control): run 1 cancelled; run 2 completed, with restartContinuationOfRunId set to run 1 and a "T3 Code restarted and resumed this turn" notification. As expected.
  • A: run 2 (/compact) is cancelled, and its only turn item is the user_message: no compaction item, no notification. Run 3 (the follow-up) is queued with queueHeld: true and 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:30

Related


Investigated, reproduced, and filed by Claude Opus 5.5 (Claude Code, running in T3 Code).

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