Fix #2264: replace consensus-timeout HITL decision with OVERSEER_ALERT - #2269
Conversation
The platform should not open a side-effect-bearing `choice` decision on its own initiative when consensus times out. Operators already have the levers (`cancel_task`, `restart_phase`, `provide_input`), and "Continue waiting" was the only correct answer in every documented misfire (#2243 post-mortem, #2245 force-kill cycle). Replace the two `_persist_hitl_decision` calls in `_handle_brc_consensus_timeout` with an `OVERSEER_ALERT` publication that carries the metadata schema specified in the issue (`anomaly_type`, `phase`, `blocking_agents`, `latest_proposal_at`, `latest_heartbeat_at`, `consensus_timeout_minutes`, `priority`, plus `slice_id` when present). Critical-blocker escalation maps to `priority=high`; the no-tracker / tracker-import-error fallback maps to `priority=medium`. The existing `_emit_event(CONSENSUS_TIMEOUT)` on the fallback path is preserved. The SDLC skill's existing `OVERSEER_ALERT` flow surfaces these as non-blocking notifications (Check agent logs / Acknowledge / Cancel pipeline). No skill changes needed. Sequenced as step 3 of #2243 (step 1 progress gate landed in #2254; step 2 per-phase defaults filed separately). Migration note: any operator-side automation watching `pending_decisions` for the strings "BRC consensus failure" / "Consensus not reached after N minutes" must migrate to scanning `recent_messages` for `OVERSEER_ALERT` with `metadata.anomaly_type == "consensus-timeout"`.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns.
This PR moves toward agent-mode principles by replacing a rigid binary choice HITL gate (Continue waiting / Accept current state / Abort phase) with a non-blocking OVERSEER_ALERT notification. That's a clear "prefer what over how" win — instead of forcing the operator into three pre-defined options (two of which silently destroyed work in every documented misfire), the platform just informs them and lets them use their existing tool surface (cancel_task / restart_phase / provide_input) at their own discretion.
The alert metadata (anomaly_type, phase, blocking_agents, latest_proposal_at, latest_heartbeat_at, consensus_timeout_minutes, priority, slice_id) is lightweight orienting context, not pre-fetching — exactly the kind of "small amounts of structured data that inform the task" the guide calls out as fine. It also has a legitimate downstream consumer (SDLC skill's id-based dedup and notification flow), so the structured shape is justified.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review
The migration from auto-HITL choice to OVERSEER_ALERT is the right protocol shape and the refactor is mostly clean. No blocking correctness or security issues. A handful of non-blocking issues below — primarily a subject-format mismatch with the SDLC skill's "Check agent logs" parser, plus some minor polish.
Non-blocking
1. Alert subject doesn't match the format the skill parser expects
skills/sdlc/SKILL.md:432 documents the alert subject format as <anomaly_type>: <agent_role> [<priority>] and tells the host to "extract the agent role from the alert subject" so it can call get_container_logs for the affected agent. This PR emits subjects of the shape consensus-timeout: <phase> [<priority>] — the second field is the phase (e.g. implement), not an agent role.
What that buys the operator when they pick "Check agent logs" (orchestrator/mcp_tools.py:2155):
filtered = [c for c in containers if c.get("agent_role") == agent_role]
if filtered:
containers = filteredagent_role="implement" matches no container, the filter falls through to the unfiltered list, and the operator gets logs for an arbitrary running container — not the blocking agent. Degraded UX rather than fully broken: the body has Blocking agents: …, and "Acknowledge" / "Cancel pipeline" still work. Worth either:
- putting the first blocking role in the subject (
consensus-timeout: <blocking_role> [<priority>]) and moving the phase into metadata/body only, or - relaxing the
SKILL.mdconvention to admit the consensus-timeout shape and parsingmetadata.blocking_agentsinstead of the subject.
The slice-cascade alert at orchestrator/routes/pipelines.py:10466 (slice-cascade-block: <slice_id>) has the same issue — that's a pre-existing wart, but worth deciding the convention now that two emitters have skipped the role field.
2. Body wording about provide_input is misleading post-#2264
orchestrator/routes/pipelines.py:9768:
"The pipeline continues to poll for convergence. If you want to "
"intervene, use `cancel_task` to stop the pipeline, `restart_phase` "
"to retry, or `provide_input` against any open agent decision."The whole point of this PR is that consensus timeout does not open a decision anymore. Telling the operator to provide_input "against any open agent decision" implies one was opened by this very alert. Either drop the provide_input clause or qualify it (if any other agent decision happens to be open).
Also worth noting: the post-timeout polling budget is bounded (post_timeout_budget = 3600 at pipelines.py:11538). "Continues to poll for convergence" reads as open-ended; in practice the operator has up to 60 minutes before containers are force-killed. Adding "(up to ~60 min before force-kill)" would set the right expectations.
3. blocking_agents list mismatch on the escalate path
The escalate path means the tracker found critical blockers. But the alert metadata writes through whatever the call site passed (consensus.get("blocking_agents", [])), which is the full unconfirmed-roles set including advisory blockers (peer_consensus.py:1485). The high-priority alert ends up listing advisory roles too, which dilutes the signal. The tracker's handle_timeout() already returns critical_blockers in _brc_timeout_result — _publish_consensus_timeout_alert could prefer that on the escalate branch.
4. _latest_active_role_heartbeat has no direct unit test
orchestrator/routes/pipelines.py:9682-9720 mirrors the heartbeat half of _check_brc_progress_gate but has no equivalent dedicated test. The four migration tests at test_pipelines_routes.py:69-253 all leave active_role_names unset, so _latest_active_role_heartbeat([]) short-circuits at the if not active_role_names: return None guard before exercising the health-monitor lookup, lock snapshot, or active-role filter. The integration test at test_consensus_polling.py:296 does pass through with active_role_names, but it doesn't mock health_monitor and only asserts on priority/blocking_agents — latest_heartbeat_at is invisible. The TestBrcProgressGate fixture at test_pipelines_routes.py:276 already has a _patch_health_monitor helper that would translate directly. Add at least: empty roles → None, recent heartbeat → datetime, only-inactive heartbeats → None, monitor unavailable → None.
5. exc_info=True inconsistency on the warning paths
_publish_consensus_timeout_alert's outer except logs with exc_info=True (pipelines.py:9821), but the proposal-lookup except at pipelines.py:9879 and the heartbeat-lookup except at pipelines.py:9716 only log error=str(e). If those throw, the traceback is lost — diagnosing why metadata is None would mean digging through orchestrator logs without a stack. Three lines that all do the same thing should log the same way.
6. Minor test/code polish
test_pipelines_routes.py:44—_capture_alerts(monkeypatch_target=None)parameter is never used. Drop it.pipelines.py:9602(_check_brc_progress_gatedocstring) andpipelines.py:11476-11478still read "auto-HITL decision" / "decision-15 / decision-17 fired"; semantically those are now alerts. Comments are historically correct but a future reader will look for HITL decisions in code that no longer creates them._handle_brc_consensus_timeoutkeeps thestore: StateStoreparameter asnoqa: ARG001. If the production call site can stop passing it (one-line edit atpipelines.py:11520), the parameter and the_make_store()test fixture both go away cleanly. The PR comment says it's kept "for call-site compatibility" — but the call site is internal, so that's a self-imposed constraint.
Verified working
_handle_brc_consensus_timeoutis called exactly once per_run_concurrent_phaseinvocation (pipelines.py:11515), and theif escalate / elif not _brc_handledbranches are mutually exclusive — at most one alert per timeout. The tracker'sis_timeout_handledidempotency keeps duplicate alerts off if the handler is re-entered.CONSENSUS_TIMEOUTaudit-event preservation on the no-tracker / import-error fallback is correct; the escalate path correctly skips the host emit becausePeerConsensusTracker.handle_timeoutalready emitsCONSENSUS_FAILUREinternally.MessageType.OVERSEER_ALERT == "OVERSEER_ALERT";to_role="all"andfrom_role="orchestrator"ride past the SDLC skill filter (which keys only ontype, notfrom_role, despite whatSKILL.md:410documents)._get_message_store()failure path (None factory) andadd_messagefailure path are both logged withexc_infoand don't crash the polling loop. Test coverage for both is in place.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- Subject now uses the first blocking-agent role (fallback to phase) so the SDLC skill's "Check agent logs" parser can extract a real role and call get_container_logs against the affected agent. - Escalate path narrows alert metadata.blocking_agents to the tracker's critical_blockers — advisory roles are excluded so the high-priority signal isn't diluted. - Body drops the misleading provide_input clause (no decision is opened by this alert post-#2264) and notes the ~60-min force-kill budget so "continues to poll" reads correctly. - Proposal-lookup and heartbeat-lookup warnings now log with exc_info=True so a stack trace survives — matches the publish helper's outer except. - Add direct unit tests for _latest_active_role_heartbeat covering empty roles, recent heartbeat, inactive-role filtering, monitor-unavailable, no-heartbeats, and the exc_info logging path. - Add escalate-path fallback test for the case where the tracker result omits critical_blockers. - Drop unused _capture_alerts(monkeypatch_target=None) parameter and refresh outdated "auto-HITL decision" / "decision-15 / decision-17 fired" comments to reflect the post-#2264 alert flow. - Docs (concurrent-execution.md, sdlc-pipeline.md) document the agent_role subject slot and drop provide_input from the timeout troubleshooting note.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
orchestrator/routes/pipelines.py grew due to this PR's new helpers (_publish_consensus_timeout_alert, _latest_active_role_heartbeat). orchestrator/peer_consensus.py and scripts/select_tests.py baselines reflect current main-branch sizes merged into the CI check commit.
Autofix tracking{"Lint/Custom Checks": 1} |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…olve scripts/file-size-allowlist.yaml
Conflict Resolution SummaryResolved merge conflicts with
Other files auto-merged cleanly (no conflict markers): Verification:
Please review: Nothing semantic changed — the conflict was purely about baseline byte/line counts in the file-size allowlist where this PR's — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns (re-review at fdef6072).
The delta since c259d35 addresses the other reviewer's general code-quality feedback (subject role slot, narrowed blocking_agents on escalate, body wording, exc_info=True on warnings, direct unit tests for _latest_active_role_heartbeat, doc clarifications). None of those changes alter the agent-mode design profile — the alert is still a non-blocking OVERSEER_ALERT with lightweight orienting metadata (anomaly_type, phase, blocking_agents, latest_proposal_at, latest_heartbeat_at, priority, slice_id), no pre-fetched diffs or logs, and the operator continues to use the existing tool surface (cancel_task / restart_phase) at their own discretion.
If anything, the subject-format fix (consensus-timeout: <agent_role> [<priority>] matching skills/sdlc/SKILL.md's convention so "Check agent logs" can extract a real role for get_container_logs) is a small positive — it preserves SDLC-skill routing without baking richer context into the alert itself.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review at fdef6072
The delta since c259d35 cleanly addresses every blocking and non-blocking item from my prior review. Behaviour is correct and the new test coverage is the right shape.
Previous feedback — addressed
- Subject role slot —
_publish_consensus_timeout_alertnow derivessubject_role = blocking_agents[0] if blocking_agents else phase_value(orchestrator/routes/pipelines.py:9760), so "Check agent logs" extracts a role the host can hand toget_container_logs. The phase-only fallback is documented in the inline comment. - Body wording /
provide_input— Dropped the misleading "any open agent decision" clause and added the bounded ~60 min force-kill budget so "continues to poll" reads correctly (orchestrator/routes/pipelines.py:9777-9780). - Critical-blockers narrowing on escalate —
_handle_brc_consensus_timeoutnow extractscritical_role_namesfrom_brc_timeout_result.get("critical_blockers")and prefers it over the caller-suppliedblocking_agents(pipelines.py:9902-9914). Bothreviewer_roleandproducer_roleper entry are included with dedupe — that matches the entry shape inpeer_consensus.py:1397. _latest_active_role_heartbeatdirect tests —TestLatestActiveRoleHeartbeat(test_pipelines_routes.py:486) covers empty roles, recent heartbeat, picking-most-recent, inactive-role filtering, monitor-unavailable, no-heartbeats, and theexc_info=Truelogging path. The patches go through_patch_health_monitorand route thefrom health_monitor import …lookup to the mock — tests should pass on the no-imports path too.exc_info=Trueconsistency — Both the proposal-lookupexcept(pipelines.py:9893) and heartbeat-lookupexcept(pipelines.py:9719) now log withexc_info=True, matching the publish helper's outer except. A traceback survives if any of them throws.- Polish —
_capture_alerts(monkeypatch_target=None)parameter dropped; the run-loop comment atpipelines.py:11498-11502and the_check_brc_progress_gatedocstring updates the "auto-HITL decision" wording to "consensus-timeout alert / OVERSEER_ALERT".
Defensive coverage worth calling out
test_brc_escalate_falls_back_to_caller_blocking_agents (test_pipelines_routes.py:170) exercises the case where the tracker's escalate result omits critical_blockers (older return shape, or a matrix that cleared between handle_timeout() calls). The fallback is escalate_blocking = critical_role_names or blocking_agents, so the alert still has roles in the subject. Good defensive choice.
Conflict resolution at the merge
The scripts/file-size-allowlist.yaml resolution in fdef6072 is correct. PR-side baseline for orchestrator/routes/pipelines.py (15702 lines / 684817 bytes — verified by wc -l -c on the merged tree) and main-side for scripts/select_tests.py (1875 / 75206 — from #2262) both match the actual post-merge file sizes.
Non-blocking suggestions
a. Stale "HITL escalation" wording in orchestrator/models.py
models.py:378 still describes consensus_timeout_minutes as "Timeout for consensus before HITL escalation", and models.py:384 references "the consensus-timeout HITL decision". Same flavour of stale wording the previous review flagged in pipelines.py/run_concurrent_phase. Both fields now drive an OVERSEER_ALERT, not a HITL decision. A future reader hunting for "where the HITL decision opens" will land here and find a description that still claims one does. One-line fix on each — non-blocking polish, but the same justification as the previous round's docstring refresh.
b. PR description references the old subject format
The description block under "Alert metadata schema" still states Subject: consensus-timeout: <phase> [<priority>]. After this PR's fix, that's inaccurate — the second slot is <agent_role> (with <phase> only as a fallback when no blocking role is reported). Worth syncing the description so a future cherry-picker doesn't recreate the original wart.
c. _check_brc_progress_gate docstring at pipelines.py:9595
The "Heartbeat-cadence contract: the decision-17 path …" sentence still uses "decision-17" as a name for the coder-mid-merge-conflict path. Strictly speaking that's a historical reference (decision-17 was the original incident's decision id on issue-1557-v2), so it's not wrong — but for consistency with the rest of the comment refresh in this PR, "the alert-17 path" or just "the coder-mid-merge-conflict path" reads cleaner against the post-#2264 protocol. Marginal.
d. Author-chosen non-fix: store: StateStore parameter
The PR keeps _handle_brc_consensus_timeout(... store: StateStore, ...) with # noqa: ARG001 — kept for call-site compatibility (#2264). That's the explicit opt-out the previous review allowed for ("the call site is internal, so that's a self-imposed constraint"). Fine to leave; flagging just so the next reviewer doesn't re-litigate.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- orchestrator/models.py: update consensus_timeout_minutes and brc_consensus_progress_gate_seconds field descriptions to reference OVERSEER_ALERT (post-#2264) instead of HITL escalation/decision. - orchestrator/routes/pipelines.py (_check_brc_progress_gate docstring): reword the heartbeat-cadence contract paragraph to call out the coder-mid-merge-conflict path directly, marking decision-17 as the pre-#2264 historical name. No semantic change. - scripts/file-size-allowlist.yaml: bump pipelines.py baseline by 1 line / 77 bytes for the docstring change.
|
Addressed the non-blocking polish from the latest re-review at (a) Stale "HITL escalation" wording in (c) (b) PR description references the old subject format — I cannot edit the PR description ( -Subject: `consensus-timeout: <phase> [<priority>]` (matches the existing `OVERSEER_ALERT` subject format).
+Subject: `consensus-timeout: <agent_role> [<priority>]` — populated from the first `blocking_agents` entry so the SDLC skill's "Check agent logs" can extract a real role for `get_container_logs`. Falls back to `<phase>` when no blocking role is reported (e.g. tracker-import-error path).(d) — Authored by egg |
This comment has been minimized.
This comment has been minimized.
# Conflicts: # orchestrator/tests/test_consensus_polling.py # scripts/file-size-allowlist.yaml
There was a problem hiding this comment.
No agent-mode design concerns (re-review at ea42d12).
The only PR-authored delta since fdef6072 is 42366c678 — a docs/wording refresh that retires "HITL escalation" / "decision-17" phrasing in orchestrator/models.py (two Field(description=...) strings) and the _check_brc_progress_gate docstring in orchestrator/routes/pipelines.py, plus a 1-line/77-byte allowlist bump. No control flow changes. This addresses items (a) and (c) from the prior general-review pass and keeps the documentation consistent with the post-#2264 protocol (consensus-timeout publishes an OVERSEER_ALERT rather than opening a HITL decision).
Nothing here changes the alert's design profile: still a non-blocking broadcast with lightweight orienting metadata, no pre-fetched diffs/logs, operator continues to use the existing tool surface (cancel_task / restart_phase) at their own discretion.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review at ea42d12a (PR-side delta 42366c67)
The single PR-authored commit since my last review (fdef6072) is 42366c67, which addresses two of the three actionable non-blocking items from the previous round. No new code paths, no semantic changes — docstring/Field(description=...) text only. Cleanly merged with main at ea42d12a.
Previous feedback — addressed
-
(a) Stale "HITL escalation" wording in
orchestrator/models.py— Fixed.consensus_timeout_minutes(models.py:380): now"Timeout for consensus before publishing an OVERSEER_ALERT (#2264)".brc_consensus_progress_gate_seconds(models.py:410-413): now"Defer the consensus-timeout OVERSEER_ALERT while ..."with the issue tag updated to(#2243, #2264).
Both fields' descriptions now match the actual post-#2264 behaviour they configure.
-
(c)
_check_brc_progress_gatedocstring atpipelines.py:9595-9598— Reworded.
The "Heartbeat-cadence contract" paragraph now leads with"the coder-mid-merge-conflict path (no CONSENSUS_PROPOSE yet, only container heartbeats — the original incident's decision-17 flavour, pre-#2264)".decision-17is correctly demoted to a historical-flavour reference rather than a current name.
Previous feedback — explicitly deferred
-
(b) PR description still references the old subject format — Author flagged in their feedback comment that
gh pr editrejects the edit (PR #2269 is not owned by james-in-a-box). This needs @jwbron to update the "Alert metadata schema" block to reflectconsensus-timeout: <agent_role> [<priority>](with<phase>as fallback). Non-blocking, but worth syncing before merge so a future cherry-picker doesn't recreate the original wart. -
(d)
store: StateStoreparameter — Acknowledged as the explicit opt-out from the previous round;# noqa: ARG001 — kept for call-site compatibility (#2264)atpipelines.py:9841.
Merge integration
The main-side merge in ea42d12a cleanly integrated:
scripts/file-size-allowlist.yaml— main'sc35517064dropped per-filelines/bytesbaselines in favour ofissueonly (the merge schema rationalisation). The PR's+1 line / +77 bytesbump in42366c67is therefore obsolete post-merge — but harmless: the new schema makes the field unnecessary, and the file is correctly using the simplified format at HEAD.orchestrator/models.pyadded 41 lines from main (newpost_consensus_iteration_budget_secondsandpost_consensus_max_total_secondsfields). Inserted between the consensus-timeout field this PR rewrote and the progress-gate field, so the description updates survived intact.orchestrator/routes/pipelines.pyadded 83 lines from main (post-timeout polling loop). The consensus-timeout call site atpipelines.py:11562and the_handle_brc_consensus_timeout/_publish_consensus_timeout_alerthelpers are unchanged. Verified by reading the post-merge file: subject-role logic, critical-blockers narrowing, body wording, andexc_info=Trueconsistency are all preserved.
Verified clean post-merge
orchestrator/models.py:380, 410-413— descriptions match the actual code path atpipelines.py:9726-9833.pipelines.py:9595-9598docstring — coder-mid-merge-conflict framing reads correctly against the_latest_active_role_heartbeatlookup atpipelines.py:9683and the heartbeat path atpipelines.py:9696-9719.- Other references to "HITL decision" / "HITL escalation" in
models.py(:5,:79,:440,:720,:889-917) andpipelines.py(:243,:261,:6354,:7863,:7896,:8537,:9393,:9480-9555) are about the generic HITL decision system or other flows (refine, redirect, incomplete-consensus). Not in scope for this PR's docstring refresh — leaving them untouched is correct.
No blocking issues. Approving.
— Authored by egg
|
egg review completed. View run logs 16 previous review(s) hidden. |
Address review feedback on PR #2275: - Line 725 (Timeout Handling lead-in): replace 'before opening a HITL decision' with 'before publishing the consensus-timeout OVERSEER_ALERT' so it matches the per-bullet description that follows. - Line 856 (HITL Escalation Paths table): remove the obsolete [Continue waiting, Accept current state, Abort phase] options for consensus timeout (critical blockers) and replace with the italic-aside convention used by the surrounding 'no HITL' rows, pointing at the Timeout Handling section. Both strings contradicted the corrections this PR was making elsewhere in the same file; #2269 removed the consensus-timeout HITL decision entirely in favor of OVERSEER_ALERT. Authored-by: egg
…R_ALERT Address PR #2277 review feedback: two stale rows in concurrent-execution.md contradicted the OVERSEER_ALERT flow established by #2269. - L36 (PipelineConfig table): consensus_timeout_minutes description now matches the parallel sdlc-pipeline.md row. - L856 (HITL Escalation Paths table): critical-blocker consensus timeout no longer lists the removed three-option HITL choice; matches the shape of the advisory-only row directly below it.
…2277) * Fix #2275: docs follow-up — consensus-timeout HITL → OVERSEER_ALERT Docs leftovers from #2269 (which replaced the consensus-timeout HITL choice with an OVERSEER_ALERT but didn't reach every reference). - concurrent-execution.md: brc_consensus_progress_gate_seconds row references OVERSEER_ALERT; Timeout Handling lead sentence updated to match (it contradicted the bullets that already described the alert flow). - sdlc-pipeline.md: consensus_timeout_minutes / brc_consensus_progress_gate_seconds rows reference OVERSEER_ALERT; BRC consensus-protocol step 5 rewritten as a non-blocking alert (post-timeout polling continues; cancel_task / restart_phase / provide_input replace the removed three-option HITL). * Update concurrent-execution.md HITL/consensus-timeout rows to OVERSEER_ALERT Address PR #2277 review feedback: two stale rows in concurrent-execution.md contradicted the OVERSEER_ALERT flow established by #2269. - L36 (PipelineConfig table): consensus_timeout_minutes description now matches the parallel sdlc-pipeline.md row. - L856 (HITL Escalation Paths table): critical-blocker consensus timeout no longer lists the removed three-option HITL choice; matches the shape of the advisory-only row directly below it. --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Summary
choiceHITL decision opened on BRC consensus timeout with a non-blockingOVERSEER_ALERT.OVERSEER_ALERTflow surfaces the alert (Check agent logs / Acknowledge / Cancel pipeline); operators retaincancel_task/restart_phase/provide_inputfor active intervention.Why
_handle_brc_consensus_timeoutused to open achoice-typedpending_decisionwith options[Continue waiting, Accept current state, Abort phase]. Three problems:OVERSEER_ALERTis the right shape for "the platform thinks something might be wrong, please look at it" — a notification, not a gate.What changed
orchestrator/routes/pipelines.py_publish_consensus_timeout_alerthelper that builds anOVERSEER_ALERTMessageand writes it via_get_message_store()._latest_active_role_heartbeathelper that mirrors the heartbeat half of_check_brc_progress_gateso the alert metadata'slatest_heartbeat_atreflects active-role heartbeats only (avoids cross-phase pollution from the singletonHealthMonitor)._handle_brc_consensus_timeoutnow publishes anOVERSEER_ALERTon both former decision paths:priority=high.priority=medium. The existing_emit_event(CONSENSUS_TIMEOUT)on this path is preserved.proceed_with_notificationpath stays silent (unchanged)._handle_brc_consensus_timeoutgains an optionalactive_role_names: list[str] | Noneparameter; the call site passes[e.role.value for e in active_executions]so the alert can include heartbeat metadata. Tests that don't set it getNone-valued metadata.Alert metadata schema (per AC):
{ "anomaly_type": "consensus-timeout", "phase": "<phase>", "blocking_agents": ["..."], "latest_proposal_at": "<ISO8601 or null>", "latest_heartbeat_at": "<ISO8601 or null>", "consensus_timeout_minutes": 30, "priority": "high|medium", "slice_id": "<when present>" }Subject:
consensus-timeout: <phase> [<priority>](matches the existingOVERSEER_ALERTsubject format).Tests
orchestrator/tests/test_pipelines_routes.py— three tests that asserted onpipeline.decisions[0].questionmigrate to assert on theOVERSEER_ALERTmessage and its metadata. Theadd_decision-failure regression test re-tooled to cover message-store add failures (still logged withexc_info).orchestrator/tests/test_consensus_polling.py—test_timeout_creates_hitl_decisionrenamed totest_timeout_publishes_overseer_alert; assertion migrates to capture and inspect the published alert.Docs
docs/guides/concurrent-execution.md— timeout-handling section reflects the new alert flow and metadata.docs/guides/sdlc-pipeline.md— troubleshooting note points at theOVERSEER_ALERTnotification flow plus thecancel_task/restart_phase/provide_inputlevers.SDLC skill — no changes; the existing
OVERSEER_ALERThandling atskills/sdlc/SKILL.mdalready covers this anomaly type (id-based dedup, prompt with Check agent logs / Acknowledge / Cancel pipeline).Migration note for operators
Any operator-side automation watching
pending_decisionsfor the strings"BRC consensus failure"or"Consensus not reached after N minutes"must migrate to scanningrecent_messagesforOVERSEER_ALERTwithmetadata.anomaly_type == "consensus-timeout". The previous decision shape is no longer produced.Test plan
pytest orchestrator/tests/test_pipelines_routes.py— 15/15 pass.pytest orchestrator/tests/test_consensus_polling.py orchestrator/tests/test_consensus_timeout_recheck.py orchestrator/tests/test_slice_run_loop_integration.py— pass.make lint— pass./sdlcrather than opening achoicedecision.Related