Backend-error classification needs a real mechanism, not hard-coded strings (#164 points 3 and 4) #201
Closed
opened 2026-08-31 05:57:59 +02:00 by ltms
·
3 comments
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#201
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up to #164, which closed with points 1 and 2 done (
3bfa828,4ac688b). Points 3 and 4 stay open and would otherwise be lost with that close.Where things stand
CompletionResolvernow classifies a completed-but-unreplied turn through a fixed chain:MIN_TURN_NANOS, 2s) →FAILEDFAILEDexhaustedPattern→BACKEND_EXHAUSTED(+ credential quarantine)BACKEND_ERROR=(?i)\bAPI Error\s*:→FAILEDStep 5 is the part that needs replacing.
Point 3 — surface backend errors properly
The current pattern is one hard-coded string, and that was a deliberate stopgap, not a design. It fails in the silent direction: when a backend rewords its error, the pattern stops matching and the scrape goes back to resolving as a success. That is the exact defect #164 was filed about, reintroduced by drift, with nothing to signal it.
Step 4 already shows the shape this should take.
exhaustedPatternis per profile and configured, so a backend's wording lives next to that backend's definition, andCompletionResolver.coverage(...)logs at startup which profiles have one and which do not. Step 5 has neither.Suggested direction:
errorPatternbeside the existingexhaustedPattern, resolved through the sameExhaustedPatternLookup-style seam.(?i)\bAPI Error\s*:as the default when a profile configures nothing, so removing the hard-coded string never silently weakens an unconfigured profile.Note the ordering constraint: exhaustion must keep winning over a generic error, because only
BACKEND_EXHAUSTEDquarantines the credential. A merged pattern would lose that.The dead-code question that belongs here
The rescued branch (
851ebca) carried avisibleTurnraw-screen fallback, so a TUI-hidden error line could still classify whenlastAssistantBlockparses blank. On the current chain the step-2 empty fail fires first, so that fallback can never run.Making it live means reordering the chain — moving pattern checks ahead of the empty check. That changes which classification wins for a whole class of turns, so it needs deciding here rather than being slipped into an unrelated change. It was correctly left out of #164.
Point 4 — quarantine a profile whose first turn fails
From #164: a profile whose first turn 400s stays
readyin/membersand keeps accepting sends. Every send burns a spawn and returns a failure.The machinery already exists —
ExhaustionSinkquarantines a credential onBACKEND_EXHAUSTED, andfleet_listreportsfree: 0withcredentialIdandquarantinedForSeconds. This is about deciding whether a hard backend rejection should feed the same sink, and if so with what backoff.It is not obviously a yes. Exhaustion is a known-temporary refusal with a natural retry window; a 400 on a malformed request body is a permanent config error that will fail identically on every retry, so a timed quarantine is the wrong shape for it. A profile marked unusable until its config changes may fit better than one that silently comes back and fails again.
Why this is not urgent
The costly half of #164 is fixed: a lost turn can no longer reach a caller labelled as a successful empty reply. What remains is coverage and rot-resistance — real, but it degrades gracefully, and the failure is now a wrong classification rather than a silent data loss.
Refined into five units — design is on main
An architect member read this issue, #227, and the named source, and split the work. Full design:
docs/CB-201-227-Refinement.md(merged to main as7662e2d).#201 and #227 are one delivery program, but not one implementation unit.
CompletionResolvercan publish a typed backend-error event once its captured waiter resolution wins; #227 consumes that event without knowing any pane text. The classifier must land before the final wiring, but the policy, the roster state and the lead nudge can be built beside it.Units 1 to 4 have disjoint file ownership. Unit 5 owns every composition file, including
Fleetd.java, and starts only after Units 1 to 4 and #234 land.Policy — first values
credentialId— never profile name, never error textOne active incident per credential. Errors during cool-off neither extend it nor raise another notice. After expiry, two fresh errors are needed to rearm. A single error still fails its send and marks that member
backend_error; it does not cool the credential.Two is the smallest threshold that protects an honest one-turn failure. 60 seconds fits the measured two-member outage. A 60-second cool-off is deliberately not the 1800-second exhaustion quarantine — a short fault is not a spent credential.
Findings that changed the plan
CompletionResolver.classifyRawScrapeFallback,:260-276,:332-367). Do not rebuild or reorder it.CompletionResolver.resolvefails at:229-237before the normal pattern checks, so a fast backend failure is only a generic failure today. The classifier must cover that path.:288-317. That order stays.:311-316) — a valid member report can quote anAPI Error:line. This is the main policy risk once two matches remove capacity.MessageService.java:922-940), but would not say those failures are one outage.ReplyPushLoopalready combines replies, terminal tickets and questions on one per-lead schedule (:20-48,:305-395). A new direct injector would race it.BackendQuarantinerestarts a long cooldown per exhaustion (:60-87) — wrong store and wrong name for a short outage.quarantined(PlacementContext.java:10-22) — reusing it would make refusal text say "backend exhausted".MemberSessionhasDONEand genericFAILED, no reason (:51-59);rosterViewcannot preserve the cause after the ticket is gone (:648-687).SessionManager.onTurnComplete(:715-755), so a backend-error update must handle bothBUSYandDONE.FleetMcp.capacityViewalready shows the right shape for exhaustion (:996-1025); outage needs parallel fields with different names.FleetHealthMonitor.healthCoverage(:206-208) must not change tofullbecause of this.Riskiest assumption
That a configured regex means the backend failed this turn. Two false matches would cool a healthy credential. Cheapest experiment: replay the saved 2026-09-01 pane, one deliberate disposable failure per adapter, and one valid member report quoting each error line, through the real
CompletionResolverfixture. Outage panes must match; quoted reports must not. If quoted reports still match, narrow the patterns — do not raise the threshold to hide weak classification.Explicitly not building
Reuse of
BackendQuarantine; merged exhaustion/error patterns; grouping by error text; marking a profile permanently unusable (the classifier cannot yet separate malformed input from a transient fault); cooling on one generic error; putting the detector inFleetHealthMonitor; a second lead injector; persisting incident history across restart; work recovery; the oldvisibleTurndirection (#211 superseded it); and removing the legacyAPI Error:fallback in the first release.Units 1–4 are now delegated in parallel.
Done and merged. All five units are on
main.26bafe8959c835BackendOutagePolicy— two distinct targets inside 60s cools the credential for 60se5eb353c3672f5ac47498errorPatternconfig key, the spawn gate, and thefleet_list/fleet_profilesviewsPrerequisite
#234(theExhaustionSinkinversion) landed first as838a701, on purpose: it narrows a type, so it turned every silent collision in the parallel units into a loud compile error at merge time instead of a green build that ships a dead feature.Build on
mainafter the last merge: 1215 tests, 0 failures, 0 compile errors, BUILD SUCCESS.The hard-coded
API Error:string is gone as the only mechanism. A profile now declareserrorPattern:infleetd.yaml; a profile that declares none falls back to the built-in compatibility pattern, so this is never silently "off". A malformed pattern is rejected at startup by name.Three things worth recording, because each was nearly missed:
.contains("quarantined")— true of both the correct message and the fallback message the mutation produced, so it passed under correct and broken code alike. The worker found this, strengthened the assertion to an exactassertEquals, re-ran with the mutation still applied to confirm it now fails, and kept the stronger assertion. That is the right handling and it was reported rather than buried.#234had made the 3-argumentExhaustionSinkmethod the single abstract one, and a 2-argument lambda in a shared test file was now illegal. Git compares text; a lambda's arity is a type fact. Only the compiler finds it.Fleetd.javacall site leaves all 1215 tests green with 0 compile errors.BackendOutageFlowTestis a good test, but it copiesFleetd.main's sink lambda — so it proves the copy, and cannot notice the original being deleted. Filed as #248 and already delegated.Closing this and #227 together. A
wiki/11-Features.mdentry for theerrorPatternknob is still owed and is mine to write.Follow-up fix after this closed —
coverage()named the wrong config keyMerged as
9d37f3aonmain(1229 tests). Recording it here because the ticket has the context.I found this by reading the boot log after redeploying this feature, not from a test. Unit 5 added a second classification line for the new
errorPatternknob, and both lines came out of the same helper. The new line said:It is the
errorPatternline. It named the other knob. Anyone acting on that message would have set the wrong key infleetd.yaml, seen no change, and had no way to work out why — the message would still have been wrong after the fix.The cause was a shared helper that hardcoded one key name while serving two callers:
How I fixed it matters more than the fix
The tempting fix is an overload — add a three-argument
coverage(patternKey, ...)and leave the old two-argument one alone. That is exactly the trap recorded in this repo already: a new method that existing callers silently ignore, shipping dead with a green suite.So I changed the signature instead of adding to it:
The compiler then found all three call sites and made me pass
"exhaustedPattern"and"errorPattern"explicitly. A caller cannot get the old wrong behaviour by doing nothing, because doing nothing no longer compiles. That is the property worth having, and it is only available before the overload exists.Test that pins it, in
CompletionResolverTest:Mutation-checked: reverting the production fix turns it red at
:800, with 0 compile errors — so it is a real kill, not a broken build reporting zero failures.Verified live. Redeployed to pid 36650, jar
b8af736f44fc; both classification lines now name their own key, and the same log file holds the before (12:26) and the after (12:30) for comparison.The general point
This is the second defect in this ticket's area that no test could have caught, because both were about a message being wrong, not a behaviour being wrong. A log line is a user interface for the operator. It is worth reading your own log after deploying, once, as a deliberate step — the two minutes it costs found both of these.