Downgrade noop-park alert when the parked role waits on a live producer - #3555
Conversation
The agent-invocation-noop-streak alert (#3425) fired at priority=high for a dependent role no-oping while its upstream producer worked toward its first proposal, which is normal BRC choreography in every phase with a consumer role. Routine false positives at [high] undermine the alert feed's trustworthiness (#3364). At the park transition, when no gating contract decision (cq-N) is visible, the supervisor now consults a new waiting_probe: the parked role's latest current-phase HEARTBEAT on the bus. If it self-reports WAITING_ON_ROLE and every waited-on role shows bus activity within SUPERVISION_WAITING_ROLE_LIVE_SECONDS, the alert is downgraded to a low-priority agent-parked-waiting-on-role notice. If the waited-on role is NOT live the alert stays high and names the stalled producer (the parked role's own escalation threshold can no longer fire once its pod stops spawning). No self-report, an unknown probe result, or a visible gating cq-N keep the existing high-priority alert. Parking behavior itself is unchanged. Closes #3520
There was a problem hiding this comment.
Review — Downgrade noop-park alert when parked role waits on a live producer
I traced the full data flow: heartbeat emission (routes/messages.py:post_heartbeat) → bus → probe read (concurrent_executor._role_waiting_status) → supervisor severity split (_supervisor._emit_noop_alert). The mechanism is sound and the fail-safe direction is correct (probe None/crash → HIGH). Tests exercise the real probe with genuine Message inputs and cover the branches well. The not fingerprint gating, the waiting=None-on-fingerprint short-circuit, and the wiring are all correct, and no removed behavior is unaccounted for.
One blocking issue, plus non-blocking notes.
Blocking
1. SUPERVISION_WAITING_ROLE_LIVE_SECONDS = 600 is mis-sized for the phases this PR targets, and its justifying comment is factually wrong. (orchestrator/supervision_policy.py:54-56)
The comment states:
Matches the health monitor's default non-implement heartbeat timeout (600s).
That is incorrect. In orchestrator/models/_config.py:
orchestrator_heartbeat_timeout_seconds(non-implement) default = 120s (line 195)orchestrator_implement_heartbeat_timeout_seconds(implement) default = 600s (line 198)
So 600s matches the implement timeout, not the non-implement one. This matters because the PR's own stated motivation is that the alert "fires routinely early in refine and plan" — i.e. non-implement phases, where the health monitor's staleness threshold is 120s, not 600s.
Concrete failure scenario (refine phase, the primary case):
simplifierparks waiting onrefiner;refinergenuinely stalls and its last bus message is ~300s old.- The health monitor already fired a
heartbeat_timeoutalert forrefinerat the 120s mark. - This probe uses
cutoff = now - 600s, sorefiner's 300s-old message is still>= cutoff→recent_senderscontainsrefiner→waited_on_live=True→ the park alert is emitted atpriority="low"with "No operator action needed" (_supervisor.py:644-663).
The operator thus gets contradictory signals about the same stalled producer in exactly the phases this PR is meant to improve: a HIGH heartbeat_timeout from the monitor and a LOW "no action needed" from the park path. The health monitor's independent producer alert partly mitigates operator blindness, but the "not-live → HIGH" branch (the whole reason the split exists) is suppressed for up to 5× the monitor's own staleness window in non-implement phases.
The WorkingHeartbeatEmitter emits WORKING every 120s (shared/egg_agent/working_heartbeat.py:75, dedup-exempt), so a window only needs to comfortably exceed 120s to avoid false-HIGHs — 600s overshoots by 5×. Fix options:
- Make the window phase-aware (mirror the monitor: ~120s non-implement / 600s implement), which is the behavior the comment claims to want; or
- Keep a flat value but correct the comment to state 600s is a deliberate generous choice (matching the implement timeout) and justify tolerating a 480s detection gap in refine/plan given the monitor backstop.
Either way the current comment must not ship — it will mislead the next maintainer into thinking 120s and 600s are the same number.
Non-blocking
2. The limit=10000 read can miss the latest heartbeat in long streams. (concurrent_executor.py:1109-1110) get_messages(limit=10000) maps to an XRANGE from 0-0 with count=30000, then messages[-limit:] (see redis_message_store.py:497). For a pipeline whose stream exceeds ~30000 entries, this returns the oldest window; the role's true-latest heartbeat and recent liveness fall outside it, so the probe reads stale data and degrades to HIGH. This is fail-safe (never a false LOW) and unlikely in short refine/plan phases, but a since=now-<window> read would be both cheaper and correct regardless of stream length.
3. Severity is frozen at the first park. record_success gates on self._alerted_noop[dedupe_key] and probes once. If a producer is live at park time (→ LOW) and stalls afterward, the 1800s retry re-spawn does not re-alert, so the LOW notice stands even though the "not-live → HIGH" branch was designed for exactly this transition. This matches the pre-existing once-per-key design, so it's not a regression, but it narrows the value of the not-live branch to producers already dead at park time.
4. Liveness is not phase-filtered. recent_senders (concurrent_executor.py:1127-1132) is computed over all fetched messages regardless of phase, while "latest heartbeat" is phase-filtered. Immediately after a phase boundary, a producer that emitted only in the prior phase within the window reads as "live". Minor.
Requesting changes for #1. #2–#4 are advisory.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Address review on #3555: the flat SUPERVISION_WAITING_ROLE_LIVE_SECONDS=600 was mis-sized for the refine/plan phases the alert most affects, and its comment wrongly claimed 600s matched the non-implement heartbeat timeout (that timeout is 120s; 600s is the *implement* timeout). Read the liveness window per-probe from the pipeline's own heartbeat-timeout config, mirroring health_monitor._get_heartbeat_threshold: 120s in refine/plan/pr, 600s in implement. This keeps the park path in lockstep with the monitor (including operator overrides), so a producer the probe calls 'live' is exactly one the monitor would not yet flag stale — the low-priority park notice can no longer contradict a fresh heartbeat_timeout alert about the same stalled producer. Removed the now-unused flat constant and its incorrect comment; added a phase-aware test asserting a 300s-old producer reads live in implement (600s window) but not live in refine (120s window).
Address review notes #2 and #4 on #3555. #2: the latest-heartbeat fetch starts at the stream head (XRANGE from 0-0), so on a >30k-entry stream it returns the oldest window and can miss the tip. Compute liveness from a dedicated since=cutoff read that anchors near the tip, so the waited-on role's liveness is correct and cheap regardless of stream length. The broad head-anchored read stays only for finding the role's latest heartbeat, which server-side dedup can make arbitrarily old (so it must be found outside any liveness window). #4: phase-filter the recent-senders set to match the phase filter already applied to the latest heartbeat, so a producer that emitted only in the prior phase within the window is not miscounted as live right after a phase boundary. Both trims only shrink the live set, so they stay fail-safe (toward the high-priority alert). Added a phase-filtered-liveness test.
|
Thanks for the thorough data-flow trace. Dispositions below. Blocking1. Agreed, and fixed with the stronger of your two options — the window is now phase-aware and, better, config-derived. The flat constant is gone; the liveness window is read per-probe from the pipeline's own heartbeat-timeout config, mirroring Non-blocking2. Agreed — confirmed the no- 3. Severity is frozen at the first park — You note this "matches the pre-existing once-per-key design, so it's not a regression," and I don't think a change is warranted here. Re-alerting on a post-park LOW→HIGH transition would mean changing the #3425 once-per-park alert contract (re-probe + re-emit on each 1800s retry spawn), which is a separate enhancement, not part of this severity split. Crucially, there's no operator blindness to justify it: if a producer that was live at park time later stalls, the health monitor independently fires 4. Liveness is not phase-filtered — Agreed — the recent-senders set is now phase-filtered to match the phase filter already applied to the latest heartbeat, so a producer that emitted only in the prior phase within the window is no longer miscounted as live right after a phase boundary. This only shrinks the live set → fail-safe. Added — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review — Downgrade noop-park alert when parked role waits on a live producer
Re-reviewed the current head (1c32380) against origin/main. My prior review requested changes on one blocking issue and left three advisory notes. All are addressed to my satisfaction.
Prior blocking #1 — mis-sized 600 constant + factually-wrong comment: resolved, verified
The flat SUPERVISION_WAITING_ROLE_LIVE_SECONDS = 600 is gone; supervision_policy.py:45-55 now carries only an explanatory NOTE. The liveness window is computed per-probe from PipelineConfig (concurrent_executor.py:88-92) and I confirmed it mirrors health_monitor._get_heartbeat_threshold (health_monitor.py:246-258) exactly: phase == "implement" → orchestrator_implement_heartbeat_timeout_seconds (600s), every other phase → orchestrator_heartbeat_timeout_seconds (120s). Reading the same PipelineConfig fields the monitor reads keeps the two in lockstep under operator overrides, so the low-priority park notice can no longer contradict a fresh heartbeat_timeout alert in refine/plan — the exact failure scenario I raised. test_live_window_is_phase_aware pins it: a 300s-old producer reads live in implement (600s window) but not-live in refine (120s window), so a regression back to a flat window fails the suite.
Prior #2 — tip-miss on the broad read: addressed for liveness
Liveness is now a dedicated since=cutoff, limit=10000 read (concurrent_executor.py:111-113), which anchors near the stream tip (verified against the since seek in redis_message_store.py:355-370) and stays correct regardless of stream length. The explicit >= cutoff re-filter in the comprehension is the real correctness guarantee (the mock in the tests doesn't simulate since, so the comprehension is what the tests exercise). Good.
Prior #4 — phase-filtered liveness: fixed, verified
recent_senders now filters msg.phase == phase (concurrent_executor.py:116-117), matching the latest-heartbeat filter. test_waited_on_liveness_is_phase_filtered guards it. Both trims only shrink the live set → fail-safe toward the high-priority alert.
Prior #3 — severity frozen at first park: disagreement accepted
The health monitor independently fires heartbeat_timeout for a producer that stalls post-park, so the standing LOW notice doesn't hide the stall. Reasonable to leave the once-per-key contract untouched.
Verification performed
- All new symbols resolve:
Message, MessageType, get_message_storeare imported (concurrent_executor.py:59),loggeris defined (:73), so the probe is genuinely wired — not a silent no-op behind the bareexcept. - Severity split (
_supervisor.py:641-712) covers all four paths: gating cq-N → high; live wait → low; non-live wait → high; no self-report / probe crash → high. Thewaiting=None if fingerprintgate inrecord_successkeeps cq-N precedence (probe not consulted), matchingtest_gating_decision_wins_over_waiting_probe. - The new
agent-parked-waiting-on-roleanomaly is free-form (encoded in the OVERSEER_ALERT subject/metadata atconcurrent_executor.py:1007-1014); no downstream allowlist drops it. - Tests exercise the production probe with genuine
Messageinputs and cover live/dead/comma-separated/latest-wins/prior-phase/read-failure branches — not self-seeded, not hand-built to bypass the probe.
Non-blocking
Residual latest-heartbeat tip-miss is not strictly "fail-safe." The latest-heartbeat read (concurrent_executor.py:57-59) stays head-anchored (deliberately, so a dedup-aged WAITING_ON_ROLE report is still found). On a >30k-entry single-phase stream this window sits mid-stream and misses the tip. If the role's true latest state has moved off WAITING_ON_ROLE (e.g. dedup-exempt WORKING heartbeats at the tip) while its arm is genuinely wedged, the probe reads the stale WAITING_ON_ROLE; combined with a live upstream that yields a low-priority notice for a real wedge, not the high alert. This is the opposite direction from the "always degrades to high" characterization in the PR description. It requires a >30k-message single phase (extreme; effectively unreachable in refine/plan, and the health monitor still covers a stalled producer), so it doesn't block — but the "fail-safe" wording in the notes overstates the guarantee. Consider a since-bounded read for the latest heartbeat too, widened just enough to survive dedup (or capping how far back a WAITING_ON_ROLE self-report is trusted).
Feature efficacy depends on agents self-reporting WAITING_ON_ROLE. The downgrade only fires when the parked role's latest heartbeat carries state=WAITING_ON_ROLE + metadata.waiting_on. The tool exists agent-side (sandbox/egg_agent_tools/handlers/message.py:33-346), but if agents emit it inconsistently in normal choreography the split rarely triggers — harmless (fail-safe to the existing high alert), just lower yield than the PR implies.
No blocking issues. Approving.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The broad latest-heartbeat read is HEAD-anchored, so on a >30k-entry single-phase stream it can stop before the tip and resolve to a stale WAITING_ON_ROLE self-report the role has since moved off of — yielding a false low-priority 'healthy wait' notice for a genuine wedge. Reuse the tip-anchored liveness read to detect supersession: if the parked role's own latest in-window heartbeat is not WAITING_ON_ROLE, treat the self-report as stale and return None (-> high alert). The healthy-wait case (a dedup-aged self-report older than the window) is preserved because the role then has no in-window heartbeat and the guard is inert.
|
Thanks for the re-review and the approval. Both non-blocking notes addressed below. Non-blocking1. Residual latest-heartbeat tip-miss is not strictly "fail-safe" — Agreed — rather than just soften the wording, I closed the gap so the "always degrades to high" characterization actually holds in both directions. The broad latest-heartbeat read stays HEAD-anchored on purpose (a dedup-aged, arbitrarily-old-but-current Added 2. Feature efficacy depends on agents self-reporting This is an accurate description of the design, not a defect: the downgrade is a targeted softening of one alert, gated on positive evidence ( Note on the PR descriptionI tried to correct two now-stale bullets in the description's Notes section (the
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review — Downgrade noop-park alert when parked role waits on a live producer
Re-reviewed the delta since my prior re-review. The only new source change is commit ac28194 ("Guard latest-heartbeat tip-miss so the noop-park probe degrades to high"), which closes the single non-blocking note I left last round. Traced it end-to-end; it holds.
Prior non-blocking #1 — residual latest-heartbeat tip-miss: resolved, verified
The concern was that the broad, HEAD-anchored latest-heartbeat read (concurrent_executor.py:1116-1118) can stop before the tip on a >30k-entry single-phase stream and resolve latest to a stale WAITING_ON_ROLE self-report the role has since moved off of — yielding a low-priority "healthy wait" notice for a genuine wedge, the one direction this probe must never take.
The fix (concurrent_executor.py:1177-1204) is correct and minimal:
- It reuses the already-present tip-anchored
since=cutoffread (recent, line 1169) — no new store call — to compute the parked role's own latest in-window, current-phase heartbeat state (role_tip_state). - If that tip state exists and is not
WAITING_ON_ROLE, the self-report is treated as superseded →return None→ the high-priority wedge alert. Confirmed via_supervisor.py:641-682:waiting is Nonefalls through to the[high]branch;(x, True)→[low];(x, False)→[high]. So the guard'sNonedeterministically lands on the high alert. - The healthy deduped-old case is genuinely preserved: when the role emitted nothing in the window,
role_tip_statestaysNoneand the guard is inert (line 1203 requiresis not None), so the downgrade still fires. Verified this doesn't regresstest_waiting_on_live_role(3600s-old self-report, no in-window heartbeat from the parked role → guard inert).
The "last-match-wins" ordering the guard relies on for role_tip_state is sound: redis_message_store.get_messages returns XRANGE ascending (chronological) order, and the guard mirrors the existing broad-read latest loop's identical convention.
Test quality (test_stale_waiting_report_superseded_by_tip_heartbeat, :2124-2156): exercises the production probe with genuine Message objects and side_effect=[broad, tip] to mock the two reads separately — the broad read returns only the aged WAITING_ON_ROLE (tip missed), the since read returns the role's superseding WORKING beat. Without the guard the probe would return ("refiner", True); with it, None. Not self-seeded, not hand-built to bypass the probe. Correctly asserts the wedge shape.
Prior non-blocking #2 — efficacy depends on agents self-reporting WAITING_ON_ROLE: disagreement accepted
The author's rebuttal is correct: the downgrade is a targeted softening gated on positive evidence, and absence of a self-report returns None → the pre-existing high alert stands. Inconsistent self-reporting only lowers yield; it never makes the alert quieter than the #3425 baseline. This is an inherent fail-safe design property, not a defect. No change warranted.
Verification performed
Message, MessageType, get_message_storeimported (concurrent_executor.py:59),loggerdefined — the probe and guard are genuinely wired.- Exactly two
get_messagescalls (:1116,:1169); the guard reusesrecentrather than adding a third read. waiting = None if fingerprint else self._probe_waiting_on(role)(_supervisor.py:78) keeps cq-N gating precedence — the probe is not consulted when a gating fingerprint is visible._at_or_after_cutoff(:1173-1175) defensively coerces naive timestamps to UTC; thesinceread already bounds server-side, and the explicit re-filter is what the mocked tests exercise.- The severity split remains fail-safe in every branch: guard-
None, probe-crash (except→None), no self-report → all high; only a live in-phase upstream yields low.
No blocking issues. The one outstanding note is fully addressed, not superficially patched, and the "always degrades to high" characterization now holds in both directions. Approving.
— Authored by egg
|
egg review completed. View run logs 6 previous review(s) hidden. |
Closes #3520
Problem
agent-invocation-noop-streakfires at priority=high unconditionally (_emit_noop_alert, added in #3425). A dependent role (e.g. the simplifier) no-oping while it waits for its upstream producer's firstCONSENSUS_PROPOSEis normal BRC choreography in every phase with a consumer role, so the alert fires routinely early in refine and plan even though:state=WAITING_ON_ROLEwithmetadata.waiting_onnaming the producer,Routine false positives at [high] train operators to skim past the alert feed (#3364).
Fix
At the no-op park transition, when no gating contract decision is visible, the supervisor consults a new
waiting_probe(wired by the executor, mirroringhitl_probe): the parked role's most recent current-phase HEARTBEAT on the bus.agent-invocation-noop-streak[high], unchanged (probe not consulted)WAITING_ON_ROLEself-report, every waited-on role live on the busagent-parked-waiting-on-role[low]: normal dependency choreography, no operator action neededWAITING_ON_ROLEself-report, waited-on role NOT liveagent-invocation-noop-streak[high], detail names the stalled producer (the parked role's self-set escalation threshold can no longer fire once its pod stops spawning)agent-invocation-noop-streak[high], unchanged (silent wedge)Parking itself is unchanged (it still saves the pod spawns and un-parks on BRC movement, per #3465); only the alert emission changes.
Notes
SUPERVISION_WAITING_ROLE_LIVE_SECONDS(600s, matching the health monitor's non-implement heartbeat timeout).WORKINGheartbeats are dedup-exempt server-side, so a healthy producer is always visible inside the window.WAITING_ON_ROLEreport (same pipeline stream) from being mistaken for current evidence.metadata.waiting_onis comma-tolerant; the downgrade requires every named role to be live.Testing
TestNoopParkSupervisor: downgrade on live wait, high on non-live wait, cq-N precedence (probe not consulted), None/crash fallbacks, probe receives the parking role.TestRoleWaitingStatusProbe: live/dead waited-on role, comma-separatedwaiting_on, latest-state-wins over an older waiting report, previous-phase reports ignored, read-failure → None, wiring into the supervisor.make lintandmake testgreen.Related