Treat a slow backend as unavailable and advance the chain #124
No reviewers
Labels
No labels
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/agent-proxy!124
Loading…
Reference in a new issue
No description provided.
Delete branch "saturation-failover"
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?
Summary
Closes #108, options 2 and 4 - the pair you recommended.
The 2026-08-12 outage: a game on the local GPU host starved local inference. Echo failed 100% of turns for ~2.5 hours at ~180s each while Deep answered
pingin 2.18s through the same proxy.Two gaps, either one sufficient:
GET /api/tagsreturns 200 in 6.7ms while the GPU is fully occupied.Fix
PROXY_BACKEND_SLOW_AFTER- seconds an attempt may run before its backend counts as saturated. Defaults to 0 (off), so nothing changes until Deploy sets it.Past it: the attempt is abandoned, the chain advances to the next tier rather than retrying the same backend, and the breaker records a failure so later requests skip it for the cooldown. A backend nobody can get a token out of is unavailable, whatever it says about itself.
This is what makes the cloud failover in
sirens-echo#81reachable. It could not help before, because from the proxy's point of view nothing had failed yet.Only the wait is bounded. On the streaming path the threshold covers time to the first chunk - a stream that has started is making progress, and cutting it for being long would be the opposite of what you asked for. The non-streaming path has no first-token signal, so it bounds the whole attempt; the doc says plainly that the value must sit above the slowest legitimate completion on that route.
Saturation and a spent budget stay separable. Budget exhausted answers 504 and does not fail over, because there is no time to fail over into. Backend slow with budget remaining advances the chain.
Saying so (option 4)
Through #104's channel, as it happens:
Plus
agentproxy.outcome=saturatedandagentproxy.backend.regime=saturatedon the attempt span,llm_backend_saturated_total{logical_model,backend}, anddispatch.backend_saturated.How to verify
ward exec test-tests/test_saturation.py, 7 cases, no test sleeps longer than 50ms.test_a_quiet_backend_fails_over_instead_of_being_waited_onis the outage shape: the tower goes quiet, the hosted tier answers.test_a_saturated_backend_stops_receiving_workasserts the breaker opens.test_a_spent_budget_is_a_deadline_not_a_saturationpins the distinction.test_a_stream_already_generating_is_not_cut_for_being_longpins that only the wait is bounded.Test plan
ward exec format-check,lint,typecheck,pre-commitall cleanWhat this does not do
It reacts rather than prevents. Options 1 and 3 in the issue are what would prevent:
/api/tagsstill reports presenceFor August 19, this makes the failure legible and recoverable within one turn instead of silent for three minutes. It does not replace either mitigation you named: pinning Echo to a non-local tier, or dedicating the GPU. Both are still worth doing, and neither is automatic.
Follow-up for Deploy
PROXY_BACKEND_SLOW_AFTERneeds a value to do anything. Something comfortably above the slowest normal completion on the local tier and comfortably below the caller's own deadline is the shape; I have not measured a number and would rather you pick it than guess.Note
Branched off
sse-heartbeats(#123), tip of the chain #118 -> #119 -> #120 -> #122 -> #123. Thebackend_saturatedheartbeat needs #123's channel.Pull request closed