Conversation
…meouts Long-running shell commands could hang forever: exec.CommandContext only signals the direct shell child, so a grandchild that inherited the output pipes (e.g. a spawned daemon or pipe) survived the shell and blocked Wait indefinitely. There was also no way to bound a single invocation. - Unix: run the shell in its own process group (Setpgid) and SIGKILL the whole group on cancellation; set WaitDelay=5s so Wait always returns. - Windows: cancel via taskkill /T /F to walk the PowerShell process tree, with the same WaitDelay cap. - bash tool: add a per-call "timeout" argument. Absent = the 10m global default, "0" = unlimited, invalid values return an error result. - Note: gofmt -w applied to internal/tool/implementations.go to remove a pre-existing trailing-whitespace line inside the SYSTEM DIRECTIVE block (whitespace-only, flagged during Step 1). Tests: internal/tool/shell_timeout_test.go covers per-call timeout, "0" = unlimited, invalid timeout, global timeout expiry, and kill.
Subagents could run forever and stuck tools could block the session with
no way to bound or stop them.
- Subagent time budget: default 24h (--subagent-timeout), also settable
via config.json "subagent_timeout"; precedence flag > config >
default, invalid config values warn and fall back to 24h. "0" =
unlimited.
- Per-spawn budget: spawn_subagent gains an optional "timeout"
argument overriding the global value for that run ("0" = unlimited).
- Idle watchdog: activity tracking (stream progress, in-flight tools,
nested spawns) per subagent; --subagent-idle-timeout (15m, 0 = off)
emits SubagentIdleEvent with a transcript probe of the last activity;
--subagent-idle-kill-after (0 = notify only) escalates to a two-stage
kill: cancel the in-flight tool first, then cancel the agent run.
- Killable in-flight tools: a session kill hook lets a blocked shell call
be cancelled mid-wait instead of hanging the cancel path.
- TUI surfaces idle notifications for the affected subagent.
Tests: config resolution and round-trip, spawn timeout parsing, idle
watchdog transitions and escalation, executor in-flight kill hook.
Add a compact "Subagent control" section to the quickstart covering --subagent-timeout (24h default, 0 = unlimited, config.json subagent_timeout), --subagent-idle-timeout (15m, 0 = off), --subagent-idle-kill-after (0 = notify only), the per-spawn timeout argument of spawn_subagent, and the per-call timeout argument of bash.
Owner
|
I like the thought behind it, seems valuable. Note that things that directly affect the LLM performance need to be evaluated with care as thus I will likely proceed with #130 first. Also please make sure to note the latest comment in #127, or I can push a commit removing that part for now and then merge. As noted there we can always come back to it. |
Emasoft
added a commit
to Emasoft/late-cli
that referenced
this pull request
Sep 21, 2026
Union of PR mlhher#131 (feat/subagent-control @ 2d96f26, main-based) into the local/full stack (mlhher#127 resilient streaming + mlhher#129 OTP gate + mlhher#130 bash timeouts). Both feature sets are kept and both test suites pass. Per-file union decisions: - cmd/late/main.go: adopted incoming's budget resolution stack (24h DefaultSubagentTimeout, config.json subagent_timeout precedence via flag.Visit + ResolveSubagentTimeout, per-spawn timeout override via effectiveSubagentBudget) and the nested-spawn busy tracking (BeginNestedSpawn/EndNestedSpawn + parent heartbeat around child.Execute, SetContext(runCtx), child SetIdlePolicy). Kept local/full's transcript writer + cause classification: the classification now uses the EFFECTIVE budget and gained an idle-kill cause (child IdleKillReason() non-empty) that renders as "idle: killed by the harness idle watchdog (<probe summary>)" ahead of the user-cancel case. Flags: union --subagent-timeout (default 24h from appconfig), --subagent-idle-timeout, --subagent-idle-kill-after plus all local/full flags (--bash-timeout, --continue-project, OTP, ...); SetIdlePolicy applied to root and children. - internal/orchestrator/base.go: union of event hardening (non-blocking progress sends via trySendProgress + droppedEvents counter, blocking terminal/ChildAdded sends) with the activity-aware idle watchdog (lastActivity/inFlightTools/nestedSpawns/oldestToolStartAt, activityMiddleware outermost, two-stage tool-kill then agent-kill, injectable tick, SetIdlePolicy/MarkActivity/IdleKillReason). MarkActivity fires in both Execute and run() stream callbacks; the idle event is emitted non-blocking (select/default). - internal/executor/executor.go: harness-note delivery kept; per-call toolCtx + SetInFlightToolCancel/Clear and the "tool cancelled by the harness idle watchdog" result path added. inFlightKill is captured BEFORE the per-call toolCancel() and gates the harness note, so a watchdog tool-kill (failure-shaped "Command failed with exit code -1" from the shell SIGKILL) never asks the coder to report back; a real shell failure or a per-call timeout still gets the note. - internal/tool/implementations.go: single ShellTool.Execute with per-call timeout resolution (resolveShellTimeout: absent=global, 0/negative=unlimited, invalid=error result), global 10m default wired to --bash-timeout via SetShellTimeout, timeout message uses the effective bound. Kept local/full's harness-note-free error path (no duplicated coder sandwich) and the otp_code parameter; Parameters now declare both otp_code and timeout. - internal/tool/shell_command_unix.go, shell_command_windows.go: identical mechanism on both sides (process-group kill + WaitDelay); incoming's comments adopted. - internal/config/config.go: PermissionMode constants + SubagentTimeout entry coexist; ResolvePermissionMode and ResolveSubagentTimeout both kept. - internal/common/interfaces.go: RetryEvent + RecoveryEvent (local/full) and SubagentIdleEvent + ActivityMarker (incoming) kept together. - internal/tui/update.go: SubagentIdleEvent status-line case added to the hardened update loop. - internal/tool/shell_timeout_test.go: local/full's de-flaked timeout/ grandchild/pgrep tests plus incoming's per-call timeout and resolveShellTimeout tests. - docs/quickstart.md: incoming Subagent Control section; Common Flags row updated to the 24h default. Integration test added: TestExecuteToolCalls_WatchdogToolKillDoesNotAttachNote guards the union of the in-flight hook with the harness note. Gates: go build ./... ok, go vet ./... ok, go test ./... -race -count=1 all packages ok, gofmt clean on touched files, ./install-dev.sh check ok.
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.
Description of Changes
Four features giving the user and the orchestrator explicit control over subagent lifetime and tool call duration:
Subagent time budgets (default 24h)
--subagent-timeout(default24h;0= unlimited), also settable persistently assubagent_timeoutinconfig.json. Precedence is flag > config > default; invalid config values warn and fall back to the 24h default.Per-spawn budget on
spawn_subagenttimeoutargument ofspawn_subagent(e.g."45m","2h";"0"= unlimited; omitted = the global value). Invalid values return an error result instead of silently changing behavior.Idle watchdog
--subagent-idle-timeout(default15m;0= off) fires aSubagentIdleEventincluding a transcript probe of the subagent's last activity, surfaced in the TUI.--subagent-idle-kill-after(default0= notify only) escalates true idleness to a two-stage kill: the in-flight tool is cancelled first, then the agent run.Per-call tool timeouts + shell hardening
bashtool accepts a per-calltimeoutargument (absent = the 10m global default;"0"= unlimited; invalid = error result). Shells run in their own process group (UnixSetpgid+ group SIGKILL; Windowstaskkill /T /F) withWaitDelay=5s, so grandchild processes that inherit the output pipes can no longer hangWaitforever, and cancellation always kills the whole tree. A session kill hook makes in-flight tool calls cancellable mid-wait, which is what lets the idle watchdog (and the user) stop a stuck tool.Independence
This PR is independent of #127/#129/#130 — based on main; textually overlaps with #130 (shell hardening, runner budget): merge one, rebase the other.
Behavior changes
0disables); runs exceeding it are cancelled.--subagent-idle-kill-after); by default idle subagents are only notified about, never killed.bashinvocations default to a 10m per-call bound; cancellation kills the whole process group.Test plan
go test ./... -race -count=1full suitego build ./...;GOOS=windows go build ./internal/tool/;go vet ./...;gofmt -lclean on all changed filesinternal/tool/shell_timeout_test.go(per-call timeout,"0"unlimited, invalid timeout, global timeout expiry and group kill),internal/tool/subagent_test.go(per-spawn timeout parsing),internal/config/config_test.go(subagent_timeoutresolution precedence, invalid values, JSON round-trip),cmd/late/main_test.go(explicit--subagent-timeoutflag reporting),internal/orchestrator/base_idle_test.go(idle watchdog transitions, idle event, escalation to kill),internal/executor/inflight_test.go(in-flight tool kill hook)Known limitations
internal/pluginpackage has known environmental failures on main (3 HOME/XDG-dependent plugin tests, plus a flakyTestRunHook_ProcessGroupKillsChildrenOnCancel) — reproduced on this branch unchanged and unrelated to these features.Contributor License Agreement (CLA)
To accept your code, we legally need you to agree to our CLA so we can maintain the project's Business Source License (BSL) and future open-source transitions.
xbetween the brackets like this:[x])