pingdotgg / pingdotgg/t3code

[Bug]: Changing runtime mode mid-turn hangs ProviderCommandReactor; all new sessions stay Connecting or show no status

Open
#6,517 0 comments 0 reactions 0 assignees View on GitHub

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 stopSession with no deadline (same missing-timeout family, one thread / one provider).
  • #5781 — Grok/Cursor/Codex notification consumers die when startSession returns.
  • #5164 — OpenCode collapses auto-accept / auto into blanket ask.
  • #4518 — persisted auto runtime 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.

  1. Start a Grok thread in full-access. Send a prompt so a turn is actively running (tools/activity still flowing).
  2. While that turn is still running, switch the thread to Auto-accept edits.
  3. Immediately send another message.
  4. Optionally cycle the mode again (auto-accept-editsauto) and send another message.
  5. Archive the stuck thread, then create a new Grok thread and send a message.
  6. 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-requested is 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-terra after 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-set
  • thread.turn-start-requested
  • thread.turn-interrupt-requested
  • thread.approval-response-requested
  • thread.session-stop-requested

https://github.com/pingdotgg/t3code/blob/main/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts

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 (stopSessionInternalScope.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-editsacceptEdits. 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

  1. Do not await provider startSession / stopSession on the global command worker. Per-thread workers, or fork the restart with a timeout. One hung Grok child must not block Codex.
  2. Deadline + SIGKILL the ACP child on stop/restart. Scope.close without a timeout is how you get a 20-minute Connecting state.
  3. Fork the Grok/Cursor notification consumer into sessionScope (Effect.forkIn), not the startSession fiber (#5781).
  4. Do not let pending turn-start write starting after a successful restart has already written ready. Ignore stale session.state.changed from the instance being replaced (#4944).
  5. Map Grok auto-accept-edits / auto the 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.