From 7ab59730e4d2b3bc7029411f2ad1d1ac39aa4af8 Mon Sep 17 00:00:00 2001 From: Lucas de Castro Zanoni Date: Fri, 28 Aug 2026 20:33:17 -0300 Subject: [PATCH] fix(clawde): stop the steward's own probe manufacturing heartbeat edges The change gate fires when the probe's fingerprint differs from the last one it fired on, so every field in that fingerprint is a wake reason. The steward's probe folded in three the steward never acts on: the shared checkout's dirty flag, which flips as peers work their worktrees; both revision shas, which move on every push by anyone; and the raw continuous integration state, which cycles through its pending values on every run. Ordinary fleet traffic therefore manufactured an edge almost every tick and the edge trigger degraded into the level trigger it replaced. kira's steward woke 73 times on 27 August, four an hour, for a repository whose actionable state had barely moved. Print the decision rather than the state it was read from: commits ahead and behind instead of the shas, and a failing-or-not boolean instead of the run's phase, reusing the same predicate steward-status already derives. A red run still wakes the steward; a queued one no longer does. Agent-Machine: kira Agent-Resume: claude --resume efee1316-8d94-438e-951e-e9c978626531 --- .../scripts/steward-heartbeat-probe.py | 13 ++++---- .../unit/test_steward_heartbeat_probe.py | 32 ++++++++++++++++--- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/module/agent-types/steward/payload/scripts/steward-heartbeat-probe.py b/module/agent-types/steward/payload/scripts/steward-heartbeat-probe.py index ec8e61d..5f779f2 100644 --- a/module/agent-types/steward/payload/scripts/steward-heartbeat-probe.py +++ b/module/agent-types/steward/payload/scripts/steward-heartbeat-probe.py @@ -5,11 +5,8 @@ DECISION_FINGERPRINT_FIELDS = ( "verdict", - "head", - "upstream", "behind", "ahead", - "dirty", "inbox_unread", ) @@ -28,11 +25,15 @@ def collect_steward_status() -> dict: return json.loads(completed.stdout) +def continuous_integration_is_failing(status: dict) -> bool: + return status.get("continuous_integration", {}).get("state") == "failing" + + def decision_fingerprint(status: dict) -> str: relevant = {field: status.get(field) for field in DECISION_FINGERPRINT_FIELDS} - relevant["continuous_integration_state"] = status.get( - "continuous_integration", {} - ).get("state") + relevant["continuous_integration_failing"] = continuous_integration_is_failing( + status + ) return json.dumps(relevant, sort_keys=True) diff --git a/module/agent-types/steward/payload/tests/unit/test_steward_heartbeat_probe.py b/module/agent-types/steward/payload/tests/unit/test_steward_heartbeat_probe.py index b764b02..ab2d15d 100644 --- a/module/agent-types/steward/payload/tests/unit/test_steward_heartbeat_probe.py +++ b/module/agent-types/steward/payload/tests/unit/test_steward_heartbeat_probe.py @@ -51,7 +51,7 @@ def test_fingerprint_tracks_decision_fields_not_volatile_detail(): assert base == same_state_new_url -def test_fingerprint_changes_when_continuous_integration_state_flips(): +def test_fingerprint_changes_when_continuous_integration_starts_failing(): passing = steward_heartbeat_probe.decision_fingerprint(diverged_status()) failing = steward_heartbeat_probe.decision_fingerprint( diverged_status(continuous_integration={"state": "failing", "url": "x"}) @@ -59,14 +59,38 @@ def test_fingerprint_changes_when_continuous_integration_state_flips(): assert passing != failing -def test_fingerprint_changes_when_upstream_moves(): +def test_a_run_moving_through_its_pending_states_raises_no_edge(): + unfinished = [ + steward_heartbeat_probe.decision_fingerprint( + diverged_status(continuous_integration={"state": state, "url": "x"}) + ) + for state in ("none", "pending", "passing") + ] + assert len(set(unfinished)) == 1 + + +def test_a_peer_moving_the_shared_checkout_raises_no_edge(): before = steward_heartbeat_probe.decision_fingerprint(diverged_status()) after = steward_heartbeat_probe.decision_fingerprint( - diverged_status(upstream="cafef00d") + diverged_status(head="cafef00d", upstream="cafef00d", dirty=False) ) + assert before == after + + +def test_fingerprint_changes_when_the_checkout_falls_behind(): + before = steward_heartbeat_probe.decision_fingerprint(diverged_status(behind=1)) + after = steward_heartbeat_probe.decision_fingerprint(diverged_status(behind=2)) assert before != after +def test_fingerprint_changes_when_the_inbox_receives_a_message(): + empty = steward_heartbeat_probe.decision_fingerprint(diverged_status()) + delivered = steward_heartbeat_probe.decision_fingerprint( + diverged_status(inbox_unread=["1787959285-from-rin.json"]) + ) + assert empty != delivered + + def test_fingerprint_is_stable_json(monkeypatch): fingerprint = steward_heartbeat_probe.decision_fingerprint(diverged_status()) - assert json.loads(fingerprint)["continuous_integration_state"] == "passing" + assert json.loads(fingerprint)["continuous_integration_failing"] is False