Before submitting
Area
apps/server (with a smaller follow-on in apps/web)
Summary
When the provider call behind Stop generation fails, the failure is only written to a log line. The orchestration command is still receipted as accepted, no thread activity is appended, and the UI shows nothing. From the user's perspective the Stop button is simply dead while the turn keeps running.
The codebase already has the right mechanism for this — it is just not wired to this branch. In processTurnInterruptRequested, the "no active provider session" case calls appendProviderFailureActivity({ kind: "provider.turn.interrupt.failed", ... }). The case where the provider request itself fails does not.
Steps to reproduce
- Start a long-running turn (long tool call, browser automation, etc.).
- Put the provider in a state where
turn/interrupt is rejected, so providerService.interruptTurn fails with a ProviderAdapterRequestError. Any provider-side rejection reaches the same path — see "How I hit it" below for the concrete trigger I observed.
- Click Stop generation (repeatedly, if you like).
- Watch the thread UI, the activity feed, and
~/.t3/userdata/logs/server-child.log.
Expected behavior
A failed interrupt is surfaced to the user — a provider.turn.interrupt.failed activity in the thread, or a thread error — consistent with how the "no active provider session" branch already behaves.
Actual behavior
- The command is receipted
accepted with no error.
thread.turn-interrupt-requested is persisted normally.
- The provider call fails.
- The only trace is a
logWarning; the thread gets no error activity and the UI stays silent.
- The turn continues running.
Code path (upstream/main @ 41a430a)
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:907 — the interrupt is issued by session only (interruptTurn({ threadId })), so the runtime's internal turn id decides the target.
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1072-1081 — processDomainEventSafely catches the cause and terminates it in Effect.logWarning("provider command reactor failed to process event", …). Nothing is emitted to the thread.
apps/web/src/components/ChatView.tsx:4830 — onInterrupt only calls setThreadError when the dispatch fails. A dispatch that succeeds and a provider call that later fails is indistinguishable from success in the UI.
Contrast with ProviderCommandReactor.ts:896-904, which does the right thing for the missing-session case.
Impact
Major degradation or frequent failure — when it happens the user has no working way to stop a running agent and no indication why.
Version or commit
upstream/main @ 41a430a88 (code paths above read directly from that ref). Observed at runtime on a downstream fork that is based on it.
Environment
Linux Mint 22.3, kernel 7.0.0-28-generic, Node 22.19.0, desktop AppImage build, Codex provider (gpt-5.6-sol), Codex app-server.
Logs or stack traces
# ~/.t3/userdata/logs/server-child.log — repeated 16x in 17s, one per click
{
eventType: 'thread.turn-interrupt-requested',
cause: 'ProviderAdapterRequestError: Provider adapter request failed (codex) for turn/interrupt: expected active turn id 019f98f9-… but found 019f98f7-…
at mapCodexRuntimeError (…/apps/server/dist/bin.mjs:66909:9)'
}
Meanwhile, in state.sqlite:
-- 16 events, 16 distinct command ids, every receipt accepted, zero receipt errors
SELECT r.status, count(*), sum(r.error IS NOT NULL)
FROM orchestration_events e
JOIN orchestration_command_receipts r ON r.command_id = e.command_id
WHERE e.event_type = 'thread.turn-interrupt-requested';
-- accepted|16|0
No provider.turn.interrupt.failed activity was appended to the thread in that window.
How I hit it, and why it may matter for #231
Full disclosure on the trigger: the specific mismatch above came from a turn/steer implementation in my own fork, not from upstream — upstream/main has no turn/steer call. So the root cause of my mismatch is mine to fix, and I am not asking you to fix that part.
The reason I am filing here anyway is that the swallow-the-failure path is entirely upstream code and will hide any provider interrupt rejection, whatever the cause.
Since #231 (feat: add Steer and Queue follow-up modes) is in progress, one concrete note that may save some time. In my fork the sequence was:
turn/steer is called on the running turn with expectedTurnId: <current>.
- The response carries a different turn id than the turn the provider is actually still executing.
- That returned id is written straight into the runtime's
activeTurnId.
- Every later interrupt reads that value and is rejected with
expected active turn id X but found Y.
I confirmed step 2/3 independently: the id T3 sent decodes as a UUIDv7 minted at 11:11:31.027Z, 72 ms after the steer message — while the provider's own event stream stayed on the original turn id for the entire turn (2078 occurrences of the original id, 0 of the new one).
If the upstream steer implementation also adopts the steer response's turn id as the active one, it can land in the same state. Validating that response — or at least making the interrupt failure visible — would cover it.
Workaround
Send a plain text instruction ("stop working now, take no further actions"). That is cooperative, not a real interrupt, so an in-flight tool call can still finish first. In my incident the turn only ended that way.
Before submitting
Area
apps/server(with a smaller follow-on inapps/web)Summary
When the provider call behind Stop generation fails, the failure is only written to a log line. The orchestration command is still receipted as
accepted, no thread activity is appended, and the UI shows nothing. From the user's perspective the Stop button is simply dead while the turn keeps running.The codebase already has the right mechanism for this — it is just not wired to this branch. In
processTurnInterruptRequested, the "no active provider session" case callsappendProviderFailureActivity({ kind: "provider.turn.interrupt.failed", ... }). The case where the provider request itself fails does not.Steps to reproduce
turn/interruptis rejected, soproviderService.interruptTurnfails with aProviderAdapterRequestError. Any provider-side rejection reaches the same path — see "How I hit it" below for the concrete trigger I observed.~/.t3/userdata/logs/server-child.log.Expected behavior
A failed interrupt is surfaced to the user — a
provider.turn.interrupt.failedactivity in the thread, or a thread error — consistent with how the "no active provider session" branch already behaves.Actual behavior
acceptedwith no error.thread.turn-interrupt-requestedis persisted normally.logWarning; the thread gets no error activity and the UI stays silent.Code path (upstream/main @ 41a430a)
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:907— the interrupt is issued by session only (interruptTurn({ threadId })), so the runtime's internal turn id decides the target.apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1072-1081—processDomainEventSafelycatches the cause and terminates it inEffect.logWarning("provider command reactor failed to process event", …). Nothing is emitted to the thread.apps/web/src/components/ChatView.tsx:4830—onInterruptonly callssetThreadErrorwhen the dispatch fails. A dispatch that succeeds and a provider call that later fails is indistinguishable from success in the UI.Contrast with
ProviderCommandReactor.ts:896-904, which does the right thing for the missing-session case.Impact
Major degradation or frequent failure — when it happens the user has no working way to stop a running agent and no indication why.
Version or commit
upstream/main @ 41a430a88(code paths above read directly from that ref). Observed at runtime on a downstream fork that is based on it.Environment
Linux Mint 22.3, kernel 7.0.0-28-generic, Node 22.19.0, desktop AppImage build, Codex provider (
gpt-5.6-sol), Codex app-server.Logs or stack traces
Meanwhile, in
state.sqlite:No
provider.turn.interrupt.failedactivity was appended to the thread in that window.How I hit it, and why it may matter for #231
Full disclosure on the trigger: the specific mismatch above came from a
turn/steerimplementation in my own fork, not from upstream — upstream/main has noturn/steercall. So the root cause of my mismatch is mine to fix, and I am not asking you to fix that part.The reason I am filing here anyway is that the swallow-the-failure path is entirely upstream code and will hide any provider interrupt rejection, whatever the cause.
Since #231 (
feat: add Steer and Queue follow-up modes) is in progress, one concrete note that may save some time. In my fork the sequence was:turn/steeris called on the running turn withexpectedTurnId: <current>.activeTurnId.expected active turn id X but found Y.I confirmed step 2/3 independently: the id T3 sent decodes as a UUIDv7 minted at
11:11:31.027Z, 72 ms after the steer message — while the provider's own event stream stayed on the original turn id for the entire turn (2078 occurrences of the original id, 0 of the new one).If the upstream steer implementation also adopts the steer response's turn id as the active one, it can land in the same state. Validating that response — or at least making the interrupt failure visible — would cover it.
Workaround
Send a plain text instruction ("stop working now, take no further actions"). That is cooperative, not a real interrupt, so an in-flight tool call can still finish first. In my incident the turn only ended that way.