fix(orchestrator): surface live event-loop pods in running-agent views (#3230) - #3260
Conversation
#3230) Under the orchestrator-owned BRC event loop (#3164, now unconditional) role pods are on-demand one-shots that the loop deliberately never persists into phase_exec.agents — the consensus tracker plus live-Job labels are its only sources of truth. So the persisted agent list is empty even while role pods are Running, which both running-agent consumers read as "0 running agents": * get_status.running_agents → a blind dashboard. * concurrent.agents (the overseer's stall-duration source) → the overseer sees the split "3 BRC agents blocking, 0 running agents" against the populated tracker block and composes a false `phase stalled` alert. Backfill both views from the labels that ARE authoritative when the persisted list is empty: * routes.pipelines._live_event_agents() reconstructs the running-pod cohort from live Job labels (LIVE_POD_STATUSES, slice-scoped when a slice_id is supplied). _get_concurrent_status falls back to it. * mcp_tools._build_status_snapshot backfills running_agents from the /status endpoint's live concurrent.agents block — one server-side source of truth. Empty stays empty when no pod is live, so legitimate between-spawn quiescence is not misreported as a running cohort (or a stall).
There was a problem hiding this comment.
Review: surface live event-loop pods in running-agent views (#3230)
I traced both consumer paths end-to-end and verified the fix is functional in its real execution environment. No blocking issues. Approving with a few non-blocking observations.
What I verified
Root-cause framing is accurate. Under the now-unconditional orchestrator-owned event loop (#3164), phase_exec.agents is permanently empty for BRC concurrent phases, so both running-agent consumers read an empty list. Confirmed both consumers:
- Overseer:
egg-orch pipeline status --json→get_pipeline_statusroute (routes/pipelines.py:4001) →_get_concurrent_status(pipeline, slice_id=...)→ readsconcurrent.agentsinoverseer/monitor.py:1099,1619. ✓ Backfill reaches the overseer. - Dashboard:
get_status→_build_status_snapshot→_live_running_agents_fallback→ HTTP/api/v1/pipelines/{id}/status(route confirmed atroutes/pipelines.py:3928) → server-side_get_concurrent_status. ✓ One server-side source of truth.
No cross-module silent no-op. _live_event_agents emits status: "running" for every live pod; the dashboard fallback filter (a.get("status") == "running", mcp_tools.py:1557) is a pass-through, not a synthetic-key dead-end. The producer's output survives the consumer's filter.
Symbols/model attributes all in scope. LABEL_PIPELINE_ID, LABEL_SLICE_ID, _LIVE_POD_STATUSES, _get_spawner are module-level imports; ContainerInfo exposes status/agent_role/container_id/started_at as used. The spawner.backend.list_containers(labels=...) pattern matches existing production helpers (_count_live_pods_for_pipeline, _slice_agents_alive), so the label-query mechanism is proven in the orchestrator process.
Best-effort degradation is correct and non-regressive. Both query failures degrade to [], identical to pre-fix behavior. LIVE_POD_STATUSES = {pending, creating, running} confirmed, so terminal pods in the TTL window are correctly excluded → between-spawn quiescence reads as idle, not a stall.
Tests exercise the production path. Both suites feed inputs at the real boundary (list_containers / _make_request) and call the real _get_concurrent_status / handle_tool_call — no hand-built fixtures bypassing the helper, no self-seeding goldens, names match assertions. I ran the 8 new tests: all pass. The no_fallback_when_persisted_present test (asserts call_count == 2, no /status request) is a good guard on the short-circuit.
Non-blocking observations
-
Pending/Creating pods reported as
"running"could in theory soften LLM-based stall detection for a pod stuck unschedulable inPending. This is not a regression: the deterministic_check_incomplete_consensus_stallkeys offconsensus.blocking_agents+ proposal age, not the running-agent set, so a genuinely stuck role (never ACKs) is still caught regardless of this backfill. The tradeoff is documented and reasoned about in the docstring. Worth keeping in mind if stall-classification heuristics ever start trustingrunning_agentsas a liveness signal. -
_live_running_agents_fallbackswallows exceptions with no log line (mcp_tools.py:1554bareexcept Exception: return []), whereas the routes-side_live_event_agentslogs atdebugwithpipeline_id/error. Consider a matchinglogger.debugfor symmetry — a repeatedly-failing/statusround-trip would otherwise be invisible. -
Scoping is pipeline-wide in practice for both live consumers. Neither the overseer (
egg-orch pipeline status) nor the dashboard (_live_running_agents_fallbackrequests/statuswithout?slice_id=) passes a slice, so for a slice-DAG implement phase the backfill is a cross-slice union. That's defensible for a "what's running" view, but the slice-scoped branch of_live_event_agentsis only reachable via an explicit?slice_id=caller — narrower than the PR description implies. Not a correctness issue. -
Duplicate role entries are possible if two live pods briefly share a role during a restart race (old pod not yet terminal). Consumers that build a role set dedupe naturally; a raw list view would show both. Cosmetic.
Solid, well-scoped fix with good regression coverage.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Mirror routes.pipelines._live_event_agents, which logs at debug on a failed label query. The mcp_tools-side _live_running_agents_fallback previously swallowed exceptions silently, making a repeatedly failing /status round-trip invisible. Add a matching logger.debug for symmetry.
|
Thanks for the thorough end-to-end trace. Approval noted; here is the disposition of each non-blocking observation. 1. Pending/Creating pods reported as 2. 3. Scoping is pipeline-wide in practice for both live consumers — 4. Duplicate role entries possible during a restart race — — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review: surface live event-loop pods in running-agent views (#3230)
Re-reviewed the delta since my prior approval (cc38ccf → cfe67a4). The branch was squashed/rebased, so I traced the delta from the disposition comment and the live diff. No blocking issues. Approving.
Delta verified
The only change since my last review is the logger.debug line added to _live_running_agents_fallback's bare-except path (mcp_tools.py:1554-1561) — exactly what non-blocking observation #2 requested. It is correct:
- Kwargs-style call is consistent.
logger.debug("…", task_id=task_id, error=str(e))matches the established structlog pattern already used throughoutmcp_tools.py(logger.error("Tool call failed", tool=…, error=…)at line 1232, line 1420, line 1714) and mirrors the routes-side_live_event_agentsit's modeled on. In productionegg_loggingprovides a structlog logger that accepts kwargs; the stdlibget_loggerfallback is import-time-only and would affect every existing kwargs call equally — pre-existing, not introduced here. - Best-effort contract preserved. The log line is inside the
exceptand stillreturn []. Logging the failure does not change the degradation behavior; a repeatedly-failing/statusround-trip is now visible at debug instead of silent. - No new exception surface. The post-try line
concurrent.get("agents", []) or []is safe —concurrentis guaranteed a dict via… or {}.
Prior concerns confirmed addressed / reasonably dispositioned
- #2 (silent swallow) — fixed in
cfe67a4as described above. - #1, #3, #4 — non-blocking; declined with sound reasoning (deterministic
_check_incomplete_consensus_stallkeys offconsensus.blocking_agents+ proposal age, notrunning_agents, so genuinely stuck roles are still caught; pipeline-wide scoping is intended for a "what's running" view; the duplicate-role window is a sub-second restart race that role-set consumers dedupe). I agree on all three.
Core fix intact
The production change I approved is unchanged: _live_event_agents (server-side reconstruction from live Job labels), the _get_concurrent_status backfill, and the _build_status_snapshot fallback. I re-confirmed task_id is URL-quoted once by the caller (mcp_tools.py:1580) before being passed to the helper, matching its docstring contract. Tests still exercise the real production path at genuine boundaries (list_containers / _make_request → real _get_concurrent_status / handle_tool_call) — no self-seeding goldens, no path-bypassing fixtures, and test names match their assertions. The no_fallback_when_persisted_present guard (call_count == 2) correctly pins the short-circuit.
Clean, well-scoped fix with good regression coverage.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
| egg is addressing review feedback... |
|
Thanks for the re-review and the clean re-approval at 1. Pending/Creating pods reported as 2. 3. Scoping is pipeline-wide in practice for both live consumers — 4. Duplicate role entries possible during a restart race — No further code changes — HEAD ( — Authored by egg |
|
egg feedback addressed. View run logs 4 previous review(s) hidden. |
Fixes #3230.
Reframing
#3230 was filed as a gating defect for #3164. But #3164 already merged (
ebce0086d) and removedEGG_EVENT_LOOP_OWNER/get_event_loop_owner— the orchestrator now unconditionally owns the BRC event loop. So this is no longer a pre-merge gate; it is live default behavior in production. There is no longer a "mode" to be aware of, which simplifies the fix.Root cause
The event loop deliberately persists nothing (
event_loop.py: "the tracker plus live-Job labels are the only sources of truth"). The up-front spawn path appendedAgentExecutionrecords intophase_exec.agents; the event-loop path never does. Sophase_exec.agentsis permanently empty under the event loop, and both running-agent consumers read that one empty list:get_status.running_agents(mcp_tools._build_status_snapshot) → blind dashboard.concurrent.agents(routes.pipelines._get_concurrent_status, the overseer's stall-duration source) → the sandboxed overseer sees the split signature "3 BRC agents blocking, 0 running agents" against the populated tracker block and composes a falsephase stalledalert.The deterministic stall checks (
incomplete_consensus_stall) already key off the tracker, not the running-agent set, so they are not the false-positive source — this is squarely about the running-pod view.Fix
Live-derive the running-pod cohort from the labels that are authoritative, when the persisted list is empty:
routes.pipelines._live_event_agents(pipeline_id, slice_id)— reconstructs running agents from live Job labels via the existing spawner/list_containerspattern. Filters toLIVE_POD_STATUSES(Pending/Creating/Running) so terminal pods lingering in the TTL window don't count; slice-scoped when aslice_idis supplied so a slice-DAG implement phase reports its own slice's pods._get_concurrent_statusfalls back to it. → fixes the overseer.mcp_tools._build_status_snapshot— backfillsrunning_agentsfrom the/statusendpoint's liveconcurrent.agentsblock. One server-side source of truth. → fixes the dashboard.Empty stays empty when no pod is live, so legitimate between-spawn quiescence (a role exited, successor not yet triggered) reads as "no running agents" — the normal idle state, not a stall. Both backfills are best-effort: a failed/absent label query degrades to
[](no regression vs. today).Tests
8 new regression tests:
test_concurrent_status.py::TestLiveEventAgentBackfill(5) — backfill fires only when persisted list empty, filters terminal pods, maps role/elapsed, slice-scopes the label query, degrades to[]on query failure.test_mcp_tools.py::TestGetStatusSyncHandler(3) —running_agentsbackfilled from/statuswhen persisted empty; no fallback (and no extra request) when persisted present; quiescence stays empty.make test(full suite, expanded by the changeset narrower): 18,050 passed. The only 2 failures are the pre-existingreap-stale-egg-imagessafety-gate tests (exit 127) — known btrfs-host environment noise, root-caused in #3222, unrelated to this change.