You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: bound long-running TUI agents with a wall-clock ceiling so a busy-but-stuck agent can't run unbounded
The idle-timeout reaper resets on every PTY chunk, but Claude Code repaints
its (Ns · esc to interrupt) working counter ~1x/sec while any tool/API call is
in flight — including one stuck retrying a stalled Bedrock/network operation.
A busy-but-stuck agent therefore keeps lastOutputAt advancing forever, so
idle-reap never fires and the run had no ceiling at all: agent-b1c56083's
/do:next --swarm claim-issue orchestration churned its counter for 98 minutes
before Claude Code's own internal 'Operation timed out' finally stopped it.
Had the CLI not self-terminated, the agent would have run unbounded, holding
its lane, blocking the app cooldown, and leaking a shell session.
The long-running agent TUI path now arms an absolute wall-clock backstop from
prompt submission (default 3h, provider-configurable via tuiMaxRuntimeMs),
mirroring the one-shot runner's existing hardTimeoutTimer. On fire it salvages
as success if .agent-done exists, else finalizes as a needs-manual-finish
max-runtime-timeout failure with a captured worktree diff.
Copy file name to clipboardExpand all lines: .changelog/NEXT.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,6 +8,7 @@
8
8
- **A live agent TUI no longer gets reaped mid-paste.** The idle-timeout reaper only tracked `lastOutputAt` (PTY output), not client input, so pasting a large multi-line command into a running agent's session could sit in a silent reflow/commit window with no echo yet — indistinguishable from "idle" to the 5s poll. If the session happened to already be near its idle threshold, the reaper killed the PTY mid-paste, showing "[Session removed externally]" in the Shell page. The idle timer in `agentTuiSpawning.js` now also checks a new `shell.js` export, `getLastInputAt()`, and skips the reap while input arrived within the last 15s. This is deliberately keyed on input *recency*, not "is a Shell viewer attached": a regular (non-external) session's socket stays bound after its viewer navigates away — only external one-shot `tuiPromptRunner.js` runs get released via `shell:release-views` — so an attachment-based guard would have permanently suppressed idle-complete for any agent glanced at once in the Shell UI. (`server/services/shell.js`, `server/services/agentTuiSpawning.js`)
9
9
-**Swarm claim-issue runs now write the completion sentinel so the task actually finishes.** In `/do:next --swarm` mode the orchestrator ships every issue via a PR and never touches the working tree, so it was skipping the `.agent-done` completion sentinel — leaving the CoS task hanging with no summary as if the run never ended. Phase C of the swarm directive now explicitly instructs the orchestrator to write the sentinel (with a run summary of the issues claimed, their PRs, and merge outcomes) after the serialized merges, while still skipping the per-agent `/simplify`/push steps. (`server/services/cosTaskGenerator.js`)
10
10
-**[issue-2190] OpenCode Ollama providers now accept any --model flag.** The static `OPENCODE_CONFIG_CONTENT` lacked a models map, so OpenCode rejected every model with "not valid" and the agent sat idle. The config is now built dynamically at spawn time, declaring the provider's configured models (plus the model being run) under `provider.ollama.models` with bare ids — the exact shape OpenCode's `@ai-sdk/openai-compatible` provider validates `--model ollama/<id>` against. Applied across every OpenCode spawn path (CoS CLI/TUI agents, the toolkit CLI/TUI runners, and the light fire-and-collect CLI run), and a migration backfills existing installs' `data/providers.json` so a configured model's map is persisted too. Fixes the TUI eval path for local Ollama models.
11
+
- **A busy-but-stuck TUI agent can no longer run unbounded — a wall-clock ceiling now backstops the idle reaper.** The idle-timeout reaper resets on every PTY chunk, but Claude Code repaints its `(Ns · esc to interrupt)` working counter roughly once a second while *any* tool/API call is in flight — including one stuck retrying a stalled Bedrock/network operation. A "busy-but-stuck" agent therefore keeps `lastOutputAt` advancing forever, so idle-reap never fires and the run had no ceiling at all: a real `/do:next --swarm` claim-issue orchestration churned its counter for 98 minutes (2026-07-06, agent-b1c56083) before Claude Code's *own* internal "Operation timed out" finally stopped it — had the CLI not self-terminated, the agent would have run unbounded, holding its lane, blocking the app's cooldown, and leaking a shell session. The long-running agent TUI path now arms an absolute wall-clock backstop from prompt submission (default 3h, provider-configurable via `tuiMaxRuntimeMs`), mirroring the one-shot runner's existing `hardTimeoutTimer`. If it fires with no `.agent-done` sentinel present, the run finalizes as a needs-manual-finish FAILURE (`max-runtime-timeout`) with the same "check for open or merged-but-uncleaned PRs" guidance as the merge-queue/review-loop idle-timeout paths, capturing any uncommitted worktree diff for post-mortem first; if the sentinel *is* present the run is salvaged as success. (`server/services/agentTuiSpawning.js`, `server/lib/tuiHandshake.js`, `server/lib/aiToolkit/providers.js`, `server/lib/aiToolkit/validation.js`, `server/lib/validation.js`)
11
12
-**[issue-2191] TUI agents with zero file changes are now marked failed instead of silently succeeding.** A TUI agent that idled out after showing working indicators but made no uncommitted file changes was previously finalized as `success: idle-complete` — deleting the worktree and any evidence of what happened. Now the idle-complete path checks `git status` in the worktree: if the tree is clean, it finalizes as `failure: idle-no-changes` with the TUI buffer tail as error context. Any uncommitted diff is also captured to `worktree-diff.txt` in the agent archive dir before cleanup, so post-mortems can see what (if anything) was left behind.
12
13
-**[issue-2193] Concurrent agent spawns no longer race on git worktree creation.** When two tasks for the same app spawned in the same evaluation tick, the first `git worktree add` grabbed git's per-repo worktree lock and the second failed with a lock error — blocking that task with `blockedCategory: worktree-failed` and leaving an orphan branch (`cos/task-…/agent-…`) with no worktree. Worktree creation now serializes per source repo through a single promise tail (the `issueWriteTail` pattern), so same-tick adds queue instead of colliding; `git worktree add` also retries a few times with backoff on lock errors to absorb residual external contention; and on final failure the partially-created branch is deleted (`git branch -D` + `git worktree prune`) so orphan branches stop accumulating. (`server/services/worktreeManager.js`)
13
14
- **Scheduled improvement tasks with no configured model now follow the active provider's default instead of a stale hardcoded one.** When a self-improvement or managed-app improvement task had no explicit model in its schedule config, the generator pinned a hardcoded `claude-opus-4-5-20251101` into the task — a model that had since dropped out of the `claude-code-tui` provider config, so the scheduler spawned `claude --model claude-opus-4-5-20251101` even after the provider default moved to Opus 4.8. Since a per-task model pin is honored above the provider default (it's passed straight through to the CLI), that stale literal overrode the configured default on every scheduled run. The generator no longer pins a literal: with no configured model it leaves the task's model unset, so model selection resolves the active provider's tier/default model at spawn time and always uses a model the provider actually lists. An explicit per-task or per-schedule model pin still wins as before. (`server/services/cosTaskGenerator.js`)
emitLog('error',`Failed to finalize TUI agent ${agentId} at max-runtime salvage: ${err.message}`,{ agentId });
930
+
});
931
+
return;
932
+
}
933
+
// Capture any uncommitted work for post-mortem before cleanup, then
934
+
// fail with a needs-manual-finish message — a stuck orchestrator may
935
+
// have left PRs/worktrees behind (same recovery guidance as the
936
+
// merge-queue/review-loop idle-timeout paths).
937
+
captureWorktreeDiff(cwd,agentDir).catch(()=>{});
938
+
finish({
939
+
success: false,
940
+
exitCode: 124,
941
+
error: `TUI agent exceeded its max runtime of ${Math.round(tuiConfig.maxRuntimeMs/60000)}min — the provider/CLI likely hung (a stalled request keeps the working counter repainting so the idle reaper never fires); check for open or merged-but-uncleaned PRs and finish them manually.`,
942
+
reason: 'max-runtime-timeout',
943
+
}).catch(err=>{
944
+
emitLog('error',`Failed to finalize TUI agent ${agentId} at max-runtime: ${err.message}`,{ agentId });
945
+
});
946
+
},tuiConfig.maxRuntimeMs);
947
+
}
904
948
};
905
949
906
950
// Confirms the TUI actually received the paste before we submit. The
@@ -1233,7 +1277,8 @@ export async function spawnTuiAgent({
0 commit comments