Before submitting
Area
apps/server (root cause), apps/web (missing feedback)
Summary
After a message is steered into a running turn, Stop generation stops working for the rest of that turn. Every click is accepted by the orchestrator and every resulting provider call is rejected by the Codex app-server with a turn-id mismatch. The UI gives no indication that anything failed.
Root cause: turn/steer returns a turn id that differs from the turn the provider is actually still executing, and we write that id into session.activeTurnId without validating it. Every later interrupt reads that value and targets a turn that does not exist.
This is fork-only code. git show upstream/main:apps/server/src/provider/Layers/CodexSessionRuntime.ts | grep -c 'turn/steer' returns 0.
Steps to reproduce
- Start a long-running turn (in the observed case: browser automation via Browser Use in a signed-in Chrome).
- While a tool call is in flight, send a follow-up with Send now (steer) rather than Queue.
- Click Stop generation.
- The button responds visually but the turn keeps running. Check
~/.t3/userdata/logs/server-child.log.
Expected behavior
The running turn is interrupted and the provider emits turn/completed with status interrupted.
Actual behavior
The interrupt is sent with the wrong turn id and rejected. The turn continues. Nothing is shown in the UI.
Observed on 2026-07-25, thread 64190f5b-426e-4d0e-a7bc-09d5d8339479:
- 16 distinct stop commands between
11:13:20.973Z and 11:13:38.070Z (17.1 s), all receipted accepted, zero receipt errors.
- 16 matching provider rejections between
11:13:20.990Z and 11:13:38.085Z.
- 0 successful interrupts. The turn only ended at
11:13:58.584Z, because a plain-text "Ok, stopp die Arbeit." message was sent and the agent cooperated.
Fault chain
- Turn
019f98f7-f883-7ec0-a0ae-096102edd399 starts at 11:10:15.72Z; session.activeTurnId is set correctly.
- At
11:11:30.955Z a follow-up is steered into the running turn.
turn/steer returns 019f98f9-1ed3-7013-8c24-12d99dabdce7, which we assign to session.activeTurnId unchecked.
- Every interrupt from then on targets
019f98f9-… and is rejected, because the provider is still executing 019f98f7-….
Independent confirmation that the second id was minted at steer time — the UUIDv7 timestamps decode as:
019f98f7-f883-… -> 2026-07-25T11:10:15.683Z (turn start, 11:10:15.72)
019f98f9-1ed3-… -> 2026-07-25T11:11:31.027Z (steer message, 11:11:30.955)
72 ms after the steer message. Meanwhile the provider log for the thread contains the original id 2078 times and the new id 0 times — the original turn ran uninterrupted to the end.
Code path
- Steer overwrites the active id —
apps/server/src/provider/Layers/CodexSessionRuntime.ts:1309-1318. turn/steer is sent with expectedTurnId: currentSession.activeTurnId, then session.activeTurnId is set to response.turnId with no validation. Introduced in 883b95eba ("Feature: add queued and steered messages").
- Interrupt reads the overwritten value — same file, line
1366: const effectiveTurnId = turnId ?? session.activeTurnId.
- The event payload is unused —
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:907 deliberately interrupts by session, so the runtime state alone decides the target. (Upstream code, unchanged.)
- The failure is swallowed —
ProviderCommandReactor.ts:1072-1081 ends the cause in a logWarning; apps/web/src/components/ChatView.tsx:4858-4871 only surfaces a dispatch failure. (Upstream code — filed separately at pingdotgg/t3code.)
One detail worth noting for whoever picks this up: updateSession (CodexSessionRuntime.ts:700) only does Ref.update and emits no event. The persisted thread.session-set projection therefore still holds the correct id 019f98f7-… — the bad value exists solely in the in-memory runtime Ref. Nothing in state.sqlite records it. Reloading the UI or opening the thread in another tab does not help, because the interrupt reads the Ref, not the projection.
Impact
Blocks work completely — while it lasts there is no working way to stop a running agent, and no feedback explaining why.
Version or commit
Fork main @ 56342adb4. Bug introduced by 883b95eba (2026-07-16). Desktop AppImage fork build.
Environment
Linux Mint 22.3, kernel 7.0.0-28-generic, Node 22.19.0, Codex provider (gpt-5.6-sol, full-access), Codex app-server.
Logs or stack traces
# ~/.t3/userdata/logs/server-child.log — 16 occurrences, 11:13:20.990Z .. 11:13:38.085Z
{
eventType: 'thread.turn-interrupt-requested',
cause: 'ProviderAdapterRequestError: Provider adapter request failed (codex) for turn/interrupt: expected active turn id 019f98f9-1ed3-7013-8c24-12d99dabdce7 but found 019f98f7-f883-7ec0-a0ae-096102edd399
at mapCodexRuntimeError (…/apps/server/dist/bin.mjs:66909:9)'
}
Proposed fix
- Validate the steer response. If
response.turnId differs from the known active id, do not blindly overwrite the running session state. Keep the current id, or confirm via thread/read.
- Re-target the interrupt once, safely. If the provider reports
expected active turn id … but found …, adopt the reported active id and retry exactly once.
- Make the failure visible. A failed provider interrupt should append a
provider.turn.interrupt.failed activity and a readable thread message, not just a server log line. The mechanism already exists — processTurnInterruptRequested uses it for the "no active provider session" branch.
The fix should not be scoped to Browser Use. The cause is in the general steer/interrupt state and can occur on any long turn with a mid-turn steer.
Acceptance criteria
- Runtime test: a running turn receives a steer response with a diverging id; the genuinely active turn stays interruptible.
- Provider test: an id mismatch on the first interrupt triggers exactly one safe retry with the reported active id.
- Error-path test: a definitively failed interrupt produces a visible thread activity, not only a log entry.
- Integrated web test: start a long harmless tool call, steer a message in with Send now, press Stop, and prove
turn/completed: interrupted.
- Second tab: open the same active thread and repeat the stop from there.
Workaround
- Prefer Queue over Send now (steer) during a running turn when you may need to stop reliably.
- If Stop does not respond, send a clear text instruction ("stop working now and take no further actions"). This is cooperative, not a technical interrupt — an in-flight tool call can still finish first.
- A reload or a different tab will not repair the already-wrong in-memory session state.
Notes
Investigation write-up: docs/investigations/t3-stop-interrupt-bug-2026-07-25.html. Findings independently re-verified against state.sqlite, the provider log, server-child.log, and the source tree before filing. No subscription change was made by the original turn.
Before submitting
Area
apps/server(root cause),apps/web(missing feedback)Summary
After a message is steered into a running turn, Stop generation stops working for the rest of that turn. Every click is accepted by the orchestrator and every resulting provider call is rejected by the Codex app-server with a turn-id mismatch. The UI gives no indication that anything failed.
Root cause:
turn/steerreturns a turn id that differs from the turn the provider is actually still executing, and we write that id intosession.activeTurnIdwithout validating it. Every later interrupt reads that value and targets a turn that does not exist.This is fork-only code.
git show upstream/main:apps/server/src/provider/Layers/CodexSessionRuntime.ts | grep -c 'turn/steer'returns0.Steps to reproduce
~/.t3/userdata/logs/server-child.log.Expected behavior
The running turn is interrupted and the provider emits
turn/completedwith statusinterrupted.Actual behavior
The interrupt is sent with the wrong turn id and rejected. The turn continues. Nothing is shown in the UI.
Observed on 2026-07-25, thread
64190f5b-426e-4d0e-a7bc-09d5d8339479:11:13:20.973Zand11:13:38.070Z(17.1 s), all receiptedaccepted, zero receipt errors.11:13:20.990Zand11:13:38.085Z.11:13:58.584Z, because a plain-text "Ok, stopp die Arbeit." message was sent and the agent cooperated.Fault chain
019f98f7-f883-7ec0-a0ae-096102edd399starts at11:10:15.72Z;session.activeTurnIdis set correctly.11:11:30.955Za follow-up is steered into the running turn.turn/steerreturns019f98f9-1ed3-7013-8c24-12d99dabdce7, which we assign tosession.activeTurnIdunchecked.019f98f9-…and is rejected, because the provider is still executing019f98f7-….Independent confirmation that the second id was minted at steer time — the UUIDv7 timestamps decode as:
72 ms after the steer message. Meanwhile the provider log for the thread contains the original id 2078 times and the new id 0 times — the original turn ran uninterrupted to the end.
Code path
apps/server/src/provider/Layers/CodexSessionRuntime.ts:1309-1318.turn/steeris sent withexpectedTurnId: currentSession.activeTurnId, thensession.activeTurnIdis set toresponse.turnIdwith no validation. Introduced in883b95eba("Feature: add queued and steered messages").1366:const effectiveTurnId = turnId ?? session.activeTurnId.apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:907deliberately interrupts by session, so the runtime state alone decides the target. (Upstream code, unchanged.)ProviderCommandReactor.ts:1072-1081ends the cause in alogWarning;apps/web/src/components/ChatView.tsx:4858-4871only surfaces a dispatch failure. (Upstream code — filed separately at pingdotgg/t3code.)One detail worth noting for whoever picks this up:
updateSession(CodexSessionRuntime.ts:700) only doesRef.updateand emits no event. The persistedthread.session-setprojection therefore still holds the correct id019f98f7-…— the bad value exists solely in the in-memory runtimeRef. Nothing instate.sqliterecords it. Reloading the UI or opening the thread in another tab does not help, because the interrupt reads theRef, not the projection.Impact
Blocks work completely — while it lasts there is no working way to stop a running agent, and no feedback explaining why.
Version or commit
Fork
main @ 56342adb4. Bug introduced by883b95eba(2026-07-16). Desktop AppImage fork build.Environment
Linux Mint 22.3, kernel 7.0.0-28-generic, Node 22.19.0, Codex provider (
gpt-5.6-sol,full-access), Codex app-server.Logs or stack traces
Proposed fix
response.turnIddiffers from the known active id, do not blindly overwrite the running session state. Keep the current id, or confirm viathread/read.expected active turn id … but found …, adopt the reported active id and retry exactly once.provider.turn.interrupt.failedactivity and a readable thread message, not just a server log line. The mechanism already exists —processTurnInterruptRequesteduses it for the "no active provider session" branch.The fix should not be scoped to Browser Use. The cause is in the general steer/interrupt state and can occur on any long turn with a mid-turn steer.
Acceptance criteria
turn/completed: interrupted.Workaround
Notes
Investigation write-up:
docs/investigations/t3-stop-interrupt-bug-2026-07-25.html. Findings independently re-verified againststate.sqlite, the provider log,server-child.log, and the source tree before filing. No subscription change was made by the original turn.