Skip to content

Atelier : une panne qui ne se voyait pas - #42

Merged
Shaenn merged 9 commits into
mainfrom
atelier-fiabilisation
Sep 3, 2026
Merged

Atelier : une panne qui ne se voyait pas#42
Shaenn merged 9 commits into
mainfrom
atelier-fiabilisation

Conversation

@Shaenn

@Shaenn Shaenn commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Neuf correctifs sur l'Atelier, tous partis du même constat : il n'avait aucun moyen de dire qu'il avait perdu le contact. La reconnexion SSE et la resynchronisation par snapshot étaient correctes ; entre la coupure et la reprise, l'écran continuait d'affirmer l'état d'avant.

Voir

  • Un flux coupé ne laisse plus l'écran affirmer ce qu'il ne sait plus. connected existait et n'était pas déstructuré par la page — la dernière phase reçue restait affichée, chrono compris, puisqu'il tourne côté client. Une session coupée pendant un compactage annonçait ainsi un compactage qui n'avait plus lieu.
  • Une session qui s'arrête le dit. Le for await qui se terminait sans exception posait ended sans écrire un mot : c'était le seul chemin de fin qui ne passait pas par appendSystem.
  • Un plantage ne se lit plus en gris. AssistantTurn lisait level, TimelineNodeView ne le lisait pas.

Survivre

  • Un tour envoyé après une mort n'est plus avalé. Le SDK ne referme pas la file qu'il tenait : son itérateur restait inscrit comme destinataire du prochain push, et le prompt de la relance partait dans un générateur que plus personne ne tirait.
  • La relance reprend le bon contexte, et le dit. Elle repassait le resume d'origine, jamais l'identifiant attribué à la session.
  • Une demande en vol survit à une coupure. Le snapshot ne portait ni les permissions ni les questions : une demande émise pendant une coupure ne revenait jamais, et l'agent restait suspendu jusqu'au garde-fou du quart d'heure.
  • Une session figée cesse d'occuper une place du parc. « Elle travaille » protégeait sans réserve ; le travail se prouve désormais par le pouls du SDK.

La compaction

  • Une compaction ne s'affiche plus deux fois. Le complément de la couture dédupliquait par comptage, alors que le fichier ne s'écrit pas dans l'ordre du direct.

Au passage

  • Le verre des panneaux flottants avait perdu son flou au passage du build : le minifieur ne gardait que -webkit-backdrop-filter. Dialogues, menus et drawers se lisaient au travers.
  • Quatre tests de corpus.test.ts tombaient en dépassement — le délai avait vieilli, pas le code.

Vérification

pnpm verifie vert (532 tests, dont 11 neufs sur la file d'entrée, la couture et le balayeur).

Vérifié aussi dans l'application, BFF servant le build : coupure du serveur en plein tour, retour du serveur, rechargement pendant une permission puis pendant un AskUserQuestion, /compact pendant une action, reprise d'une session après redémarrage.

🤖 Generated with Claude Code

Shaenn and others added 9 commits September 3, 2026 01:14
`EventSource` se reconnaît seul, et le `snapshot` qui suit remet tout d'aplomb :
la coupure n'est pas une panne. Mais tant qu'elle dure, la dernière phase reçue
restait affichée avec son chrono qui montait — il tourne côté client. Une session
coupée pendant un compactage annonçait ainsi un compactage qui n'avait plus lieu,
sous un statut « Au travail » figé à la dernière trame.

`connected` existait et n'était pas déstructuré par la page. Il ne suffisait pas
non plus : faux entre l'ouverture et la première trame, il aurait annoncé une
perte là où il n'y avait qu'une attente. D'où `lost`, qui ne se lève qu'après un
flux qui a vécu.

Les outils en vol s'arrêtent de battre du même geste : ils viennent d'`activity`,
que la coupure remet à `IDLE_ACTIVITY`.

Au passage : une erreur appartient au statut qui la porte. Sans cette remise à
zéro, celle d'un tour ancien restait sous le composeur pour le reste de la
session, y compris après une reprise réussie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deux défauts qui se répondent, et qui laissaient une panne sans témoin.

Le `for await` qui se terminait sans exception posait `ended` et n'écrivait
rien : c'était le seul chemin de fin qui ne passait pas par `appendSystem`. Une
session s'arrêtait donc sans un mot, et l'écran ne distinguait pas cette fin-là
d'un plantage. Un arrêt demandé, lui, a déjà dit ce qu'il était — d'où la garde
sur `stopped`.

Et quand le fil disait quelque chose, il pouvait le dire en gris :
`AssistantTurn` lit `level`, `TimelineNodeView` ne le lisait pas. Un crash sans
tour assistant pour l'accompagner — le cas du premier message — s'affichait sous
l'étiquette « Système », où il ressemblait à une note d'information.

S'y ajoute une trace sur la sortie d'erreur, horodatée et portant le `runId` :
une session qui s'arrête pendant qu'on regarde ailleurs ne laisse aucune trace
ailleurs, et c'est ce qu'il faut pour instruire une panne qu'on ne sait pas
reproduire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Quatre tests de `corpus.test.ts` tombaient en dépassement : les cinq secondes de
vitest ont été écrites pour des fixtures, alors que chacun relit soixante
transcrits réels d'un corpus qui ne cesse de grossir. Les propriétés tenaient
pourtant à chaque fois — c'est le délai qui avait vieilli, pas le code.

On allonge plutôt que de réduire l'échantillon : ce fichier est un filet, et un
filet qu'on rétrécit pour qu'il aille plus vite n'attrape plus rien.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…texte

Trois défauts qui ne se voyaient qu'ensemble, sur le chemin de la relance.

Le SDK ne referme pas la file qu'il tenait quand sa boucle meurt : son itérateur
restait inscrit comme destinataire du prochain `push`. L'ordre de `send()` étant
« pousse le prompt, puis relance la boucle », le tour de la reprise partait dans
un générateur que plus personne ne tirait — le message ne partait jamais et la
session restait « au travail » pour toujours. D'où `abandon()`, qui dénoue sans
fermer : la file survit à son consommateur.

La relance repassait le `resume` d'origine, jamais l'identifiant que la session
s'était vu attribuer. Une session née sans reprise recommençait donc une
conversation neuve : même fil à l'écran, aucun souvenir derrière. Et elle le
faisait en silence, ce qui est pire que la panne — elle le dit maintenant, et
elle dit lequel des deux cas s'applique.

Enfin `query()` était construit hors du `try`, et `run()` appelé par `void` : ce
que le constructeur du SDK lève en montant n'avait aucun destinataire. La session
restait « au travail » sans qu'aucun message ne vienne jamais.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le `snapshot` portait le fil, l'activité et les shells, mais pas ce qui attend
une réponse. Une demande de permission émise pendant une coupure du flux ne
revenait donc jamais : le bandeau n'apparaissait pas et l'agent restait suspendu
jusqu'au refus du garde-fou, un quart d'heure plus tard. Dans l'autre sens, une
demande réglée pendant la coupure laissait un bandeau fantôme dont le clic ne
pouvait plus rien dénouer. Elles font partie de l'état, au même titre que le
reste, et c'est la liste entière qui remplace celle du client — vide comprise.

Les deux constructions du `snapshot` n'en font plus qu'une : elles divergeaient
déjà, l'une portant un commentaire que l'autre n'avait pas.

`stop()` n'annonçait rien : la socket restait ouverte, son battement continuait,
et l'écran se figeait sur le dernier statut reçu. Il le dit maintenant — pour un
arrêt demandé d'un autre onglet, et pour l'extinction du serveur.

Le balayeur, lui, ne peut pas être annoncé : il ne ramasse que ce que plus
personne ne regarde, donc sans abonné à qui parler. C'est au front de constater,
et il le fait depuis le lien perdu — il va voir toutes les cinq secondes si la
session existe encore, et bascule sur « Reprendre » quand elle a disparu. Un
appel qui échoue ne conclut rien : le BFF est peut-être en train de redémarrer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t tu

Le balayeur ne ramasse jamais une session en cours de tour, et c'est juste : un
`Bash` de vingt minutes n'est pas un abandon. Mais ce statut protégeait sans
réserve, si bien qu'une session figée le gardait pour toujours — elle échappait
au balayeur *et* occupait une des six places du parc, sans qu'aucun geste ne
puisse la libérer.

Le travail se prouve désormais par le pouls du SDK : n'importe quel message
suffit, même ceux dont on ne fait rien. Une heure de silence lève la protection,
délibérément le double du délai d'inactivité — un garde-fou ne peut que faire
taire, jamais inventer une panne, et un tour vivant parle bien avant. Le
ramassage exige toujours les deux silences à la fois, celui du SDK et celui de
l'humain, et un onglet ouvert reste la raison de vivre la plus forte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…crit

Le complément de la couture — ce que le direct a vu après le dernier tour humain
et que le fichier n'a pas encore — se faisait par la seule longueur, et la
longueur mélangeait deux familles. Le fichier ne s'écrit pas dans l'ordre du
direct : la frontière de compaction y précède la ligne `/compact`. Comme il est
relu toutes les deux secondes et demie pendant une action, on le surprenait à
mi-chemin, une commande de moins que le direct — et le décalage laissait passer
la frontière du direct par-dessus celle du fichier.

Le fil montrait donc deux compactions là où la session n'en avait vécu qu'une,
et perdait au passage la ligne de commande qui l'avait provoquée.

Chaque famille se complète maintenant selon son propre critère : une compaction
se reconnaît à son `uuid`, que les deux sources lui donnent identique — c'est ce
dont `diskCaughtUp` se servait déjà — et une commande `/`, qui n'a d'identité
commune ni par sa clé ni par son horloge, se compte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Constater qu'une session n'existe plus nettoie l'adresse, et ce nettoyage est
exactement ce que le veilleur d'adresse lit pour écarter un avis périmé. L'avis
s'effaçait donc à l'instant où on le posait, et l'écran d'ouverture revenait sans
un mot d'explication — vu à l'écran, pas dans le code.

Le drapeau ne vaut que pour cette navigation-là : celles que l'utilisateur fait
ensuite retrouvent le comportement d'origine, qui reste nécessaire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Les dialogues, menus et drawers se lisaient au travers : on distinguait la barre
de session et le fil derrière le lecteur de ressources.

La cause n'était pas dans la règle mais dans sa compilation. Le minifieur tient
`-webkit-backdrop-filter` et `backdrop-filter` pour équivalents et ne garde que
la dernière des deux : écrite en premier, la propriété standard disparaissait du
CSS livré, et il ne restait que le préfixe, que ce Chrome n'honore pas. Le fond
translucide à 86 % restait donc seul, sans le flou qui le rend opaque.

Le repli `@supports` ne rattrapait rien, et ne le pouvait pas : il teste le
support du navigateur, acquis depuis longtemps, pas la présence de la règle.

Inverser les deux lignes suffit. Vérifié dans le CSS livré, puis à l'écran :
`backdrop-filter` calculé vaut de nouveau `blur(40px)`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Shaenn
Shaenn merged commit 0d14710 into main Sep 3, 2026
1 check passed
@Shaenn
Shaenn deleted the atelier-fiabilisation branch September 3, 2026 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant