fix(remote): stop button gives immediate feedback and prevents stuck thinking on remote server - #8619
Conversation
…tored agent selections opencode <=1.18 writes ESC ]0;<cwd>: ready BEL to stdout for every non-help command even when stdout is a pipe (agent list, models --verbose, debug skill). T3's ChildProcessSpawner captures that stdout via collectStreamAsString and the parsers stored a polluted agent id like "\x1b]0;imbios: ready\x07build" in model_selection_json. Later sendTurn used that polluted id and opencode rejected it with "Agent not found: \"\x1b]0;imbios: ready\x07build\"" which was surfaced as session.error UnknownError + a generic SessionPrompt UnknownError wrapper (the stack the user pasted). Fix: - packages/shared/src/stripTerminalEscapes.ts: shared OSC/CSI sanitizer - apps/server/src/provider/opencodeRuntime.ts: strip before parseModels/Agent/Skills and via parse* entry points; keeps skills from silently degrading to [] when polluted - apps/server/src/provider/Layers/OpenCodeProvider.ts: sanitize inventory agent names/variants and --version parsing; build clean capability option ids - apps/server/src/provider/Layers/OpenCodeAdapter.ts & textGeneration/OpenCodeTextGeneration.ts: sanitize stored getModelSelectionStringOptionValue values before promptAsync - packages/shared/src/model.ts: sanitize persisted option values and model slugs on read (repairs 3 polluted threads without DB migration) - tests: add OSC/ANSI regression cases for both parsers Polluted threads still read as clean via model.ts sanitizer; no migration needed but DB can be cleaned with stripTerminalEscapes. Fixes the reported UnknownError at SessionPrompt.createUserMessage and the earlier "Agent not found" session.error.
…ng on remote Fixes pingdotgg#8618 Remote stop had no optimistic state, so clicks over relay (100-400ms RTT + 50ms shell coalesce) looked dead while local 10-20ms masked it. Also stale activeTurnId omitted turnId, causing thread.turn-interrupt-requested to be ignored by threadReducer/ProjectionPipeline, and successful interrupts that left the provider alive kept session in running forever (Working for Xm Ys stuck). This commit adds isStoppingTurn (mirrors isStoppingBackgroundWork) that shows Stopping... instantly and clears when isWorking false or thread switches. Remaining fallbacks (turnId guard relaxation, server 5s escalation, singleFlight/timeout) are tracked in the forkhub intent fix-remote-stop-no-feedback-stuck-thinking-7h3k9p2m and will follow in follow-up commits.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
| * through `shell: true` spawns. | ||
| */ | ||
| const OSC_RE = /\x1b\].*?(?:\x07|\x1b\\)/g; | ||
| const CSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g; |
There was a problem hiding this comment.
🟡 Medium src/stripTerminalEscapes.ts:17
stripTerminalEscapes leaves colon-form CSI sequences in the output, so \x1b[38:2::255:0:0mbuild (primary) becomes 38:2::255:0:0mbuild (primary) and the OpenCode agent-list parser drops that agent. CSI_RE only accepts [0-9;?], excluding valid ECMA-48 parameter bytes such as :, so match the complete 0x30–0x3f range.
| const CSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g; | |
| const CSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g; |
🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/shared/src/stripTerminalEscapes.ts around line 17:
`stripTerminalEscapes` leaves colon-form CSI sequences in the output, so `\x1b[38:2::255:0:0mbuild (primary)` becomes `38:2::255:0:0mbuild (primary)` and the OpenCode agent-list parser drops that agent. `CSI_RE` only accepts `[0-9;?]`, excluding valid ECMA-48 parameter bytes such as `:`, so match the complete `0x30`–`0x3f` range.
There was a problem hiding this comment.
One finding: the new optimistic stopping state in ChatView.tsx is never rendered, so the Stop button still gives no feedback on remote.
Posted via Macroscope — UI Consistency
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This is a broad production behavior change spanning remote session orchestration, event projection, command concurrency, and web/mobile Stop-state handling, including forced provider-session teardown. Unresolved substantive findings identify cases where Stop can remain unusable or leak its disabled state across threads, so the change warrants human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
# Conflicts: # apps/server/src/provider/Layers/OpenCodeAdapter.ts # apps/server/src/provider/Layers/OpenCodeProvider.ts # apps/server/src/provider/opencodeRuntime.cliParsers.test.ts # packages/shared/package.json
Stop on a remote server gave no UI feedback and left the thread stuck in Thinking: interrupts without a turnId were dropped by both projections, and a provider that ignored the abort kept the session pinned at running while every further Stop was accepted with no effect (pingdotgg#8618, pingdotgg#4713, pingdotgg#8802). - web/mobile: optimistic stopping state (Stopping feedback, disabled) held until the session leaves running - client/server projection: turnId-less interrupts fall back to the session-pinned turn instead of no-op - reactor: repeat Stop releases a wedged session (terminal turn still pinned, or same session running past a 5s grace) via stopSession + forced session stop, so the thread is resumable - interruptTurn is singleFlight per thread so rapid Stop clicks share one in-flight request instead of queueing
| } | ||
| // Remember this request so a later repeat Stop can escalate. | ||
| if (Number.isFinite(requestedAtMs)) { | ||
| recentTurnInterrupts.set(input.threadId, { turnId: activeTurnId, requestedAtMs }); |
There was a problem hiding this comment.
🟡 Medium Layers/ProviderCommandReactor.ts:1383
recentTurnInterrupts grows without bound when abandoned wedged threads never emit a cleanup event, so a long-lived server accumulates one entry per thread indefinitely. Bound the map (or expire entries) when recording a stop request.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderCommandReactor.ts around line 1383:
`recentTurnInterrupts` grows without bound when abandoned wedged threads never emit a cleanup event, so a long-lived server accumulates one entry per thread indefinitely. Bound the map (or expire entries) when recording a stop request.
| (activeTurnId === null || latestTurn.turnId === activeTurnId) && | ||
| latestTurn.state !== "running" && | ||
| latestTurn.completedAt !== null && | ||
| latestTurn.completedAt < input.createdAt |
There was a problem hiding this comment.
🟡 Medium Layers/ProviderCommandReactor.ts:1346
The zombie check fails to release an already-completed turn when completedAt and createdAt use different valid timezone offsets, because their ISO strings are compared lexicographically rather than chronologically. Parse both IsoDateTime values before comparing them so a wedged session is released correctly.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderCommandReactor.ts around line 1346:
The zombie check fails to release an already-completed turn when `completedAt` and `createdAt` use different valid timezone offsets, because their ISO strings are compared lexicographically rather than chronologically. Parse both `IsoDateTime` values before comparing them so a wedged session is released correctly.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c8b4b3f. Configure here.
| useEffect(() => { | ||
| // Per-thread: switching threads must not leak Stopping... to B | ||
| setIsStoppingTurn(false); | ||
| }, [activeThreadId]); |
There was a problem hiding this comment.
Disabled Stop blocks wedged-session release
High Severity
A successful interrupt leaves isStoppingTurn / isStoppingThread set until the session leaves running, which disables Stop. maybeReleaseWedgedSession only calls stopSession on a later Stop after the 5s grace. When the provider ignores the abort, the control never re-enables and Thinking/Working never clears.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit c8b4b3f. Configure here.
| if (selectedThreadSessionStatus !== "running" && selectedThreadSessionStatus !== "starting") { | ||
| setIsStoppingThread(false); | ||
| } | ||
| }, [selectedThreadIdentity, selectedThreadSessionStatus]); |
There was a problem hiding this comment.
Stopping state leaks across threads
Medium Severity
isStoppingThread is a single boolean, and the reset effect only clears it when the newly selected session is not running or starting. Switching from a stopping thread onto another live thread leaves that thread’s Stop disabled and announced as Stopping.
Reviewed by Cursor Bugbot for commit c8b4b3f. Configure here.


Fixes #8618. Related: #4713, #8802.
Problem
Using a remote T3 Code server (relay/tunnel), clicking the red Stop button gives no UI feedback and the thread can stay stuck in Thinking / Working indefinitely.
Root causes, all verified on current main:
onInterruptfires and waits silently; the Stop button has no pending/disabled variant (contrastisStoppingBackgroundWork→Stopping...).turnIddrops the interrupt — the client only includesturnIdwhen the snapshot has it, and both the client reducer (threadReducer.ts) and the server projection (ProjectionPipeline.ts) early-return forturnId === undefined.ProviderCommandReactoronly tears down on interrupt failure. When the provider ignores the abort (or the turn already ended without asession-set), the session staysrunningand every further Stop is accepted with no effect.interruptTurnisserialper thread, so rapid Stop clicks pile up behind each other.Fix
isStoppingTurn/isStoppingThreadset on click, cleared on failure or when the session leavesrunning/starting, reset on thread switch. The Stop button shows a spinner, is disabled, and announces Stopping.thread.turn-interrupt-requestedfalls back to the session-pinned turn in both the client reducer and the server projection.completedAt < createdAtguard keeps healthy in-flight interrupts on the normal path), or when a repeat Stop arrives 5s+ after the previous one for the same still-running session. Release = best-effortstopSession+ forcedsession-set stoppedwith a visible reason, so the thread is resumable (same end state as the manual SIGTERM workaround in Thread session stuck inrunningafter turn interrupt — stop button becomes a no-op #4713).interruptTurnis nowsingleFlightper thread so rapid clicks share one in-flight request.Also restores the
@t3tools/shared/stripTerminalEscapesexport dropped while merging main (fixes server typecheck).Verification
vp test run—ProviderCommandReactor.test.ts(54, incl. 3 new: zombie release, repeat-stop escalation, in-grace negative),ProjectionPipeline.test.ts(28, incl. 1 new turnId-less fallback),threadReducer.test.ts(36, incl. 2 new),ComposerPrimaryActions.test.tsx(17, incl. 1 new), plusthreads-atoms/threads-sync.t3(server),@t3tools/client-runtime,@t3tools/web,@t3tools/mobile,@t3tools/shared. Scopedvp lintexit 0.npx t3 --shareon remote, connect via pairing URL, start a long turn, click Stop → button feedback instantly, Thinking/Working clears even if the provider is wedged; repeat Stop escalates within seconds.Note
Fix stop button feedback and escalate repeat-stop for stuck remote sessions
turnIdby falling back to the thread session'sactiveTurnId.ProviderCommandReactorto stop wedged sessions. A repeated stop after 5 seconds invokes a full session stop instead of a normal interrupt.stripTerminalEscapesandsanitizeTerminalValueutilities in stripTerminalEscapes.ts to remove terminal escape sequences from CLI output and model option values.Macroscope summarized c8b4b3f.