Skip to content

fix(core): bound silent agent turns and fail stalled children from wait_for_agents - #1283

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
fix/agent-stall-guard
Open

fix(core): bound silent agent turns and fail stalled children from wait_for_agents#1283
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
fix/agent-stall-guard

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

A child agent could go silent forever: its model stream tripped the 300s idle guard (_with_idle_timeout), the transient-retry path in _run_cycle logged replaying turn (attempt 1/5) and re-issued the turn — and then nothing. Zero events for 10h+ until an external watchdog killed the whole run. The idle guard only bounds gaps between stream events once the model stream is iterating; a hang during request setup of the re-issued turn, inside _aclose(), or in tool transport is unbounded. With no per-agent liveness, the child stayed running and its parent sat in wait_for_agents indefinitely (each 300s wait timed out and was re-entered, since the child still looked active).

Two layers, both driven by one setting STRIX_AGENT_STALL_TIMEOUT (RuntimeSettings.agent_stall_timeout, default 1800s, 0 disables):

1. Per-turn stall guard in _run_cycle — the whole run-event stream is now wrapped:

async for event in _with_stall_guard(stream, stall_timeout, agent_id):
    coordinator.touch(agent_id)
    ...

_with_stall_guard does asyncio.wait_for(events.__anext__(), timeout) over stream.stream_events(); on timeout it stream.cancel(mode="immediate") and raises TimeoutError, which the existing transient-retry branch already treats as retryable — so a wedged replay is abandoned and replayed again (up to _MAX_TRANSIENT_MODEL_RETRIES), then surfaces as crashed instead of hanging.

2. Liveness reaping from a waiting parentAgentRuntime.last_activity (monotonic) is refreshed on every run event (coordinator.touch), on registration, and on every status change. AgentCoordinator.reap_stalled(max_silence, under=me) walks only the caller's descendants (_subtree_order_locked) and, under the coordinator lock, marks any still-running agent silent past the threshold failed (with an error string) and collects its task; tasks are cancelled after the lock is released. The status check and the write happen in the same critical section, so an agent that emitted an event or finished in the meantime is never overwritten. The cancelled agent's own loop finaliser then delivers its terminal notice to the parent as usual.

wait_for_agents calls it before parking and again on wait timeout, with threshold agent_stall_timeout + _STALL_REAP_GRACE_S (300s) so layer 1 gets to recover first. Results appear as stalled_agents in the tool output; the timeout note tells the model to treat that list as the failure notice and not wait on those agents again.

waiting agents are never reaped (a parked agent is silent by design), the caller itself is excluded, and agents outside the caller's subtree are untouched.

Tests: tests/test_agent_stall_guard.py covers hung-turn abandon+replay, exhaustion → crashed, disabled guard, parent reaping a silent child (and next wait returning no_active_agents), a heartbeating child not being reaped, waiting children being left alone, and reaping staying inside the caller's subtree.

Note: tests/test_pricing.py::test_resolves_common_bare_model_names fails on main independently of this change (grok-4.5 now resolves via openrouter).

Link to Devin session: https://app.devin.ai/sessions/6d30fd146b6b417186c25f5909cdf587
Open in Devin Desktop: https://app.devin.ai/desktop/session/6d30fd146b6b417186c25f5909cdf587?variant=devin

…it_for_agents

The model-stream idle guard only covers the model call. A turn that wedged
anywhere else the run loop awaits between events (a re-issued request that
never opens, a hung stream close, tool transport) had no bound, so the agent
stayed running forever and a parent parked in wait_for_agents never returned.

- run cycle: abandon a turn that emits no run event for
  STRIX_AGENT_STALL_TIMEOUT (default 30m) and route it through the
  transient-retry path
- coordinator: track per-agent last_activity; reap_stalled fails and cancels
  agents silent past a threshold
- wait_for_agents: reap stalled children before parking and on timeout,
  reporting them as stalled_agents

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; the follow-up changes resolve the previously reported reaping-scope, stale-candidate, and failure-notice concerns without introducing a new actionable issue.

Summary

  • Adds a configurable per-turn stall timeout and retries abandoned streams through the existing transient-failure path.
  • Tracks agent activity and reaps only silent, running descendants of the waiting caller.
  • Surfaces reaped agents directly in wait_for_agents results.
  • Adds focused tests for retry, exhaustion, disabling, heartbeat, waiting-state, and subtree behavior.

Comment thread strix/core/agents.py Outdated
Comment thread strix/core/agents.py Outdated
Comment thread strix/tools/agents_graph/tools.py Outdated
…lures atomically

Reap only descendants of the waiting agent, re-check status and silence
under the coordinator lock at the moment the failure is written, and stop
claiming the terminal notice is already queued in the wait result.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants