fix: add spawn timeout and WaitAll deadline to prevent infinite hangs
- Spawn goroutines now have a 30-minute hard timeout (spawnTimeout) - WaitAll accepts a timeout parameter so heartbeat cleanup doesn't block forever if a subagent gets stuck - CancelTask stores per-task cancel func for explicit cancellation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
354497f8a8
commit
3a231766a4
2 changed files with 22 additions and 7 deletions
|
|
@ -947,7 +947,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
defer func() {
|
||||
if opts.Background {
|
||||
if agent.SubagentMgr != nil {
|
||||
agent.SubagentMgr.WaitAll()
|
||||
agent.SubagentMgr.WaitAll(35 * time.Minute) // slightly above spawnTimeout
|
||||
}
|
||||
if agent.IsInWorktree(opts.SessionKey) {
|
||||
commitMsg := "heartbeat: auto-save"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
)
|
||||
|
||||
// spawnTimeout is the hard upper bound for a single spawn goroutine.
|
||||
// MaxIterations × HTTP timeout provides the soft limit; this is a safety net.
|
||||
const spawnTimeout = 30 * time.Minute
|
||||
|
||||
type SubagentTask struct {
|
||||
ID string
|
||||
Task string
|
||||
|
|
@ -124,11 +128,11 @@ func (sm *SubagentManager) Spawn(
|
|||
|
||||
sm.reporter.ReportSpawn(taskID, label, task)
|
||||
|
||||
// Start task in background with a detached context.
|
||||
// Start task in background with a detached context that has a hard timeout.
|
||||
// The spawned goroutine must outlive the parent (e.g. heartbeat session)
|
||||
// which may finish before the subagent completes.
|
||||
// The cancel func is stored on the task so CancelTask() can stop it.
|
||||
spawnCtx, spawnCancel := context.WithCancel(context.Background())
|
||||
spawnCtx, spawnCancel := context.WithTimeout(context.Background(), spawnTimeout)
|
||||
subagentTask.cancel = spawnCancel
|
||||
sm.wg.Add(1)
|
||||
go func() {
|
||||
|
|
@ -377,10 +381,21 @@ func (sm *SubagentManager) buildPresetRegistry(preset Preset, writeRoot string)
|
|||
return registry
|
||||
}
|
||||
|
||||
// WaitAll blocks until all spawned subagent goroutines have finished.
|
||||
// Used by heartbeat cleanup to avoid destroying worktrees while subagents are still running.
|
||||
func (sm *SubagentManager) WaitAll() {
|
||||
// WaitAll blocks until all spawned subagent goroutines have finished
|
||||
// or the timeout expires. Returns true if all goroutines finished,
|
||||
// false on timeout.
|
||||
func (sm *SubagentManager) WaitAll(timeout time.Duration) bool {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
sm.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
return true
|
||||
case <-time.After(timeout):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// CancelTask cancels the context for a running subagent task.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue