[Bug]: Stop does not lift the background-work guard: provider/model switch stays refused ("press Stop" is a dead end)
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- electron, sqlite, typescript
Direzione di ricerca
Start in apps/server, where the issue identifies sessionHasBackgroundWork, startNextQueuedRun, the direct-send sessionsToDetach path, and the Claude query-replace guard; trace how thread.stop and run.interrupt update session state. Compare the adapter's in-memory background-work probes with derivePendingBackgroundWork, then reproduce a refused provider or model switch after Stop. Done means an explicit Stop lets the switch proceed despite stale adapter state, with the failure path diagnosable.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Before submitting
- I searched existing issues and found related ones (#32296, #16073, #25715) but not this one: here Stop itself never lifts the refusal, so the message that says "press Stop" is a dead end.
- I included enough detail to investigate the problem.
Area
apps/server
Environment
T3 Code (Beta) 0.1.9 desktop (Electron 44), Linux x86_64. Claude Agent and Pi providers, switched back and forth inside one long thread.
What happens
After a thread has used background work (background shell commands, an Agent subagent, long run_in_background commands), sending a message that changes provider or model is refused with:
The previous provider is still running background agents or commands, and switching would end them. Wait for them to finish, or press Stop, then send the message again.
(or, for a model/setting change inside a Claude session: Claude is still running background agents or commands, and this model or setting change would end them...)
Everything the agent started has long finished, nothing is running, and pressing Stop (button, thread.stop, or run.interrupt) changes nothing: the same refusal comes back on every retry. It only went away when the stuck provider session row was set to stopped directly in statev2.sqlite (the guard skips sessions whose status is stopped/error), which is how I got unstuck the first time. A server restart presumably clears it too, since the flag lives in memory (not tested).
I could not capture which in-memory condition stays set: the refusal is a dispatch error and is not written to the logs or to orchestration_v2_command_receipts, so it is invisible afterwards. That is worth fixing on its own.
Why Stop cannot help (from reading the 0.1.9 server bundle, apps/server/dist/runtimeLayer-*.mjs)
Two independent sources of truth:
- The refusal comes from
sessionHasBackgroundWork(session, providerThreads), called fromstartNextQueuedRun(which then holds the queue) and from the direct-send path (sessionsToDetach, which refuses). It asks the adapter's in-memory state:runtime.hasPendingBackgroundWork.- Claude: any non-empty entry in
pendingBackgroundTasksByNativeThread, anysessionSubagentsByTaskIdentry stillrunning, or any buffered user/assistant/result frame inwakeBuffers. - The non-Claude adapter whose probe is
owesWork(I did not confirm which driver exactly; it was the one used by my second provider): running shells, image jobs,wakes.length > 0,reports.size > 0, a background call, or a busy subagent. - Any failure of the probe is treated as busy:
probe.pipe(catchCause(() => succeed(true))).
- Claude: any non-empty entry in
- Stop (
dispatchThreadStop/dispatchRunInterrupt) only reconciles the projection: it derives pending work withderivePendingBackgroundWorkover turn items and runs and marks those settled. It never touches the adapter's roster, wake buffers, reports, or wakes.
So when the adapter flag is stale (or the probe throws) while the projection is already settled, Stop has nothing to settle and returns, and the guard keeps refusing. The error text tells the user to press Stop, which cannot work in this state.
Expected behavior
An explicit Stop should be authoritative for the thread: after it, a provider or model switch proceeds and the old session is detached (which ends whatever is really still running, as the message warns). Stale in-memory state must not survive an explicit Stop.
Suggested fix
Smallest change I tested: remember the explicit Stop per thread and let it satisfy the guard for a short window. Local patch against 0.1.9 (module-level helper plus one check at each of the three guards):
const stopAck = new Map();
const STOP_ACK_TTL_MS = 10 * 60 * 1000;
function markStopped(threadId) { stopAck.set(threadId, Date.now()); }
function stopRecent(threadId) {
const at = stopAck.get(threadId);
if (at === undefined) return false;
if (Date.now() - at > STOP_ACK_TTL_MS) { stopAck.delete(threadId); return false; }
return true;
}
// in the command switch, only for the user-facing commands (not internal callers of dispatchRunInterrupt):
case "run.interrupt": markStopped(command.threadId); /* ... */
case "thread.stop": markStopped(command.threadId); /* ... */
// guards:
// startNextQueuedRun: if (!stopRecent(threadId) && (yield* sessionHasBackgroundWork(session, ...))) busySessionIds.add(session.id)
// direct send: if (!stopRecent(command.threadId) && (yield* sessionHasBackgroundWork(detaching, ...))) return yield* new OrchestratorDispatchError(...)
// Claude query replace: ... && !existing.stopping && !stopRecent(turnInput.threadId) && (yield* liveProcessRunsBackgroundWork(existing))
A more thorough fix would have Stop clear the adapter state too (roster, wake buffers, pi reports/wakes), and the probe should not fail closed: a throwing hasPendingBackgroundWork should be logged and not treated as "busy forever". Also please persist or log the dispatch error so a stuck guard can be diagnosed after the fact.
Not verified
- I could not reproduce deterministically. It showed up twice in one long thread with heavy use of background commands and several provider switches (Claude Agent <-> Pi).
- The patch is installed locally; I have not yet seen a second occurrence with it, so I can only say it should remove the dead end, not that it fixes the leak that leaves the flag set.
Related: #32296 (same guard refuses a direct send that the queued path accepts), #16073, #25715.
- Lingua principale
- TypeScript
- Stelle
- 24.8k
- Fork
- 6.4k
- Merge medio
- 4h 16m
- PR unite (30g)
- 352
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di pingdotgg/t3code
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di pingdotgg/t3code
Issue simili
-
Autocomplete still accepts an option after becoming disabledForse già presa @Xapollon430 l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
reduxjs/react-redux#2416 · 1 commento ·