[Bug]: Changing runtime mode mid-turn hangs ProviderCommandReactor; all new sessions stay Connecting or show no status
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Related but not the same:
- #4944 — Claude instance switch hangs in
stopSessionwith no deadline (same missing-timeout family, one thread / one provider). - #5781 — Grok/Cursor/Codex notification consumers die when
startSessionreturns. - #5164 — OpenCode collapses auto-accept / auto into blanket ask.
- #4518 — persisted
autoruntime mode crash-loops startup (decoder), different failure.
Area
apps/server
Steps to reproduce
Observed on T3 Code desktop 0.0.33 (Linux AppImage) with Grok CLI 1.0.3 (grok-4.6), then reproduced the lockout on a brand-new Codex (gpt-5.6-terra) thread without touching that provider.
- Start a Grok thread in full-access. Send a prompt so a turn is actively running (tools/activity still flowing).
- While that turn is still running, switch the thread to Auto-accept edits.
- Immediately send another message.
- Optionally cycle the mode again (
auto-accept-edits→auto) and send another message. - Archive the stuck thread, then create a new Grok thread and send a message.
- Create a new Codex/GPT thread and send a message.
Deterministic unit shape: a fake Grok adapter whose startSession / stopSession never resolves, plus a second provider adapter that would start immediately. Dispatch thread.runtime-mode.set on an active thread, then thread.turn.start on a different thread/provider. The second turn must not stay queued behind the first startSession.
Expected behavior
- Switching runtime mode should either apply in-session or restart that thread's provider with a deadline.
- A hung Grok ACP child must not block session starts for other threads or other providers.
- Sidebar should not stay on Connecting after the restarted session has already reported
ready. - New threads that never got a session should not look idle while a turn is queued.
- Archiving / Stop session must still work.
Actual behavior
Changing to Auto-accept edits mid-turn restarts the Grok ACP session. That restart is awaited on the single global ProviderCommandReactor worker. When the restart does not return, every later provider intent is queued forever:
- further messages on the same thread do nothing
- most new threads show no status in the left nav (no session row was ever created)
- sometimes the original thread shows Connecting (
session.status === "starting") - new Grok sessions never get a session row
- new Codex/GPT sessions never get a session row either
- archive-triggered
thread.session-stop-requestedis never processed
Blank vs Connecting is the same hang at two different points: Connecting only if turn-start wrote starting before the worker wedged; blank if the new thread's startSession never ran.
Restarting the T3 app is the only recovery (kills the leftover grok agent stdio and the stuck worker).
Impact
Blocks work completely
Version or commit
T3 Code desktop 0.0.33. Source paths below are current main as of 2026-08-13 (pingdotgg/t3code).
Environment
- Linux (GNOME/Wayland), T3 Code AppImage 0.0.33
- Grok CLI 1.0.3, model
grok-4.6 - Also locked out Codex
gpt-5.6-terraafter the Grok hang - Local backend, no WSL
Logs or stack traces
Live orchestration_events from the stuck thread (ordered by sequence). First Grok turn was healthy until 19:12:38Z.
seq 201420 19:12:43.732Z thread.runtime-mode-set mode=auto-accept-edits
seq 201422 19:12:43.774Z thread.turn-start-requested mode=auto-accept-edits # user follow-up
seq 201423 19:12:43.861Z thread.session-set status=stopped mode=full-access
seq 201424 19:12:43.732Z thread.session-set status=ready mode=auto-accept-edits
seq 201425 19:12:44.203Z thread.session-set status=ready mode=full-access # late stale events
seq 201426 19:12:44.204Z thread.session-set status=ready mode=full-access
seq 201427 19:12:44.204Z thread.session-set status=ready mode=full-access
seq 201428 19:12:43.774Z thread.session-set status=starting mode=auto-accept-edits # overwrites ready
seq 201451 19:13:13.239Z thread.runtime-mode-set mode=auto
seq 201453 19:13:13.252Z thread.turn-start-requested mode=auto # never adopted
seq 201612 19:17:59.319Z thread.session-stop-requested # archive — never processed
A subsequent Grok thread and a subsequent Codex thread both persisted thread.created + thread.turn-start-requested and never received thread.session-set. Projection: one leftover projection_thread_sessions row stuck at starting / auto-accept-edits / grok; the two new threads have no session row.
A grok agent stdio child of the T3 server from 19:12 UTC was still alive 20+ minutes later, State: S, wchan=futex_do_wait, ~1s CPU. Stdio still connected to the server. That child was never reaped.
Why this happens (code)
1. One global worker awaits provider start/stop
ProviderCommandReactor drains all provider intents on a single makeDrainableWorker:
thread.runtime-mode-setthread.turn-start-requestedthread.turn-interrupt-requestedthread.approval-response-requestedthread.session-stop-requested
thread.runtime-mode-set calls ensureSessionForThread, which restarts the provider when thread.runtimeMode !== thread.session.runtimeMode and awaits providerService.startSession(...).
processTurnStartRequested does fork sendTurn, but it does not fork ensureSessionForThread. A hung restart therefore parks the worker. That is why Codex/GPT cannot start a session either — they are not failing, they are queued behind Grok.
2. Runtime-mode change always tears down the ACP process
const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode;
// ...
const restartedSession = yield* startProviderSession(/* resumeCursor */);
GrokAdapter startSession stops any existing session first (stopSessionInternal → Scope.close(ctx.scope)), then spawns grok agent stdio. There is no kill deadline on that close (same class of bug as #4944). If the child ignores stdin EOF or is blocked in a turn, Scope.close never returns.
session/load has a 90s timeout. initialize, authenticate, session/new, session/set_model, and Scope.close do not.
3. starting is written after ready
ensureSessionForThread(..., { pendingTurnStart: true }) dispatches status: "starting" before the (possibly already completed) restart bind. In the trace above, seq 201428 overwrote seq 201424. Sidebar pill is:
if (thread.session?.status === "starting") return { label: "Connecting", ... };
(apps/web/src/components/Sidebar.logic.ts)
If startSession never runs at all (new threads after the worker is wedged), session is null and the pill is null — that is the common "nothing in the left nav" case.
4. Grok does not implement Auto-accept edits
Claude maps auto-accept-edits → acceptEdits. Codex maps it to on-request + workspace-write. Grok only auto-approves when runtimeMode === "full-access":
https://github.com/pingdotgg/t3code/blob/main/apps/server/src/provider/Layers/GrokAdapter.ts
auto-accept-edits and auto still wait on a permission Deferred. Approvals are handled on the same stuck worker, so even if Grok asked, the answer can never be delivered.
5. Notification consumer dies on restart (#5781)
GrokAdapter forks the session/update consumer with Effect.forkChild inside startSession's Effect.scoped. The ACP process is transferred to sessionScope; the consumer is not. After the first mode-change restart the session can look ready while tool/text updates never project. That makes the follow-up turn look dead and encourages another mode cycle, which is what parked the worker.
Suggested fix
- Do not await provider
startSession/stopSessionon the global command worker. Per-thread workers, or fork the restart with a timeout. One hung Grok child must not block Codex. - Deadline + SIGKILL the ACP child on stop/restart.
Scope.closewithout a timeout is how you get a 20-minute Connecting state. - Fork the Grok/Cursor notification consumer into
sessionScope(Effect.forkIn), not thestartSessionfiber (#5781). - Do not let pending turn-start write
startingafter a successful restart has already writtenready. Ignore stalesession.state.changedfrom the instance being replaced (#4944). - Map Grok
auto-accept-edits/autothe way Claude/Codex do, instead of treating them as “still ask” and then serializing the answer behind the restart.
Workaround
Restart the T3 Code app. Archiving the thread or creating a new session will not help until the worker is gone — those commands sit on the same queue.
Until this is fixed: do not change runtime mode mid-turn on Grok. Wait until the turn is idle, or start a new thread already in the desired mode.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with apps/server/src/orchestration/Layers/ProviderCommandReactor.ts and trace runtime-mode-set, turn-start-requested, and session-stop-requested through the deterministic hanging-adapter scenario. Then inspect apps/server/src/provider/Layers/GrokAdapter.ts and apps/web/src/components/Sidebar.logic.ts for restart, notification, and status behavior. Done means a hung provider cannot block another provider, stale status does not overwrite ready, and recovery commands still run.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100