fix(bin): keep the long pause cadence for a live declared-pause agent - #2419
Open
Kallas95 wants to merge 3 commits into
Open
fix(bin): keep the long pause cadence for a live declared-pause agent#2419Kallas95 wants to merge 3 commits into
Kallas95 wants to merge 3 commits into
Conversation
A declared paused: was ignored by supervision while the agent was still alive on a healthy idle pane, so a legitimate long external wait raised repeated possible-wedge stale alerts. pause_state_class only returned the bounded pause cadence for an exited agent; a live agent fell through to the none (surface) path. Now a declared pause absorbs on the long cadence whether the agent is alive or has exited, while a live agent at a captain-held (non-pause) gate still surfaces once. Endpoint-death detection keeps priority: a paused agent that subsequently dies still re-surfaces on the bounded cadence for a recheck, and an agent that resumes working returns to normal wedge cadence.
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.
Intent
Fix: a declared paused: status is ignored by supervision while the worker is still alive, so a legitimate long external wait raises repeated stale alerts. Expected: when a task's last classified status event is a declared paused: (verb FM_CLASSIFY_PAUSED_VERB, default paused), the watcher applies the long bounded recheck cadence even if the pane/agent is alive - exactly the semantics already served to exited agents. Safety bounds (must not weaken real-blockage detection): the long cadence applies only to an otherwise healthy and idle pane; any event after paused: (new status line, turn-end, activity) returns to normal cadence; a paused: must never mask an agent that subsequently dies - endpoint-death detection keeps priority over the long cadence; do not touch blocked: semantics or wedge escalations for panes without a declared pause. Established the exact owners in bin/fm-watch.sh (pause_state_class) and bin/fm-classify-lib.sh (status_is_paused). Delivery: this repo's known constraint - origin kunchenguid/firstmate has no write access, machine firewall refuses push to master/main and refuses git rebase; push only branch fm/fm-paused-live-agent to the fork https://github.com/Kallas95/firstmate by explicit URL (worktree remotes unchanged), open the upstream PR against kunchenguid/firstmate:main. The upstream repo runs no CI on fork PRs: local green validation + upstream PR open is the terminal state; close the CI wait by the pipeline's supported cancel path without waiting for checks that will never arrive.
What Changed
pause_state_classinbin/fm-watch.shnow guards both live-agent short-circuits withstatus_is_paused, so a task whose last classified status is a declaredpaused:stays on the boundedPAUSE_RESURFACE_SECSrecheck instead of dropping back to the normal cadence and re-escalating everySTALE_ESCALATE_SECSwhile the worker is alive. A live agent at a captain-held (non-pause) gate still surfaces once, and aworkingcrew state or a non-pause status line still returns the pane to the normal cadence.tests/fm-watch-triage.test.sh: reworked the live-gate case intotest_exited_declared_pause_is_bounded_but_live_gate_absorbed, which now asserts the live declared pause absorbs (no wake printed or enqueued, pause marker recorded, wedge timer never started) and keeps that cadence on re-arm past the wedge threshold. Addedtest_live_paused_agent_death_still_rechecks, which absorbs a live pause, then kills the agent and asserts the pane re-surfaces as a paused recheck instead of staying hidden behind the pause or being labeled a possible wedge.docs/architecture.mdand thePAUSE_RESURFACE_SECS/pause_state_classcomments describe the split: declared pauses use the bounded cadence whether the agent is alive or exited, while a captain-held endpoint joins it only when the backend confidently reports the agent dead.Risk Assessment
✅ Low: Correctif à deux gardes strictement délimité par status_is_paused au point d'ancrage désigné par l'intention (pause_state_class), dont chaque borne de sécurité requise (cadence longue seulement sur pane inactif, retour à la cadence normale sur tout événement, mort non masquée via recheck borné, sémantique blocked:/wedge intacte) est vérifiée dans le source et exercée par des tests comportementaux pilotant un vrai sous-processus fm-watch.sh.
Testing
J'ai exercé la suite propriétaire du watcher (tests/fm-watch-triage.test.sh, 50 ok / 0 échec) puis, parce que des tests verts ne prouvent pas à eux seuls l'expérience utilisateur, j'ai construit un scénario de supervision end-to-end qui pilote le véritablebin/fm-watch.shsur la fixture tmux/fm-crew-state hermétique du dépôt et rejoue ce que vit un captain pendant une longue attente externe légitime : le même scénario exécuté contre le binaire du commit de base réveille le captain 6 fois sur 6 avecstale: fleet:fm-vendor-quota, contre le commit cible 0 fois sur 6, le watcher inscrivant à la place ses décisionsabsorbed stale (paused, awaiting external, age Ns)dans son journal de triage — la régression est donc reproduite avant le correctif et absente après. J'ai ensuite vérifié end-to-end les quatre bornes de sécurité de l'intent (sémantiqueblocked:et escalade wedge inchangées et identiques au commit de base, mort d'agent après pause toujours détectée et jamais requalifiée en wedge, retour à la cadence normale au premier nouvel événement de statut, verbeFM_CLASSIFY_PAUSED_VERBconfigurable honoré). Aucun échec, aucun test instable, aucun problème d'environnement à corriger. Ce changement ne touche aucune surface visuelle :fm-watch.shest un superviseur shell dont la sortie utilisateur est un flux de raisons de réveil en terminal, donc les preuves sont des transcriptions CLI et le journal de triage persisté plutôt que des captures d'écran. À noter, un hook de pare-feu local bloquebash tests/<fichier>en analysant le contenu du script (faux positif sur unrm -f "$err"interne à la suite) ; la suite a néanmoins été exécutée complètement une fois, et la preuve rouge/vert contre le code de base a été obtenue au niveau produit via les transcriptions A/B. L'arbre de travail est propre : le répertoire temporaire.evidence-baseline/créé pendant les tests a été supprimé.Evidence: Comparaison avant/après : ce que le captain voit pendant une pause déclarée sur agent vivant
A declared paused: while the agent is STILL ALIVE - what the captain actually sees Same scenario, same fixture, same fake backend. Only bin/fm-watch.sh differs. Task fleet:fm-vendor-quota, last status event: paused: waiting on the upstream vendor to approve the API quota bump Agent alive (pane_current_command=grok), pane healthy and idle 500s (> 240s wedge). look | BEFORE (base f1a4af4) | AFTER (target 48df450) -----+---------------------------------------+---------------------------------------- 1 | ALERT: stale: fleet:fm-vendor-quota | quiet - not interrupted 2 | ALERT: stale: fleet:fm-vendor-quota | quiet - not interrupted 3 | ALERT: stale: fleet:fm-vendor-quota | quiet - not interrupted 4 | ALERT: stale: fleet:fm-vendor-quota | quiet - not interrupted 5 | ALERT: stale: fleet:fm-vendor-quota | quiet - not interrupted 6 | ALERT: stale: fleet:fm-vendor-quota | quiet - not interrupted -----+---------------------------------------+---------------------------------------- sum | 6 stale wakes delivered | 0 stale wakes delivered log | triage log empty (nothing absorbed) | 8 x absorbed on the pause cadence Absorb lines the fixed watcher wrote to .watch-triage.log: [2026-08-15T08:48:11+0200] absorbed stale (paused, awaiting external, age 22s): fleet:fm-vendor-quota [2026-08-15T08:48:12+0200] absorbed stale (paused, awaiting external, age 23s): fleet:fm-vendor-quota [2026-08-15T08:48:14+0200] absorbed stale (paused, awaiting external, age 25s): fleet:fm-vendor-quotaEvidence: Bornes de sécurité vérifiées sur le watcher corrigé (blocked intact, mort non masquée, retour cadence normale)
= SAFETY BOUNDS - watcher: AFTER the fix - target commit 48df450 = A. pane with NO declared pause (last event: blocked: ...), agent still alive first look : SURFACED -> stale: fleet:fm-blocked pause cadence marker .paused-fleet_fm-blocked taken: no <- blocked: never borrows the pause cadence idle 500s : SURFACED -> stale: fleet:fm-blocked (idle 500s, possible wedge, escalation 1) B. declared pause on a live agent, and then that agent DIES live pause : quiet (absorbed, captain not interrupted) now dead : SURFACED -> stale: fleet:fm-gate (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) C. declared pause absorbed, then the crew posts a NEW status event paused : quiet (absorbed, captain not interrupted) new event : SURFACED -> signal: .../pause-then-event/state/res.status next look : SURFACED -> stale: fleet:fm-resume pause cadence marker .paused-fleet_fm-resume still held: no <- back to the normal cadenceEvidence: Transcription complète AVANT le correctif (code de base f1a4af4) — 6 alertes stale sur 6 regards
Evidence: Transcription complète APRÈS le correctif (48df450) — 0 alerte, absorptions inscrites au journal de triage
Evidence: Verbe de pause configurable (FM_CLASSIFY_PAUSED_VERB=on-hold) — même cadence longue
Evidence: Harnais de reproduction du scénario principal (rejouable contre n'importe quel bin/)
Evidence: Harnais de reproduction des bornes de sécurité
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-watch.sh:397- Dans le fast path de pause_state_class (bin/fm-watch.sh:396-397), fm_backend_agent_alive est sondé à chaque poll (défaut FM_POLL=15s) même quand le dernier statut est paused:, alors que dans ce cas le résultat de la sonde ne peut plus changer la décision (la condition modifiée court-circuite sur status_is_paused). Tester d'abord! status_is_paused "$last"(pur test de chaîne) et n'exécuter la sonde backend qu'ensuite éviterait une requête backend (tmux/herdr) toutes les 15 s par pane en pause pendant toute une longue attente externe. Attention : uniquement le bloc fast-path (lignes 395-402) ; le second bloc (ligne 413) doit conserver sa sonde car agent_alive y est consommé par la promotion none+dead→paused de la ligne 420.tests/fm-watch-triage.test.sh:900- Interprétation de la borne « endpoint-death detection keeps priority over the long cadence » : après la mort d'un agent absorbé en pause vivante, la détection de mort ne devance pas la cadence — la re-surface n'arrive qu'à la prochaine fenêtre PAUSE_RESURFACE_SECS (≤1h par défaut, ancrée sur le mtime du fichier de statut), jamais immédiatement. C'est identique à la sémantique préexistante des agents exités que l'intention désigne comme référence (« exactly the semantics already served to exited agents »), et le nouveau test test_live_paused_agent_death_still_rechecks encode délibérément ce compromis (re-surface bornée étiquetée « paused recheck », jamais masquée, jamais « possible wedge »). Aucune action requise ; noté pour que l'auteur puisse confirmer cette lecture.✅ **Test** - passed
✅ No issues found.
bash tests/fm-watch-triage.test.sh— suite propriétaire du triage watcher : 50 ok, 0 échec, sortie 0 (couvretest_exited_declared_pause_is_bounded_but_live_gate_absorbedréécrit et le nouveautest_live_paused_agent_death_still_rechecks)Scénario produit A/B contre le vraibin/fm-watch.sh:bash paused-live-agent-scenario.sh <worktree> <EV>/baseline/bin "BEFORE"(bin extrait de f1a4af4 viagit archive) → 6 alertesstale:sur 6 regardsbash paused-live-agent-scenario.sh <worktree> <worktree>/bin "AFTER"→ 0 alerte sur 6 regards, 8 lignesabsorbed stale (paused, awaiting external…)dans.watch-triage.logbash safety-bounds-scenario.sh <worktree> <worktree>/bin— phase A : paneblocked:+ agent vivant surface puis escaladepossible wedge, escalation 1et ne prend pas.paused-*bash safety-bounds-scenario.sh— phase B : pause vivante absorbée, puis agent mort → resurface enstale: … (paused 501s, awaiting external … not a wedge), aucune mentionpossible wedgebash safety-bounds-scenario.sh— phase C : nouvel événement de statut aprèspaused:→ réveilsignal:immédiat puisstale:au regard suivant,.paused-*relâchébash safety-bounds-scenario.sh <worktree> <EV>/baseline/bin— phase A rejouée sur le code de base : sortie identique, sémantiqueblocked:/wedge inchangéeFM_CLASSIFY_PAUSED_VERB=on-hold ROUNDS=3 bash paused-live-agent-scenario.sh … <worktree>/bin→ 0 interruption, le verbe configurable obtient la même cadence longuegit status --porcelain --untracked-files=allaprès nettoyage du répertoire transitoire.evidence-baseline/→ arbre de travail propre🔧 **Document** - 1 issue found → auto-fixed ✅
tests/fm-watch-triage.test.sh:895- Le commentaire de test ajouté par ce changement contient un mot français dans une prose anglaise : « endpoint-mort detection keeps priority over the long cadence » devrait lire « endpoint-death detection ». Non corrigé parce que cette phase ne peut pas modifier les tests ; correction triviale à appliquer par la phase qui possède tests/fm-watch-triage.test.sh.🔧 Fix: docs: fix endpoint-death typo in pause cadence test comment
✅ Re-checked - no issues remain.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.