purgeTerminalAgents removes agents whose goroutine is still alive — done channel never closes
Severity: Low-Medium
File and Lines
internal/subagent/manager.go, lines 1110-1141 (purge), 783-790 (Complete early-return), 658-662 (Cancel)
Problem Description
purgeTerminalAgents checks only Status (Completed/Failed/Cancelled) to decide if an agent can be safely removed. But Cancel() sets Status to Cancelled without closing the done channel — it relies on the goroutine's Complete() call to do that. If the agent is purged from the map before its goroutine calls Complete(), the Complete() lookup fails (ok=false → early return), closeDone() is never called, and done stays open forever.
Trigger Scenario
- Agent A is Cancel'd (Status=Cancelled, EndedAt=T1) but goroutine still running (slow LLM shutdown)
- 20+ other agents complete with EndedAt > T1
- Watchdog ticker fires
purgeTerminalAgents — A is oldest terminal → deleted
- A's goroutine returns, calls
Complete("A") → m.agents["A"] not found → early return
closeDone() never called → done channel permanently open
Why it's real
purgeTerminalAgents does not check whether the goroutine has finished (goroutineStarted or done closed)
- The
goroutineStarted field exists (line 102) but is not used by purge
Cancel() explicitly does NOT close done (line 658 comment)
Complete() early-returns if agent not in map (line 786-789)
Mitigation
- Requires 20+ terminal agents simultaneously (rare)
CancelAll has timeout fallback (won't permanently deadlock)
CancelAll only collects Running/Pending agents in doneChs, so already-cancelled agents aren't waited on
Fix Suggestion
In purgeTerminalAgents, skip agents whose done channel is not yet closed:
if sa.done != nil {
select {
case <-sa.done:
// goroutine confirmed exited — safe to purge
default:
continue // goroutine still running — skip
}
}
purgeTerminalAgents removes agents whose goroutine is still alive — done channel never closes
Severity: Low-Medium
File and Lines
internal/subagent/manager.go, lines 1110-1141 (purge), 783-790 (Complete early-return), 658-662 (Cancel)Problem Description
purgeTerminalAgentschecks onlyStatus(Completed/Failed/Cancelled) to decide if an agent can be safely removed. ButCancel()sets Status to Cancelled without closing thedonechannel — it relies on the goroutine'sComplete()call to do that. If the agent is purged from the map before its goroutine callsComplete(), theComplete()lookup fails (ok=false→ early return),closeDone()is never called, anddonestays open forever.Trigger Scenario
purgeTerminalAgents— A is oldest terminal → deletedComplete("A")→m.agents["A"]not found → early returncloseDone()never called →donechannel permanently openWhy it's real
purgeTerminalAgentsdoes not check whether the goroutine has finished (goroutineStartedordoneclosed)goroutineStartedfield exists (line 102) but is not used by purgeCancel()explicitly does NOT closedone(line 658 comment)Complete()early-returns if agent not in map (line 786-789)Mitigation
CancelAllhas timeout fallback (won't permanently deadlock)CancelAllonly collects Running/Pending agents indoneChs, so already-cancelled agents aren't waited onFix Suggestion
In
purgeTerminalAgents, skip agents whosedonechannel is not yet closed: