A late transient 5xx classifies a fully productive dev session as an environment fault, pausing the run instead of retrying
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 52/100
Línea de trabajo
Comienza con tally() de tokens.py y la señal de tres estados delimitada en #611; después, sigue env_fault.py y escalation.py, incluida la clasificación de env-fault y la rama PAUSE. Usa la evidencia citada de productive timeout y el valor expired_clock como el caso que se debe cubrir. Se considera terminado cuando el uso medido distinto de cero evita la pausa de env-fault y sigue charge-and-retry, mientras que el uso no medible conserva el comportamiento actual.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Environment
- bmad-loop 0.11.1 (uv tool install), macOS 25.6.0 (Darwin), tmux 3.7c
- adapter:
claude,scm.isolation = "worktree",branch_per = "story" limits.session_timeout_min = 90,max_dev_attempts = 2
The bug
env_fault classification is a conjunction of two signals — an env_fault_patterns hit in
the last ENV_FAULT_TAIL_BYTES (64 KiB) of the pane log, and a terminal status in
ENV_FAULT_STATUSES (adapters/env_fault.py:55,65,169). Neither signal says anything about
whether the session did work.
escalation.py:135 then states the assumption the PAUSE rests on:
if result.env_fault:
# A transport/API failure (the CLI never reached the API, #194): the
# attempt did no real work, so pause for a human instead of charging
# it — re-arm resets the budget ...
That holds when the outage kills the session early — #194's shape, where dev-2 idled out
with tokens=0. It does not hold when a genuine, transient provider error lands in the
final 64 KiB of a session that has been productive for its whole clock. The session is then
a real timeout wearing an env-fault costume: the story is not retried (the budget is
re-armed but nothing re-drives), the run PAUSEs, and it stays paused until a human returns.
Unattended, that inverts the feature's purpose: #194 traded a wrongly-charged attempt for a
pause; here the pause is spent on a session that should simply have been charged and retried,
and the cost is however long the operator is away.
Evidence
Run 20260903-202834-5a20, story 3-4a, adapter claude.
The session was productive for its entire clock.
| signal | value |
|---|---|
session-start → session-end |
20:28:35 → 21:58:36 = exactly 90 min |
status |
timeout |
expired_clock |
both — wall and monotonic expired; the host never suspended |
tokens |
9,931,122 raw |
tokens_weighted |
1,280,747 |
| work produced | 22 files, 1296 insertions, 277 deletions, spec written with status: 'in-progress' |
expired_clock: "both" is worth singling out. The adapter already distinguishes "wall"
alone as the suspend signature (adapters/base.py:133); "both" is its opposite — the
monotonic clock also ran out, so the process was alive and scheduled for the full 90 minutes.
Combined with 1.28M weighted tokens, "the CLI never reached the API" is directly falsified by
data the journal already carries.
The pattern hit by 1,750 bytes.
The shipped claude.toml:59 pattern API Error: (5[2]9 Overloaded|5[0]0 Internal server error)
matched a real, transient 529.
| measurement | bytes |
|---|---|
| pane log size | 3,175,355 |
first Overloaded occurrence |
offset 3,111,569 |
| distance from EOF | 63,786 |
ENV_FAULT_TAIL_BYTES |
65,536 |
| margin inside the window | 1,750 |
The first 97.99% of the log is clean, productive work. Had the session emitted 1,751 more
bytes after the error, the pattern would have fallen outside the tail window, the result
would have been an ordinary timeout, and decide_dev would have RETRIED it — which, on
this repo's history, is the path that lands the story (two stories in a prior run,
3-2 and 3-7, both timed out on dev-1 and completed on dev-2).
So the classification here turns on how much output happened to follow the error, not on
anything about the failure.
Cost of the false PAUSE. run-paused at 21:58:36; the operator returned at 06:07 the
next morning. 8 h 09 min idle, zero tokens spent, one story's worth of work parked. The
run had completed 0 stories at that point.
Expected vs actual
- Expected: a session that consumed measurable tokens and exhausted its monotonic clock
is a story timeout, whatever appears in its last 64 KiB. It should be charged an attempt
and retried while budget remains. - Actual: a single late transient 5xx re-labels 90 productive minutes as "the attempt did
no real work" and pauses the run for a human.
Suggested direction
The missing signal is a veto, not a new pattern: measured-nonzero token usage should
disqualify the env_fault verdict (or at minimum the PAUSE branch in decide_dev), falling
back to the ordinary charge-and-retry path.
This needs exactly the machinery #611 is already scoped to build — the tri-state token
signal ("measured zero" / "measured nonzero" / "not measurable"), blocked today by
tokens.py's tally() collapsing "known zero" and "could not read the transcript" on the
claude-jsonl parser. #611 aims that signal one way (classify a zero-token timeout as
an env fault); this issue is the same signal aimed the other way (refuse the env-fault verdict
when tokens are measured nonzero). Both directions fall out of the same tri-state, and
"not measurable" preserves today's behaviour, so the veto adds no new false-negative surface.
expired_clock is a second, already-recorded corroborator: "both" means the session was
alive for its full budget, which is incompatible with "the CLI never reached the API".
A cheap interim guard, if the tri-state is too large: gate the PAUSE on
tokens_weighted in (None, 0). That is the mirror of the interim mitigation proposed in #194
itself ("a session that ends timeout with zero token usage is almost certainly an
environment fault"), and it would have classified this run correctly.
Related
- #194 — introduced the env-fault branch this issue is the inverse of; its own text
already floats token usage as the discriminator. - #611 — the tri-state token signal this fix needs, aimed the opposite way.
- #617 — same pattern, same 5xx line, but the false positive there is a quoted error in
ordinary prose; here the error is real and the misclassification is about when it arrived. - #508 — other residuals in the same classifier's tail window.
- Lenguaje dominante
- Python
- Estrellas
- 165
- Forks
- 69
- Merge medio
- 1 d 3 h
- PR fusionados (30 d)
- 75
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de bmad-code-org/bmad-loop
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 69/100
bmad-code-org/bmad-loop#973 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
bmad-code-org/bmad-loop#965 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
bmad-code-org/bmad-loop#835 ·
Los mantenedores suelen responder en 1 día
-
tickets: an injected plan_halt result for a built plan escalates with stopped-attempt wordingAbierto
Dificultad 4/5 3-5 días Aptitud para principiantes 36/100
bmad-code-org/bmad-loop#972 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 1-2 días Aptitud para principiantes 49/100
bmad-code-org/bmad-loop#971 ·
Los mantenedores suelen responder en 1 día
Todos los issues de bmad-code-org/bmad-loop
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
Arekkazu/sgpmp-backend#744 ·
Los mantenedores suelen responder en 1 día
-
Review backlog (security): master-branch remote guard trusts unbound provenance package fieldAbiertoagent/reviewer
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
projectbluefin/utah-packages#634 ·
Los mantenedores suelen responder en 1 día
-
enhancement good first issue help wanted
Dificultad 2/5 1-2 días Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
home-assistant/core#188613 ·
Los mantenedores suelen responder en 1 día
-
WebsiteAbiertogood first issue
Dificultad 2/5 1-3 horas Aptitud para principiantes 91/100
TanbirRamim/open-source-radar#314 ·
Los mantenedores suelen responder en 1 día