Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
840906f
documenter: correct README runtime-wiring section for #3596
claude Jul 25, 2026
83e9255
coder: snapshot enrichment + forward-progress detector for #3596
Jul 25, 2026
4890014
tester: contract tests for #3596 detection plane + snapshot enrichment
Jul 25, 2026
f9401bc
tester: fix lint issues in test files
Jul 25, 2026
844620a
tester: fix lint in runtime wiring tests
Jul 25, 2026
4a02ece
tester: format test files with ruff
Jul 25, 2026
513d21b
tester: fix xfail markers and add contract compliance tests for forwa…
Jul 25, 2026
21ef085
coder: address all NACKs on forward-progress detector and detection p…
Jul 25, 2026
9f93361
coder: remove dead code in _detect_commit_stall (reviewer_code_holist…
Jul 25, 2026
a6c8754
Persist BRC history for slice-1 (#2548)
Jul 25, 2026
340c2fa
Wire detection plane to adjudication + corrective routing, add transi…
james-in-a-box[bot] Jul 25, 2026
82e5e48
Source agent exit info from container_info/agent_exits, not a nonexis…
james-in-a-box[bot] Jul 25, 2026
25f53a3
Fix liveness, crash-restart and target-role derivations in the detect…
james-in-a-box[bot] Jul 25, 2026
7aeccd5
Bound adjudicator spawns, use a monotonic tick clock, build executors…
james-in-a-box[bot] Jul 25, 2026
41239e9
Release adjudication claims that never spawned, reject pod ids as tar…
james-in-a-box[bot] Jul 25, 2026
784bd74
Source expected_duration_s from PipelineConfig and populate restart_p…
james-in-a-box[bot] Jul 25, 2026
66d99b5
Address review feedback: target-role validation, restart-propagation …
james-in-a-box[bot] Jul 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .egg-state/agent-outputs/coder/brc-memory-issue-3596-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# BRC Memory — Issue #3596 (coder)

## Pipeline Context
- Issue: #3596 — Operator cannot tell a working agent from a wedged one
- Role: coder
- Phase: implement (per BRC event)

## Operator's cq-1 Resolution (binding)
The operator resolved cq-1 with a detailed response:
1. The detection plane IS already wired and IS evaluated in production — do NOT add a new tick or invocation path.
2. The actual defect is that `snapshot_from_health_context` populates only 5 of 13 top-level fields and 3 of 7 RunningAgent fields.
3. Fix `role=str(cid)` defect — container IDs are mapped to agent roles via pipeline state.
4. Scope: four narrow-and-deep items (snapshot enrichment, forward-progress detector, peer-progress gate fix, status enrichment).
5. Null is not zero — any new progress field must be null when unmeasurable.
6. Session transcripts remain a real gap — agent_log_store captures at Job removal, not at event-pod EXIT.
7. Do not rebuild agent_log_store.py or the /health/alerts endpoint.

## Work Completed

### Task 1: Detector Starvation Audit
- Audited all 25 registered detection-plane detectors
- Found: 0 fully populated, 3 partially starved, 22 fully starved
- The `role=str(cid)` bug affects ALL detectors that key on agent role
- Audit saved to: `.egg-state/agent-outputs/coder/detector-audit-issue-3596.md`

### Task 2: Fix role=str(cid) defect
- Fixed in `snapshot_from_health_context` — now maps container IDs to agent roles via `AgentExecution.container_id` → `AgentExecution.role`
- Falls back to `str(cid)` when no mapping is found

### Task 3: Populate RunningAgent liveness fields
- `last_tool_call_age_s` — populated from ProgressStore (last ProgressEvent timestamp per agent)
- `last_heartbeat_age_s` — populated from HealthMonitor._last_heartbeat (only when singleton tracks the right pipeline)
- `exit_code`, `exit_reason` — populated from pipeline's AgentExecution
- All fields are null when unmeasurable, never 0

### Task 4: Populate git_state
- `agent_commit_counts` — per-agent commit counts via `git rev-list --count origin/<base>..<worktree_HEAD>`
- `agent_last_commit_age_s` — per-agent age of last commit (for forward-progress stall detection)
- `commit_count`, `last_commit_sha`, `last_commit_at`, `branch` — branch-level git state
- `fsck_errors`, `index_lock_present`, `lock_age_s` — worktree corruption detection
- Best-effort: git failures degrade to empty dict

### Task 5: Populate container_transitions
- Currently returns empty tuple — kubernetes_monitor doesn't track transition history
- Documented in code that this needs a future enhancement to track from→to transitions

### Task 6: Populate decision_state
- `pending_hitl`, `open_decisions` — from pipeline.decisions
- `approved_unapplied`, `oldest_open_age_s` — from resolved decisions
- `replay_pending`, `replay_count` — not tracked on pipeline model (set to False/0)

### Task 11: Populate phase_state.expected_duration_s + raw.runtime
- `phase_state.expected_duration_s` — populated from pipeline config or phase-specific defaults (refine=600s, plan=900s, apply=300s, implement=3600s)
- `raw.runtime.run_pipeline_thread_alive` — from driver_heartbeat.tick_age_seconds
- `raw.runtime.thread_last_tick_age_s` — from driver_heartbeat.tick_age_seconds
- `raw.runtime.spawn_age_s` — from driver_heartbeat.spawn_age_seconds

### Task 7: Forward-progress detector
- New tier-1 detector in `health_checks/tier1/forward_progress.py`
- Three firing modes: stall (last commit >600s ago), reset (commit count decreased), no-commits-at-completion
- **Stateless** design: reads `git_state.agent_last_commit_age_s`, `git_state.agent_commit_counts`, `git_state.agent_prev_commit_counts` from the snapshot
- Registered in `DetectionPlane.default()` and `health_checks/tier1/__init__.py`
- 3 calibration corpus fixtures added (stall__bad, stall__normal, reset__bad)

### Tests
- 15 unit tests for forward-progress detector (all pass)
- 15 unit tests for snapshot enrichment (all pass)
- 3 calibration corpus fixtures for forward-progress (all pass)
- All 115 existing detection plane + calibration tests still pass
- All 860 health check + detection tests pass
- mypy: no issues found
- ruff: all checks passed
- bandit: only low-severity issues (same patterns as existing code)

## Deferred Design Choices (to raise as contract decisions)
1. Commit counting scope: worktree branch only, or also track patch-id-equivalent across rebases?
2. Tool-call proxy: progress store vs. new working_heartbeat emitter?
3. Alert payload shape: structured evidence fields vs. free-form string with metadata?
4. Log TTL configurability: 24h sufficient or per-pipeline?

## Remaining Work (not yet started)
- Peer-progress gate fix — depends on understanding the current gate implementation in health_monitor.py
- Status endpoint enrichment — depends on understanding the /status route in routes/pipelines/__init__.py
- Container transitions tracking — kubernetes_monitor doesn't currently track transition history; needs enhancement to populate container_transitions for container death/restart loop detectors
- Deferred design choices (4 items) — to raise as contract decisions when reached
178 changes: 178 additions & 0 deletions .egg-state/agent-outputs/coder/detector-audit-issue-3596.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Detector Starvation Audit — Issue #3596

For each of the 25 registered detection-plane detectors, this audit names the snapshot fields its predicate reads and whether `snapshot_from_health_context` populates them.

## Current snapshot_from_health_context output (detection_plane.py)

Populated fields:
- `snapshot_id` — yes
- `pipeline_id` — yes
- `phase` — yes
- `running_agents` — yes, but only `role`, `state`, `lifecycle_owner` (BUG: role=str(cid))
- `phase_state` — yes, but only `status`, `lifecycle_owner`, `event_loop_owner`, `started_age_s`, `awaiting_spawn`
- `consensus` — NO (empty dict)
- `decision_state` — NOW POPULATED (was NO)
- `container_transitions` — NO (empty tuple)
- `gateway_error_counters` — NO (empty dict)
- `cost_counters` — NO (empty dict)
- `midturn_messages` — NO (empty tuple)
- `git_state` — NOW POPULATED (was NO)
- `raw` — NO (empty dict)

RunningAgent populated fields:
- `role` — FIXED (was str(cid), now mapped from pipeline state)
- `state` — yes ("running")
- `lifecycle_owner` — yes
- `exit_code` — NOW POPULATED (was None)
- `exit_reason` — NOW POPULATED (was None)
- `last_tool_call_age_s` — NOW POPULATED (was None)
- `last_heartbeat_age_s` — NOW POPULATED (was None)

phase_state populated fields (post-enrichment):
- `status` — yes
- `lifecycle_owner` — yes
- `event_loop_owner` — yes
- `started_age_s` — yes
- `awaiting_spawn` — yes
- `expected_duration_s` — NOW POPULATED (was missing)

raw populated fields (post-enrichment):
- `raw.runtime` — NOW POPULATED (was empty)
- `run_pipeline_thread_alive` — from driver_heartbeat
- `thread_last_tick_age_s` — from driver_heartbeat
- `spawn_age_s` — from driver_heartbeat

## Detector-by-detector audit (post-enrichment)

### PhaseStallDetector (detection_plane.py:295-376)
Fields read: phase_state.status, running_agents, phase_state.lifecycle_owner, phase_state.awaiting_spawn, decision_state.pending_hitl, decision_state.open_decisions, phase_state.started_age_s, consensus.blocking_agents
Status: **PARTIALLY POPULATED** — decision_state now populated; consensus.blocking_agents still empty

### detect_heartbeat_stall (consensus_stall.py:217-269)
Fields read: phase_state.status, running_agents, RunningAgent.last_tool_call_age_s, RunningAgent.last_heartbeat_age_s
Status: **NOW UNBLOCKED** — last_tool_call_age_s and last_heartbeat_age_s are populated

### detect_container_death (container_k8s.py:75-153)
Fields read: container_transitions, running_agents (for exit_code/exit_reason)
Status: **STILL STARVED** — container_transitions not populated; exit_code/exit_reason only for non-live agents

### detect_container_oom_evicted (container_k8s.py:275-311)
Fields read: container_transitions
Status: **STILL STARVED**

### detect_container_restart_loop (container_k8s.py:220-268)
Fields read: container_transitions
Status: **STILL STARVED**

### detect_overseer_self_injection (container_k8s.py:160-213)
Fields read: running_agents (role, exit_reason, exit_code), container_transitions
Status: **PARTIALLY UNBLOCKED** — role fixed; exit_reason/exit_code populated for non-live agents; container_transitions still empty

### detect_run_pipeline_thread_liveness (runtime_liveness.py:84-131)
Fields read: raw.runtime (run_pipeline_thread_alive, thread_last_tick_age_s)
Status: **NOW UNBLOCKED** — raw.runtime populated from driver_heartbeat

### detect_duration_drift (runtime_liveness.py:138-186)
Fields read: phase_state.status, phase_state.started_age_s, phase_state.expected_duration_s, phase_state.drift_ratio
Status: **PARTIALLY UNBLOCKED** — expected_duration_s now populated; drift_ratio still not populated

### detect_agent_restart_propagation (runtime_liveness.py:193-252)
Fields read: raw.runtime (restart_propagation), phase_state (restart_requested_age_s, restart_role), container_transitions
Status: **STILL STARVED**

### detect_auto_advance_wedge (decision_queue.py:80-124)
Fields read: phase_state.status, raw.runtime (auto_advance_pending, auto_advance_age_s)
Status: **PARTIALLY UNBLOCKED** — phase_state.status populated; auto_advance_pending/auto_advance_age_s still not populated

### detect_approved_decision_orphaned (decision_queue.py:131-171)
Fields read: decision_state (approved_unapplied)
Status: **NOW UNBLOCKED** — decision_state populated

### detect_restarted_decision_replay (decision_queue.py:178-212)
Fields read: decision_state (replay_pending, replay_count)
Status: **PARTIALLY UNBLOCKED** — replay_pending/replay_count set to False/0 (not tracked on pipeline model)

### detect_hitl_queue_backlog (decision_queue.py:224-261)
Fields read: decision_state (oldest_open_age_s)
Status: **NOW UNBLOCKED** — decision_state populated

### detect_worktree_corruption (worktree_branch.py:81-124)
Fields read: git_state (fsck_errors, index_lock_present, lock_age_s, branch)
Status: **NOW UNBLOCKED** — git_state populated

### detect_disk_inode_pressure (worktree_branch.py:131-174)
Fields read: raw.resources (disk_used_pct, inode_used_pct, disk_threshold_pct)
Status: **STILL STARVED** — raw.resources not populated

### detect_pr_external_mutation (worktree_branch.py:181-215)
Fields read: raw.pr_state (pr_head_sha, pushed_sha, external_mutation)
Status: **STILL STARVED**

### detect_pushed_pr_not_updated (worktree_branch.py:222-267)
Fields read: git_state (pr_head_sha, last_pushed_sha, pushed_age_s, pr_externally_mutated)
Status: **PARTIALLY STARVED** — git_state populated with branch/commit info but not PR-specific fields

### detect_cost_anomaly (cost_budget.py:51-87)
Fields read: cost_counters (cost_per_hour_usd, max_llm_cost_per_hour, tokens, cost_usd)
Status: **STILL STARVED**

### detect_gateway_error_spike (gateway_health.py:58-87)
Fields read: gateway_error_counters (5xx_rate_per_min, rate_threshold_per_min)
Status: **STILL STARVED**

### detect_gateway_repeated_denial (gateway_health.py:94-127)
Fields read: gateway_error_counters (identical_403_streak, denial_signature)
Status: **STILL STARVED**

### detect_gateway_token_expiry (gateway_health.py:134-162)
Fields read: gateway_error_counters (token_expired, 401)
Status: **STILL STARVED**

### detect_llm_substrate_unreachable (llm_substrate.py:62-96)
Fields read: raw.llm (litellm_reachable, consecutive_failures)
Status: **STILL STARVED**

### detect_effective_model_drift (llm_substrate.py:103-131)
Fields read: raw.llm (requested_model, effective_model)
Status: **STILL STARVED**

### detect_anthropic_5xx_sustained (llm_substrate.py:138-169)
Fields read: raw.llm (anthropic_5xx_streak, window_s)
Status: **STILL STARVED**

### detect_brc_thrash (brc_thrashing.py:57-92)
Fields read: consensus (nack_cycles, late_confirmed_then_renack)
Status: **STILL STARVED**

### detect_incomplete_consensus_deferral (brc_thrashing.py:99-131)
Fields read: consensus (incomplete_consensus_deferrals, deferral_cap)
Status: **STILL STARVED**

### detect_overseer_self_health (self_monitor.py:349-393)
Fields read: raw.self_health (classifier_failure_rate, advisor_failure_rate, failure_threshold)
Status: **STILL STARVED**

### detect_branch_divergence (_alerts.py:850-890)
Fields read: git_state (is_ancestor_of_base, patch_id_matches, pr_subject_divergence, branch)
Status: **PARTIALLY UNBLOCKED** — branch populated; is_ancestor_of_base, patch_id_matches, pr_subject_divergence not populated

## Summary (post-enrichment)

- Total registered detectors: 25
- Fully populated (all fields): 0
- Partially starved: 9 (down from 3 — more detectors now have some fields)
- Fully starved: 16 (down from 22)
- **Newly unblocked**: detect_heartbeat_stall, detect_approved_decision_orphaned, detect_hitl_queue_backlog, detect_worktree_corruption, detect_run_pipeline_thread_liveness
- **Partially unblocked**: detect_duration_drift (expected_duration_s now populated), detect_auto_advance_wedge (phase_state.status populated)
- **role=str(cid) bug fixed** for all detectors that key on agent role

## Priority order for remaining snapshot enrichment

1. **container_transitions** — unblocks 4 detectors (container_death, oom_evicted, restart_loop, overseer_self_injection)
2. **raw.llm** — unblocks 3 detectors (llm_substrate_unreachable, effective_model_drift, anthropic_5xx)
3. **gateway_error_counters** — unblocks 3 detectors
4. **consensus** — unblocks 2 detectors (brc_thrash, incomplete_consensus_deferral) + helps PhaseStallDetector
5. **cost_counters** — unblocks detect_cost_anomaly
6. **raw.resources** — unblocks detect_disk_inode_pressure
7. **raw.pr_state** — unblocks detect_pr_external_mutation
8. **raw.self_health** — unblocks detect_overseer_self_health
50 changes: 50 additions & 0 deletions .egg-state/agent-outputs/documenter/brc-memory-issue-3596-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# documenter BRC memory — issue #3596, slice-1

## Verdict: PROPOSED (README wiring correction)

- My commit: pending
- Task: task-1-13 (correct `health_checks/README.md` runtime-wiring section)
- Files: `orchestrator/health_checks/README.md`

## What I did (documenter-owned)

- Verified the claim in README line 88: `routes/pipelines._run_overseer_detection_plane`
builds the snapshot, evaluates the default plane, and routes findings.
- **CONFIRMED FALSE**: `_run_overseer_detection_plane` exists in
`orchestrator/routes/pipelines/_overseer.py:309` but has **zero call sites**
in production code (only imported in `__init__.py`). The detection plane
is never invoked from `_run_runtime_tick_checks` in `kubernetes_monitor.py`.
- `HealthCheckRunner.run_detection_plane()` exists in `runner.py:159` but is
also never called from the runtime tick.
- All 27 registered detectors are starved in production — they only fire
when driven by the calibration corpus in tests.
- Verified `snapshot_from_health_context` in `detection_plane.py:511`:
- Populates only `phase_state` (5 fields) and `running_agents` (3 of 7 fields).
- Does NOT populate: `container_transitions`, `git_state`, `decision_state`,
`cost_counters`, `gateway_error_counters`, `midturn_messages`, `raw.*`,
`RunningAgent.last_tool_call_age_s`, `RunningAgent.last_heartbeat_age_s`,
`RunningAgent.exit_code`, `RunningAgent.exit_reason`.
- **CONFIRMED the `role=str(cid)` defect**: `RunningAgent(role=str(cid), ...)`
puts a container UUID in the role field, not a role name.
- Corrected the README's "Runtime wiring" section to:
1. State clearly that the detection plane is NOT yet wired into the runtime tick.
2. Describe the intended wiring path (KubernetesMonitor → HealthCheckRunner →
run_detection_plane → EventBus → escalation).
3. Document that snapshot_from_health_context only populates phase_state and
running_agents, leaving most fields at empty defaults.
4. Note that task 1a wires the plane and tasks 1b-g enrich the snapshot builder.
- Updated the detector catalogue note to state both conditions (plane wired +
snapshot populated) must hold for a detector to fire in production.

## If re-spawned (review/NACK handling)

- If a reviewer NACKs my README correction, re-read their reason and the
current state of `kubernetes_monitor.py` / `detection_plane.py` / `_overseer.py`
to verify the claim, fix, re-commit, re-propose.
- The key verifiable facts:
- `grep -rn "run_detection_plane" orchestrator/ --include="*.py" | grep -v "def\|import\|#\|test"`
returns zero call sites.
- `snapshot_from_health_context` at `detection_plane.py:511` only sets
`phase_state` and `running_agents`.
- `RunningAgent(role=str(cid), ...)` at `detection_plane.py:536` uses
container ID as role.
Loading
Loading