From 3b2becc6380aa2b03ca26aad8314c92c9bb10c1d Mon Sep 17 00:00:00 2001 From: Lexus2016 Date: Tue, 25 Aug 2026 14:59:14 +0200 Subject: [PATCH 01/11] fix: a turn that strands a background task must not report success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit subtype:'success' is the one rung the auto-continue ladder does not cover. A run that started something with run_in_background and then ended its turn saying it would wait and continue reports a clean end_turn, so the ladder — which fires only on NON-success — never saw it. Nothing resumes a headless `claude -p`: the process exits and takes the background shell with it. The chat printed "Done" over work that had not happened, and the continuation the agent promised could never arrive. run-continuation.js holds both post-run decisions, pure so they can be pinned without a server or an SSH host: - shouldNudgeBackgroundWait() spends ONE run harvesting the task, and only for a launch in the last run. Detection is structural (a Bash call carrying run_in_background), never textual — user-facing prose is written in the UI language, so an English regex is dead on a French install. - describeTurnBudgetAnomaly() names an error_max_turns that did not come from our budget. Measured on CLI 2.1.231 a run capped at N reports num_turns === N + 1, so a stop at 3 of 50 is a cap imposed elsewhere and "raise Max turns" is actively wrong advice (#67 follow-up). BACKGROUND_TASK_INSTRUCTION states the constraint in the system prompt, so the harvest run is the second line of defence rather than the first. Also: the SSH auto-continue path now logs (it logged nothing at all, on either branch) and names the budget and what was spent, which is what the local CLI loop has always done. --- CHANGELOG.md | 39 ++++++++++++ CLAUDE.md | 40 +++++++++++- package.json | 2 +- run-continuation.js | 94 +++++++++++++++++++++++++++ server.js | 105 ++++++++++++++++++++++++++++--- test/ask-user-question.test.js | 7 ++- test/run-continuation.test.js | 112 +++++++++++++++++++++++++++++++++ 7 files changed, 387 insertions(+), 12 deletions(-) create mode 100644 run-continuation.js create mode 100644 test/run-continuation.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7165033c..8f5ccb05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## Unreleased + +A turn that walked away from a background job now finishes the work instead of +printing "Done" over it, and an `error_max_turns` that did not come from this +chat's budget is named as such rather than sending the user to the wrong dial. + +### A turn that strands a background task is not a finished turn + +- **The gap: `subtype:'success'` is the one rung the auto-continue ladder does not + cover.** A run that started something with `run_in_background` and then ended its + turn saying it would wait and continue reports a clean `end_turn`. Nothing resumes + a headless `claude -p` — the process exits and takes the background shell with it — + so the chat printed `✅ Done` over work that had not happened, and the promised + continuation could never arrive. +- **Detection is structural, never textual.** A `Bash` call carrying + `run_in_background` is the same JSON on every install; user-facing prose is written + in the UI language, so a regex over English phrases like "I'll check back" is dead + on a French or Ukrainian one. `run-continuation.js` holds the rule, pinned by + `test/run-continuation.test.js`. +- **One harvest run, and only for a launch in the LAST run.** An early launch the + agent went on to collect is not a stranded task, and the cap keeps a false positive + costing exactly one short run. +- **The system prompt states the constraint up front** — collect a background job's + result inside the same turn, because there is no later moment in which to get it. + The rescue is the second line of defence, not the first. + +### An `error_max_turns` that is not ours says so (#67 follow-up) + +- **"Raise Max turns" is wrong advice when that is not the cap being hit.** Measured + against CLI 2.1.231, a run capped at N reports `num_turns === N + 1`, so a genuine + exhaustion lands AT the cap. A stop at 3 turns against a 50-turn dial is a limit + imposed somewhere else — a different CLI version, a `settings.json`, a hook on the + machine the agent runs on — and the message now says that instead. +- **The remote auto-continue notice names the budget and what was spent.** The local + CLI loop has done this since it was written; the SSH one said only "resuming on + remote", which is why diagnosing the report needed a round trip. +- **The SSH auto-continue path logs.** It previously logged nothing at all, on either + the max-turns or the non-success branch. + ## 7.9.0 A remote chat can no longer get stuck for good, a broken local Claude CLI install diff --git a/CLAUDE.md b/CLAUDE.md index a2e410df..21a4b814 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ docker compose up -d docker compose logs -f claude-chat ``` -No linting and no build step configured. `npm test` chains 68 test files under `test/`: 18 DOM-less render/UI-logic tests (`test/render/*.test.mjs`, run through `node --test`) plus 50 plain-`node` suites in `test/` covering the overload detector, env load order, multi-agent results, terminals, bots, telegram, updates, kanban scheduling, i18n completeness, the config precedence resolver plus its secret masking, usage-limit detection, the filesystem path guard (including the SVG sandbox header and the symlink rule on the `@`-mention search endpoints) plus the tunnel-blocks-terminal rule, WS session re-subscription, the SSH remote CLI-session import, the live engine pane / interactive-prompt watchdog, the cross-project global workspace aggregation, the rule that an SSH credential never leaves the server process, the Windows command-quoting oracle, the auth token lifecycle, the multi-agent dependency scheduler (waves, plan sanitising, and the rule that a failure warning must survive dep-context truncation), the SSH stream parser's three guards, the SSH run's termination guarantee (`ssh-termination.test.js` drives `ClaudeSSH.send()` through a fake ssh2 `Client` in `require.cache` and asserts `onDone` fires EXACTLY once on every ending — a missed one hangs the chat forever, a doubled one re-emits stderr) and the recovery contract of "Restart Session" (`session-restart.test.js` boots a real server against a fake `claude` that never exits, then pins that a restart ABORTS that turn and releases the session instead of refusing), the remote non-interactive shell environment (`remote-env.test.js`, which runs the generated prelude through real `bash -lc`: it must parse, print nothing on stdout, and never end on a false test — the caller chains `&& claude …` behind it), the remote CLI-list framing parser, the bot inbox's SQL seam (`bot-inbox.test.js` pins that `from_bot AS "from"` keeps the exact key `planInboxDelivery` reads — rename one without the other and every letter is silently retired as malformed), the one-time config/.env migration onto CCS_CONFIG_PATH and the mid-task clarification delivery contract on the subscription engine (`interrupt-delivery.test.js` — pins that the tmux injection block sits BEFORE the poll loop's completion `break`, that draining does not imply delivery, that a failed paste is re-queued and warns non-terminally, and that the task runner passes the same callbacks the chat path does), the CLAUDE.md / AGENTS.md discovery rules (`agents-md.test.js`, which also pins that AGENTS.md reaches the subprocess as `--append-system-prompt` and never as `--system-prompt`), and the remote file browser's three guard layers (`remote-files.test.js` runs the generated POSIX script through a real `/bin/sh` against a temp tree that contains symlinks OUT of the project; `remote-files-api.test.js` boots a server against a fake remote via `CCS_REMOTE_EXEC_HOOK` and drives `/api/files` the way the SPA does), the editor deep links (`editor-links.test.js` pins the two URI shapes literally — the browser link puts `vscode-remote` in the AUTHORITY and the CLI argument puts it in the SCHEME, and collapsing the two silently breaks one path; `editor-open-api.test.js` boots a real server with `PATH` pointed at an EMPTY directory, which both makes the `opened:'client'` fallback deterministic and guarantees the suite never launches an editor window on a developer's desktop), and the new-chat defaults chain (`chat-defaults.test.js` pins the pure resolver — the built-ins are asserted to be exactly what the SPA hardcoded before #58, and the choice lists to be exactly the toolbar's `data-v` sets and `MODEL_MAP`'s aliases; `chat-defaults-api.test.js` boots a real server in a throwaway `APP_DIR` and pins that a project writes back a SPARSE override object — a five-key snapshot passes every other assertion in that file and still breaks the feature). On the render side, `tables.test.mjs` also pins the ReDoS bound in renderMd step 3.4, `xss.test.mjs` runs 24 adversarial payloads end-to-end, and `forged-tokens.test.mjs` covers the case where user text contains the renderer's own placeholder control bytes, and `pane-font.test.mjs` pins the clamp DIRECTION of `_fitEnginePaneFont` (a wide engine pane may only shrink; a narrow split pane must be allowed to grow). `script-scope.test.mjs` pins which `