feat(session): end a turn that has stopped producing anything - #103
Merged
Conversation
A turn that goes quiet had nothing to stop it. The provider accepts the request, sends nothing, and the session sits busy until someone notices — thirty minutes with a spinner, everything typed queueing behind it, and a bill for the retries. The watchdog measures silence, not elapsed time. A wall-clock deadline also ends healthy long turns; only inactivity distinguishes a dead turn from a slow one. Two things are deliberately not silence: a tool that is running, since tools execute inside the provider SDK and emit nothing at all while they work, and a permission waiting on a person. Either would otherwise make a long build look exactly like a provider that went away. Where it acts is a judgement about who is present. In the TUI and the desktop app it reports and leaves the turn alone: someone is sitting there, can read the warning, and pressing escape is their call to make. In a scripted run, an editor speaking ACP or a scheduled job nobody will ever press anything, so silence is the whole failure and ending it is the only useful act. - `packages/redcode/src/session/stall.ts` — the decision, kept pure so every branch is testable without timers, fibers or a provider. - The abort writes its reason onto the message before interrupting. Every later writer on that path guards with `??=`, so whoever arrives first decides what the message says; otherwise this would read as an ordinary user interrupt. - The cancel is detached on purpose: cancelling interrupts the watchdog's own fiber partway through, and the part that returns the session to idle runs after that point. - The TUI now identifies itself as `REDCODE_CLIENT=tui`. The server runs inside its worker and could not otherwise tell itself apart from a scripted run. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b
This was referenced Sep 4, 2026
filipeforattini
added a commit
that referenced
this pull request
Sep 4, 2026
The turbo `test` task was declared for five names, one of which — `opencode#test` — stopped matching anything at the rename, so the main package and the TUI have not run in CI since. Declaring it once covers all 30 suites. Turning them on surfaced two real defects, both mine: - mise detection matched any `mise/installs` path, so a machine whose Bun comes from mise called every install mise-managed; - a source-text assertion pinned the exact shape of the worker's `env` literal and broke when #103 added a key, while saying nothing about whether the environment still reaches the worker. Keep `^build` only on the four tasks that already had it: the generic task made every test run build the release binaries. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b
filipeforattini
added a commit
that referenced
this pull request
Sep 4, 2026
* ci: run every package's tests, not five of them turbo.json declared `test` for five task names, one of which — `opencode#test` — stopped matching anything at the rename. The main package and the TUI have not run in CI since: a regression I shipped in #96 broke three TUI tests and nothing noticed until I ran them by hand. Declare the task once so every package that has a test script runs it, and raise the job timeout to match the new scope. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b * ci: run every package's tests, and fix the two failures that hid there The turbo `test` task was declared for five names, one of which — `opencode#test` — stopped matching anything at the rename, so the main package and the TUI have not run in CI since. Declaring it once covers all 30 suites. Turning them on surfaced two real defects, both mine: - mise detection matched any `mise/installs` path, so a machine whose Bun comes from mise called every install mise-managed; - a source-text assertion pinned the exact shape of the worker's `env` literal and broke when #103 added a key, while saying nothing about whether the environment still reaches the worker. Keep `^build` only on the four tasks that already had it: the generic task made every test run build the release binaries. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b * test: fix what turning the suites on revealed - `redcode run`: #98 made an unknown finish continue the turn instead of ending it in silence, which is the recovery we want; the two tests still pinned the old shape. They now assert the recovery and what must not change with it. - `httpapi-codegen`: expectations pinned "/" separators and read a fixture directory through `URL.pathname`, which is "/C:/..." on Windows. Neither had ever run there. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b * test: make the newly-running suites pass on machines other than a quiet Linux box Everything here failed for a reason unrelated to what the test was checking: - TUI frame captures gave up after 125 ms and command registration after 250 ms, so the whole-suite run read a blank frame as a broken component; - `tool.write` asserted 0644, which is only what a umask of 022 produces — forcing the mode would be worse than the bug, so the test now follows umask; - the codegen fixture compared bytes across a CRLF checkout; - the webfetch converter test builds 10,000 nested divs on purpose and the embedded-server suite boots a server per test, both against a 5 s default. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b * test: resolve effect through the module graph, not this package's node_modules `packages/client/node_modules` does not exist on the Windows runner — the installer hoists to the workspace root — so the file threw at import time and the whole suite failed before a test ran. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b * test: stop pinning POSIX separators in TUI path assertions Both tests build paths with `path`, and both asserted the result with a hard-coded "/". They described a POSIX machine, which is the only kind they had ever run on. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b * test: build the SDK before the CLI tests, and call quoted binaries the way PowerShell needs - `@reddb-io/redcode#test` spawns the real CLI, which reads generated SDK sources; without `^build` it read a file that had not been generated yet. - One shell test built a quoted command without the call operator that the helper beside it already applies, so PowerShell parsed the path as a string and the first flag as a syntax error. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Primeira tranche do plano. Um turno que fica em silêncio não tinha nada que o encerrasse: o provider aceita a requisição, não manda nada, e a sessão fica ocupada até alguém perceber — trinta minutos de barra rodando, tudo que você digita empilhando atrás, e a conta das tentativas.
O que ele mede
Silêncio, não tempo decorrido. Um prazo por relógio também mata turno saudável longo; só inatividade distingue turno morto de turno lento. Foi a escolha do hermes, o único dos cinco harnesses estudados desenhado para rodar sem ninguém olhando.
Duas coisas deliberadamente não contam como silêncio:
cargo buildde meia hora é indistinguível de um provider que sumiu. O teste que prova isso é o mais importante do PR, e verifiquei que ele falha sem o guard: sem ele, o watchdog mata o turno com a ferramenta em execução.Onde ele age
Isto é um julgamento sobre quem está presente, e foi a sua decisão. Na TUI e no app, ele reporta e não encerra: tem gente sentada ali, que lê o aviso, e apertar Esc é uma escolha dela. Num run script, num editor falando ACP ou num job agendado, ninguém vai apertar nada nunca — ali o silêncio é a falha inteira e encerrar é o único ato útil.
Consequência honesta: o travamento que você vê na TUI passa a ser visível, não encerrado. Se na prática isso se mostrar errado, é uma configuração de distância.
Detalhes que custaram para acertar
??=, então quem chega primeiro decide o que a mensagem diz. Sem isso, um encerramento por watchdog fica idêntico a você ter apertado Esc.busy— exatamente o bug que este PR existe para matar.REDCODE_CLIENT=tui. O servidor roda dentro do worker dela e, até agora, não conseguia se distinguir de um run headless: ambos eramcli.Verificação
packages/redcode/src/session/stall.tsé puro, então cada ramo tem teste sem timer, sem fiber e sem provider: 9 testes.bun typechecknos 31 pacotes.Ainda sem cobertura
Chamadas ao modelo fora de um passo — compactação, título, resumo — não passam por este handle e continuam sem vigilância. Está anotado no plano como trabalho seguinte, junto com o prazo por ferramenta, que é a próxima tranche que você escolheu.
https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.