fix(experiments): give the spawn-thrash detector a real progress signal#704
Conversation
SpawnThrashDetector reset only when completed_tasks increased, so it was blind to an agent that had claimed a task and was actively working it (logging context/artifacts/decisions, reporting progress) but had not yet finished. A single contract task routinely takes longer than the 5-poll (~2.5 min) fuse, so the detector tore down healthy-but-slow runs and blamed a "BLOCKED dependency" that did not exist. Feed observe() a monotonic, cumulative `activity` tally assembled from the work signals an agent emits while genuinely working — completed_tasks plus context_requests, artifacts_created, decisions_logged, blockers_reported. Any increase resets the thrash streak. in_progress is deliberately excluded: it flickers 0->1->0 on claim-and-exit churn (the lease lifecycle) and would mask the very thrash this detector exists to catch; the cumulative work counters only ever rise. Also rewrite the teardown message to state the observation (0 completions, no logged work) and list the real candidate causes (deferred MCP tools, skill mismatch, BLOCKED dependency) instead of asserting one misleading one. Tests: rename the observe() keyword, refresh docstrings, and add test_logged_work_resets_without_a_completion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @lwgray's task —— View job PR Review Complete ✅Overall Assessment: This is a well-implemented fix that addresses a real operational issue. The solution is elegant, focused, and maintains backward compatibility while significantly improving the spawn-thrash detection capability. Todo List:
✅ Strengths
🔍 Technical ReviewCode Quality - Excellent
Architecture - Sound
Activity Signal Composition - Well Designedactivity = (
done
+ int(status.get("context_requests", 0))
+ int(status.get("artifacts_created", 0))
+ int(status.get("decisions_logged", 0))
+ int(status.get("blockers_reported", 0))
)This captures all legitimate agent work while avoiding the Test Quality - Good
🔍 Potential ConcernsMinor Issues
Edge Cases Handled Well
📊 Performance Considerations - Good
🔒 Security Considerations - No Issues
💡 Suggestions for Enhancement
📝 Documentation Quality - ExcellentThe improved teardown message is much more informative:
✅ Final Recommendation: APPROVEThis PR successfully addresses the core issue with a clean, well-tested solution. The implementation is sound, the test coverage is appropriate, and the change is safe. The improved spawn-thrash detection will significantly reduce false positive teardowns and associated cost waste. Risk Level: Low - purely additive change with comprehensive test coverage |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ffe15a3d79
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # See SpawnThrashDetector.observe. | ||
| activity = ( | ||
| done | ||
| + int(status.get("context_requests", 0)) |
There was a problem hiding this comment.
Wire context requests before using them as progress
With this new term, a get_task_context call is supposed to reset the thrash streak, but the counter never increments today: get_task_context only emits log_agent_event("context_requested", ...), and a repo-wide rg record_context_request shows no caller for LiveExperimentMonitor.record_context_request. In a healthy run where a claimed agent requests context and then works while the active layer still has another TODO task (so to_spawn > 0), activity remains flat and the detector can still fast-fail after spawn_thrash_polls. Please either record context requests into the monitor or don't rely on this field.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ab70949. You're right — record_context_request existed but had no caller. get_task_context now records it into the active monitor (mirroring log_decision), so context_requests actually increments and counts as forward progress.
| + int(status.get("context_requests", 0)) | ||
| + int(status.get("artifacts_created", 0)) | ||
| + int(status.get("decisions_logged", 0)) | ||
| + int(status.get("blockers_reported", 0)) |
There was a problem hiding this comment.
Count report_task_progress as forward progress
This activity total omits the normal keep-alive/progress path: the assigned-agent workflow explicitly tells workers to call report_task_progress at 25/50/75%, but get_experiment_status only exposes task counts and these logged-work counters, so intermediate progress updates leave activity unchanged. When one task in the active layer is genuinely advancing via report_task_progress while another active-layer TODO keeps to_spawn > 0, the detector still accumulates idle-spawn polls and can tear down a healthy run after the fast-fail window. Please expose/count progress updates or include task progress changes in this tally.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ab70949. Added a progress_updates counter + record_progress() to LiveExperimentMonitor, exposed it in get_status (plus the mlflow metrics + text summary), and call it from report_task_progress. It is the earliest signal — fires on the first 25% report, well before any artifact lands — so a claimed agent advancing via progress reports keeps activity rising even while another active-layer TODO keeps to_spawn > 0. Folded into the runner activity tally alongside the now-wired context_requests.
… thrash signal Addresses two PR #704 review findings: the `activity` tally the spawn-thrash detector reads leaned on signals that never moved during the window that matters. 1. context_requests was dead. get_task_context emitted a "context_requested" event but never called LiveExperimentMonitor.record_context_request, so the counter stayed 0. Wire the call (mirrors log_decision). 2. report_task_progress (the 25/50/75% heartbeat) was not counted at all. get_experiment_status exposes only task counts + logged-work counters, so an agent advancing via progress reports left `activity` flat. Add a progress_updates counter + record_progress() to the monitor, expose it in get_status (plus the mlflow metrics + text summary), and call it from report_task_progress. progress_updates is the earliest of these signals -- it fires as soon as a claimed agent reports, well before the first artifact -- closing the claim->first-artifact gap that could otherwise fast-fail a healthy run. Both are folded into the runner's activity tally. Tests: monitor heartbeat counters + get_status exposure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @lwgray's task —— View job PR Review Complete ✅Overall Assessment: This is a well-implemented fix that addresses a real operational issue. The solution is elegant, focused, and maintains backward compatibility while significantly improving the spawn-thrash detection capability. ✅ Strengths
🔍 Technical ReviewCode Quality - Excellent
Architecture - Sound
Activity Signal Composition - Well Designedactivity = (
done
+ int(status.get("progress_updates", 0))
+ int(status.get("context_requests", 0))
+ int(status.get("artifacts_created", 0))
+ int(status.get("decisions_logged", 0))
+ int(status.get("blockers_reported", 0))
)This captures all legitimate agent work while avoiding the Test Quality - Good
🔍 Addressing Codex ConcernsThe automated codex review raised two important points that I investigated: P1: Context Request Wiring ✅ ResolvedCodex concern: Context requests weren't being recorded. P1: Progress Updates ✅ Already ImplementedCodex concern: 🔍 Potential ConcernsMinor Issues
Edge Cases Handled Well
📊 Performance Considerations - Good
🔒 Security Considerations - No Issues
💡 Suggestions for Enhancement
📝 Documentation Quality - ExcellentThe improved teardown message is much more informative:
✅ Final Recommendation: APPROVEThis PR successfully addresses the core issue with a clean, well-tested solution. The implementation is sound, the test coverage is appropriate, and the change is safe. The improved spawn-thrash detection will significantly reduce false positive teardowns and associated cost waste. Risk Level: Low - purely additive change with comprehensive test coverage |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab7094902c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| _progress_monitor = get_active_monitor() | ||
| if _progress_monitor and _progress_monitor.is_running: | ||
| _progress_monitor.record_progress( |
There was a problem hiding this comment.
Count only accepted progress reports as activity
When a stale or non-holder agent calls report_task_progress, this increments progress_updates before the stale-completion and lease-holder guards can reject the request. Since spawn_agents.py now treats any progress_updates increase as forward progress, a recovered/zombie worker that keeps sending rejected updates can keep resetting the spawn-thrash detector while newly spawned agents are still not making claimable progress. Move this heartbeat after the ownership/validation gates that prove the update is accepted, or avoid counting rejected updates.
Useful? React with 👍 / 👎.
| _monitor = get_active_monitor() | ||
| if _monitor and _monitor.is_running: | ||
| _monitor.record_context_request(agent_id=agent_id, task_id=task_id) |
There was a problem hiding this comment.
Count only successful context fetches as activity
This records a context request before get_task_context verifies that the task exists or that the context system can return a successful response. In runs where workers repeatedly call this tool with an invalid task id or when context is disabled, context_requests still rises even though the tool returns an error, and the runner will treat those failed calls as forward progress and suppress the spawn-thrash fast-fail. Record the counter only on successful context responses, or separate failed requests from the activity signal.
Useful? React with 👍 / 👎.
Fixes #703.
Problem
SpawnThrashDetectorreset its idle-spawn counter only whencompleted_tasksincreased, so it was blind to an agent that had claimed a task and was actively working it (logging context/artifacts/decisions) but hadn't finished within the 5-poll (~2.5 min) fuse. A singlecontract_firsttask takes minutes, so the detector tore down healthy-but-slow runs and blamed a "BLOCKED dependency" that didn't exist.Fix
observe()now takes a monotonic, cumulativeactivitytally the runner assembles from the signals an agent emits while genuinely working:Any increase resets the thrash streak.
in_progressis deliberately excluded — it flickers 0→1→0 on claim-and-exit churn (the lease lifecycle) and would mask the very thrash the detector exists to catch; the cumulative work counters only ever rise.The
to_spawn == 0hold behavior is unchanged, so this is purely additive protection: it can only prevent false teardowns, never suppress a real one (a genuinely doomed run logs no work →activitystays flat → still fires).Better teardown message
Before:
After:
Tests
tests/unit/experiments/test_spawn_controller.py: renamed theobserve()keyword (completed=→activity=), refreshed docstrings, and addedtest_logged_work_resets_without_a_completion(a mid-task artifact log with zero completions resets the streak). 22 passed.Note
Complements #702 (worker-side
--strict-mcp-configfix). The lease-expiry system is the per-task recovery mechanism; this is the run-level cost-control backstop — see #703 for the full comparison and the future "expose lease-renewal freshness" option.🤖 Generated with Claude Code