Atelier : une panne qui ne se voyait pas - #42
Merged
Conversation
`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>
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.
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
connectedexistait 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.for awaitqui se terminait sans exception posaitendedsans écrire un mot : c'était le seul chemin de fin qui ne passait pas parappendSystem.AssistantTurnlisaitlevel,TimelineNodeViewne le lisait pas.Survivre
push, et le prompt de la relance partait dans un générateur que plus personne ne tirait.resumed'origine, jamais l'identifiant attribué à la session.snapshotne 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.La compaction
Au passage
-webkit-backdrop-filter. Dialogues, menus et drawers se lisaient au travers.corpus.test.tstombaient en dépassement — le délai avait vieilli, pas le code.Vérification
pnpm verifievert (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,/compactpendant une action, reprise d'une session après redémarrage.🤖 Generated with Claude Code