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
Open
fix(core): bound silent agent turns and fail stalled children from wait_for_agents#1283devin-ai-integration[bot] wants to merge 2 commits into
devin-ai-integration[bot] wants to merge 2 commits into
Conversation
…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>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Contributor
…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>
Contributor
Author
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.
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_cycleloggedreplaying 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 stayedrunningand its parent sat inwait_for_agentsindefinitely (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,0disables):1. Per-turn stall guard in
_run_cycle— the whole run-event stream is now wrapped:_with_stall_guarddoesasyncio.wait_for(events.__anext__(), timeout)overstream.stream_events(); on timeout itstream.cancel(mode="immediate")and raisesTimeoutError, 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 ascrashedinstead of hanging.2. Liveness reaping from a waiting parent —
AgentRuntime.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-runningagent silent past the thresholdfailed(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_agentscalls it before parking and again on wait timeout, with thresholdagent_stall_timeout + _STALL_REAP_GRACE_S (300s)so layer 1 gets to recover first. Results appear asstalled_agentsin the tool output; the timeout note tells the model to treat that list as the failure notice and not wait on those agents again.waitingagents 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.pycovers hung-turn abandon+replay, exhaustion →crashed, disabled guard, parent reaping a silent child (and next wait returningno_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_namesfails onmainindependently of this change (grok-4.5now 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