fix(kanban): circuit breaker halts saga after repeated identical failure signature (BUILD-261) - #9
Merged
Conversation
…ure signature (BUILD-261) Incident: on 2026-07-09 a releaser kanban card merged 8 PRs across two batches (NousResearch#465-468, NousResearch#469-472), each remediation reviewed/verified/merged, yet the post-merge "Master Release" workflow failed with the byte-identical error signature every time. The existing consecutive_failures breaker never tripped because every attempt "succeeded" from the worker's point of view (it resets on completion); check_respawn_guard only rate-limits how often a respawn is attempted (it fired 761x/24h), it never inspects whether repeated attempts are converging. Adds a second, content-aware circuit breaker: - normalize_failure_signature(): reduces a failure log to a stable signature (first ##[error] line, or final line; strips timestamps/run-ids/SHAs, collapses whitespace). - block_task()'s worker-self-report path (kind=needs_input/capability/ transient) now records a `failure_signature` task_event from the block reason. Deliberately NOT hooked into the crash/timeout/spawn-failure funnel (_record_task_failure) — that path already has its own independently-configurable failure_limit breaker, and layering a second, lower-default-threshold breaker on the identical event stream would silently override an operator's more lenient failure_limit for any task whose infra errors happen to repeat verbatim (very common). - check_failure_signature_breaker(): compares the last N (default 2, configurable via kanban.failure_signature_threshold config or HERMES_KANBAN_FAILURE_SIGNATURE_THRESHOLD env) recorded signatures for a task and its linked remediation children (task_links); if identical, dispatch_once refuses to respawn and instead blocks the task (kind=needs_input) with a comment describing the trip (both signatures + run refs). Distinct signatures never trip it. - Blocking reuses block_task's existing `blocked` task_event, which the gateway's _kanban_notifier_watcher already delivers to subscribers (Telegram included) — no new notify channel added. Tests: signature normalization (incl. the incident's literal sample), trip-at-2-identical, no-trip-on-distinct, threshold config, only-most- recent-window, remediation-children inclusion, and full dispatch_once integration (blocked status + comment + no respawn). A regression guard test pins that _record_task_failure does NOT emit signatures, documenting why that path is excluded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FoCncveCNmgqD39G2geeF2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Incident
On 2026-07-09 a releaser kanban card (
t_90949a61) merged 8 PRs across two batches — 4 (aldnoah NousResearch#465-468) then 4 more (NousResearch#469-472) — whose post-merge "Master Release" workflow failed with the byte-identical error signature every single time:Each remediation was reviewed, verified, and merged, and the identical symptom recurred immediately after — nothing detected the non-convergence. The existing
consecutive_failurescircuit breaker never tripped because every attempt "succeeded" from the worker's point of view (a merge is a success; the counter resets to 0 on completion).check_respawn_guardfired 761x in 24h, but it only rate-limits how often a respawn is attempted — it never inspects whether repeated attempts are actually converging.Fix
A second, content-aware circuit breaker in
hermes_cli/kanban_db.py:normalize_failure_signature(text)— reduces a (possibly multi-line) failure log to a stable signature: the first##[error]...line (or the final non-blank line if there's no such marker), with timestamps/run-ids/SHAs stripped and whitespace collapsed. Two CI runs of the identical underlying failure normalize to the identical signature even though timestamps/run-ids differ.block_task()'s worker-self-report path (kind=needs_input|capability|transient— a worker callingkanban_blockwith a reason it can't resolve) now also records afailure_signaturetask_event from the block reason._record_task_failure): that path already has its own independently-configurablefailure_limitbreaker (DEFAULT_FAILURE_LIMIT=2), and layering a second breaker with a different default threshold onto the identical event stream would silently override an operator's more lenientfailure_limitfor any task whose infra errors happen to repeat verbatim byte-for-byte (very common — e.g. "no PATH", workspace-resolution errors). Caught this exact regression while implementing (seetest_record_task_failure_does_not_emit_failure_signature_event) — it broke 4 pre-existing tests intest_kanban_core_functionality.pybefore the fix was scoped correctly.check_failure_signature_breaker(conn, task_id, threshold=None)— compares the last N (default 2, configurable viakanban.failure_signature_thresholdconfig orHERMES_KANBAN_FAILURE_SIGNATURE_THRESHOLDenv, mirroring thefailure_limitpattern) recorded signatures for the task and its linked remediation children (task_links, covering the "saga" shape where a parent card fans out a fresh child task per attempt instead of looping one task_id). If they're all identical, the signal is "not converging."_dispatch_once_locked's ready-task loop checks the breaker beforecheck_respawn_guard(whose defer is only a one-tick skip). If tripped, the task is blocked (kind=needs_input) instead of respawned, with a comment describing the trip (both signatures + run refs) via_trip_failure_signature_breaker.block_task's existingblockedtask_event — the exact event kind the gateway's_kanban_notifier_watcheralready polls and delivers to subscribers on whatever platform they're on (Telegram included). No new notify channel was added, per the spec.Tests (TDD — written first)
All in
tests/hermes_cli/test_kanban_db.py:TestNormalizeFailureSignature— 9 unit tests including the literal incident sample, timestamp/run-id/SHA stripping, whitespace collapsing, final-line fallback, empty/None input.dispatch_onceintegration: circuit breaker blocks + comments + prevents respawn on identical signatures; spawns normally on distinct signatures; honorssignature_repeat_thresholdkwarg.block_taskintegration: records signature forneeds_input, does NOT record fordependencyblocks (healthy backpressure, not a failure) or empty reason text._record_task_failuredoes NOT emitfailure_signature(documents/pins the scoping decision above).Test results
Files touched
hermes_cli/kanban_db.py—normalize_failure_signature,check_failure_signature_breaker,_trip_failure_signature_breaker,_record_failure_signature,_resolve_failure_signature_repeat_threshold,_recent_failure_signatures,block_taskhook,dispatch_once/_dispatch_once_lockednewsignature_repeat_thresholdkwarg + ready-loop hook,DispatchResult.circuit_breaker_tripped.gateway/kanban_watchers.py— readskanban.failure_signature_thresholdconfig, threads it intodispatch_once.hermes_cli/kanban.py— same config threading for thehermes kanban dispatchCLI path.tests/hermes_cli/test_kanban_db.py— new test coverage described above.🤖 Generated with Claude Code
https://claude.ai/code/session_01FoCncveCNmgqD39G2geeF2