Ekho-Labs/ai-coworker · PR #42
MERGE · APPROVED
Pull request review dashboard

Expose an authorized circuit reset and stop counting human mislabels toward the breaker

The org-wide circuit breaker of the AI co-worker control plane opened after three mislabeled intake issues and could not be closed: no reset path existed in production. Every wake event since Aug 12 died with organization circuit is open. This PR makes recovery self-service through the existing authenticated GitHub label surface and removes the failure mode that opened the breaker in the first place.

branch fix/circuit-reset-and-mislabel-counting base main 15 files +183 / −29 6 commits

01 Overview

Ekho-Labs/ai-coworker is the Go control plane for an event-driven AI co-worker. GitHub issues in Ekho-Labs/infra-work are the sole human control surface, driven entirely by labels.

THE PROBLEM

The organization-wide breaker opens after 3 consecutive failed runs and blocks every new claim. Deterministic human input errors counted the same as real failures — and once open, there was no way back:

"The current binaries don't expose a reset HTTP endpoint, so treat an open breaker as an operator-blocking condition until an authorized reset is available." — docs/operations.md (before this PR)

circuit.Service.Reset existed, fully wired with an authorizer — nothing in production called it.

THE FIX

1. A fifth designated control label, agent:circuit-reset: an authorized human (triage-or-higher and control-team member) adds it to any infra-work issue; the controller closes the breaker organization-wide. No generation claimed, nothing spent.

2. Breaker counting is now explicit per call site: deterministic intake rejections (blockRun) never touch the breaker; autonomous planner / worker / publication / verification failures (failRun) still count.

15
files changed
+183 / −29
lines
6
commits
5
control labels (was 4)
25 ok
packages, go test -race

02 Incident timeline

All timestamps UTC. Failure threshold: 3 consecutive failed runs.

Aug 12 · 15:29
infra-work #194 gets agent:ready with no kind label → run blocked: intake issue must have exactly one kind label
Aug 12 · 19:24
Same mislabel retried → blocked again
Aug 12 · 20:13
Third retry → threshold reached → ORGANIZATION CIRCUIT OPENS (persisted in Postgres circuit_states) circuit open
circuit open · every wake event denied · Aug 12 20:13 → Aug 17 20:34
Aug 17 · 00:21
agent:ready + kind:research added to #202 (Historical Port Traffic Data) → denied: organization circuit is open
Aug 17 · 00:57
Label removed and re-added (retry) → denied again
Aug 17 · 04:09
Authorized comment on #202 (wake event) → denied; delivery set aside after 12 attempts
Aug 17 · 20:34
Operator break-glass: UPDATE circuit_states SET open=false, consecutive_failures=0 → org unblocked circuit closed
Aug 17
PR #42: self-service authorized reset + mislabel counting fix this PR

03 Control flow

Swimlanes: GitHub → Intake → Controller → Circuit state (Postgres). Violet marks paths added by this PR; hover a node to inspect it.

existing flow new in this PR counts toward breaker resets failure count
GitHub
webhook delivery
issue labels + comments on infra-work · HMAC-signed · durable in ledger
Intake
authorize actor
org member / collaborator · triage+ · control-team member · label in designated set (now five: ready, pause, stop, plan-approved, circuit-reset)
qualifying event
handed to the controller state machine
re-authorize reset
FIX
IntakeResetAuthorizer
was a comment-kind request; now a control-label request naming agent:circuit-reset + designated set
Controller
HandleQualifyingEvent
switch on control label
agent:pause / stop
fence the issue and all targets
agent:plan-approved
approve the pending run's manifest revision
NEW
agent:circuit-reset
labeled → Circuit.Reset (re-authorized by the circuit service) · unlabeled = no-op · org-wide · no claim, no spend · idempotent for webhook replay
agent:ready → claim
EnsureClosed gate (rejects while open) → classify the intake issue
NEW
blockRun
deterministic intake rejection (missing / duplicate kind label, unknown risk label, kind:new-project) → new intake_rejected event → blocked + diagnostic + budgets settled · BREAKER UNTOUCHED
failRun
autonomous failure (planner, worker arms, publication, verification) → blockRun + RecordResult(failed)
run succeeds
RecordResult(succeeded) → failure count resets to 0
Circuit state
circuit_states (Postgres)
open flag · consecutive_failures · threshold 3 → OPEN · read by EnsureClosed before every claim
failRun ++count
success = 0
Reset: open=false, count=0
ORG-WIDE RESET
The intake issue only carries the signed, authorized command. The reset applies to the whole organization, not the issue it was placed on.
DOUBLE AUTHORIZATION
The intake layer authorizes the actor first; circuit.Service.Reset re-authorizes through the IntakeResetAuthorizer before touching state.
NO EDGE FROM blockRun
The deliberate absence of a blockRun → circuit edge is the fix: human mislabels can no longer open the breaker.

04 Changes by commit

1feat(state): add an intake_rejected event that blocks without a breaker count
internal/state/event.gointernal/state/machine.gointernal/state/machine_test.go
New intake_rejected event; transition {classifying, intake_rejected} → {blocked, no_automatic_retry}. Kept distinct from model_task_failure so it can never count toward the breaker.
2feat(controller): reset the circuit by control label and spare the breaker human mislabels
internal/controller/controller.gointernal/controller/types.gointernal/controller/arms.gointernal/controller/collect.gointernal/controller/circuit_reset_test.go
CircuitResetControlLabel handled before claim (no generation, no spend); Circuit interface gains Reset; block split into blockRun / failRun; arms and collect call sites now count explicitly. Unit tests: labeled → Reset forwarded with org + actor; unlabeled → no-op.
3feat(config): enroll agent:circuit-reset as a designated control label
internal/controller/config.gointernal/controller/control_labels_test.go
Appended to the default designated control labels, making it a valid qualifying event at the intake authorization boundary.
4fix(controller): authorize circuit resets as control-label events
cmd/controller/assemble.go
The IntakeResetAuthorizer was configured as a comment authorization request, contradicting the documented control-label reset semantics. Now a control-label request naming the reset label and the designated set.
5test(integration): three mislabeled generations leave the circuit closed
test/integration/control_plane_test.go
Replays the exact #194 incident against real Postgres with a real circuit service: three mislabeled generations → circuit stays CLOSED.
6docs: document the agent:circuit-reset label and breaker counting rules
README.mddocs/operations.mddocs/using-the-agent.md
20-label vocabulary, the reset procedure, and which failures count toward the breaker.

05 Findings

4 of 4 shown

circuit.Service.RecordResult and the CircuitStore use load-modify-save without a transaction; concurrent results or resets can lose updates.

Follow-up: make store mutations atomic.

source: oracle architecture review

The status-projection label exists only in the docs; no code applies it when the breaker opens. Pre-existing gap, out of scope for this PR.

source: oracle architecture review

The running controller image predates this fix; agent:circuit-reset activates on the next deploy. The production circuit row was already closed by operator break-glass, so the org is not blocked meanwhile.

Adding agent:circuit-reset while the breaker is closed only zeroes the failure count. Harmless and idempotent.

No findings match the current filters.

06 Verdict

MERGE · APPROVED
sisyphusanthropic/claude-fable-5 implementation + manual QAapproved
oracleunknown architecture reviewapproved
testsgo build · go test -race ./... 25 packages incl. Postgres-backed integrationgreen