Skip to content

La Passerelle : piloter l'Atelier depuis une messagerie - #32

Merged
Shaenn merged 28 commits into
mainfrom
chantier/passerelle
Aug 20, 2026
Merged

La Passerelle : piloter l'Atelier depuis une messagerie#32
Shaenn merged 28 commits into
mainfrom
chantier/passerelle

Conversation

@Shaenn

@Shaenn Shaenn commented Aug 20, 2026

Copy link
Copy Markdown
Owner

La Passerelle relie une messagerie à l'Atelier : lancer une session, la
surveiller, la débloquer, quand on n'est pas devant le poste. Elle est
éteinte par défaut, n'ouvre aucun port, et refuse de démarrer sans sa liste
blanche de conversations.

Ce que la branche apporte

  • Piloter et consulter. Ouverture de session sur un projet déjà connu de
    Claude Code, tour ordinaire sans syntaxe, /sessions, /etat, /stop,
    /fin — et une navigation dans l'arborescence d'un projet qui n'ouvre
    aucune session.
  • Rendu riche. Markdown traduit en messages structurés, tableaux compris,
    pagination des documents longs, repli sur le texte brut plutôt que la perte.
  • Décider de loin. Permissions, plans lus avant approbation, questions à
    choix simple ou multiple, échéance du quart d'heure qui refuse par défaut.
  • Fenêtre de contexte. /etat et /compacter, avis de compaction avec son
    résumé replié, et un seul avis au passage des 80 %.
  • Bulle d'activité pendant un tour, avec les libellés de l'Atelier.

L'usage recommandé, et ses limites

Ce qui transite passe par les serveurs de la messagerie, sans chiffrement de
bout en bout. Le manuel, SECURITY.md, le README et CLAUDE.md disent
maintenant que la Passerelle vise l'usage personnel et n'est pas recommandée
en contexte professionnel
, et nomment la piste cherchée pour s'en passer —
un réseau privé rendant l'Atelier joignable depuis un téléphone, ce qui
supposerait d'adapter l'interface à cet écran.

Vérifications

pnpm lint, pnpm typecheck, pnpm test (518 tests) et pnpm format passent.

Fusion attendue par merge commit — pas de squash : les correctifs commités
sur la branche doivent rester lisibles. v1.3.0 sera posé sur le merge.

🤖 Generated with Claude Code

Shaenn and others added 28 commits August 18, 2026 21:44
Le streaming input du SDK était déjà en place — `agent/queue.ts` est
exactement ce mécanisme, et `POST /send` le nourrit. Ce qui manquait
n'était pas le tuyau mais la porte.

Les channels MCP, qui semblaient la réponse, n'ont pas pu la fournir :
`Options` du SDK n'expose aucun champ `channels`, `createSdkMcpServer`
n'accepte pas de capability `experimental` — donc le serveur in-process
d'`agent/ask.ts` ne peut pas en être un — et un channel ne livre que
dans une session déjà ouverte, jamais pour en ouvrir une.

D'où cette forme : la Passerelle appelle le registre directement, dans
le même process. Son long-polling est sortant, si bien qu'aucun port ne
s'ouvre — l'écoute reste `127.0.0.1` et `guard.ts` ne bouge pas d'une
ligne.

Ce que cela coûte, et qui est assumé : un secret entre dans AURA, elle
appelle un service externe, et qui écrit dans une conversation autorisée
obtient un accès distant au poste. La liste blanche est la garde, pas une
commodité — sans elle, la Passerelle refuse de démarrer.

Une session pilotée d'ici reste abonnée au runner : c'est l'abonnement,
et lui seul, qui la protège du balayeur des trente minutes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La fonctionnalité était livrée sans sa page. Le manuel est une surface à
part entière — il alimente le tiroir contextuel, la page /aide et le site
vitrine —, et une capacité qui ouvre un accès distant est précisément
celle qu'on ne peut pas laisser sans mode d'emploi.

Une page transversale (`routes: []`) plutôt qu'une section de l'Atelier :
la Passerelle n'a pas d'écran, et ce qu'elle demande de comprendre — ce
qu'elle coûte, ce qu'elle n'ouvre pas — ne se lit pas en marge d'autre
chose. La barre latérale du site et l'ordre du manuel s'en déduisent
seuls, il n'y avait rien à inscrire ailleurs.

Trois documents affirmaient par ailleurs quelque chose qui vient de
devenir faux, et un document de sécurité qui ment est pire que muet :

- README — « aucun appel sortant », « aucun secret à fournir », et deux
  variables de configuration là où il y en a désormais cinq ;
- SECURITY — « ni compte, ni mot de passe, ni service externe », qui
  décrivait la seule frontière du modèle.

Chacun dit maintenant ce que la Passerelle change, ce qu'elle ne change
pas, et que tout reste éteint tant qu'aucun jeton n'est posé.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Elle parlait à Telegram par `fetch` : deux cents lignes de long-polling,
d'acquittement et de reprises, qui marchaient. Ce n'est pas pour elles
qu'on prend la dépendance, mais pour ses **types**.

L'API Bot accepte les champs qu'elle ne connaît pas et les ignore en
silence. Un `header` écrit pour `is_header` ne produit donc aucune
erreur — seulement un tableau sans en-tête, sans rien pour dire pourquoi.
C'est exactement l'erreur qui a été commise, et que rien à l'exécution
n'aurait signalée. Le compilateur, lui, la refuse.

Version 2.0.0, réécriture complète, **zéro dépendance transitive**.

Ses types sont pourtant faux sur deux points de l'API riche, et il a
fallu les corriger localement (voir `passerelle/riche.ts`) : son
`RichText` n'admet ni chaîne nue ni tableau — ce qui rend inexprimable un
paragraphe de texte simple —, et `align`/`valign` y sont requis alors
qu'une cellule sans eux est acceptée. Le reste est juste, et c'est le
reste qui comptait.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Telegram n'affiche pas le Markdown, et son HTML n'a ni titre, ni liste,
**ni tableau** — `<table>` y est refusé net. Un tableau rendu en chasse
fixe se disloque dès qu'il dépasse la largeur d'un téléphone : les
colonnes passent à la ligne et l'alignement, sa seule raison d'être,
disparaît.

`sendRichMessage` est une autre API, arrivée en Bot API 10.1 : des blocs
structurés en JSON plutôt qu'un balisage. De vrais tableaux, avec
bordures et ligne d'en-tête, de vrais titres, de vraies listes — et une
borne de 32 768 caractères au lieu de 4 096, soit huit fois moins de
pages à tourner.

`riche.ts` traduit vers ces blocs ; `markdown.ts` reste le repli, pour le
jour où l'API riche refuserait un document. Les deux sont mesurés contre
l'API réelle, et les pièges relevés au passage y sont écrits — un tableau
porte `cells` à la racine et non `rows`, un titre exige un `size`
numérique, une liste porte `items[].blocks`.

Une case à cocher, elle, est écrite dans le texte (`☑︎`, `☐︎`). Le format
prévoit `has_checkbox`, mais aucun client ne le dessine, et
`sendChecklist` — la checklist native — exige un compte Business
(`PREMIUM_ACCOUNT_REQUIRED`). Sans ce préfixe, une liste de tâches
perdrait l'état de chaque ligne sans laisser de trace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trois commandes de consultation — `/projets`, `/projet`, `/voir` — et une
navigation par boutons dans un message qui **se réécrit** : une
conversation n'a pas de bouton précédent, et empiler une liste par clic
laisserait derrière soi une file d'états morts.

Ce qui est montré est l'**arborescence du projet**, pas un classement
inventé : les « catégories » de la page Projet *sont* les dossiers de
`.claude`. Un dossier qui n'en contient qu'un autre est fondu avec lui —
`rules/back/application` s'ouvre d'un clic au lieu de trois écrans qui ne
posent aucune question.

Aucune garde n'est élargie : les trois lecteurs de `projects.ts` sont
appelés tels quels, et chacun garde son bac à sable. C'est l'origine de
l'entrée qui décide lequel, jamais la forme du chemin — les confondre
faisait refuser un document de dossier inclus parfaitement légitime.

**`/atelier` n'ouvre plus que sur un projet connu.** Un chemin quelconque
de la machine ne tombe sur rien : il n'y a pas de règle à contourner,
seulement une liste dans laquelle figurer. C'est délibérément plus strict
que l'Atelier à l'écran, où l'on voit ce qu'on choisit.

Deux détails de mise en page qui ne se devinent pas, et qui sont mesurés
plutôt que supposés :

- une rangée se remplit tant que les libellés tiennent, trois au plus —
  au-delà, un bouton fait moins d'un tiers d'écran et devient une cible
  qu'on manque ;
- **le clavier prend la largeur de la bulle, donc du texte.** Un en-tête
  de vingt caractères donnait des boutons de soixante pixels. Le
  remplissage invisible qui corrige cela prend sa propre ligne : collé au
  texte, il le poussait au-delà du bord d'un téléphone et coupait la
  dernière phrase en deux.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Il décrivait une liste numérotée à plat et un rendu HTML : deux choses
qui n'existent plus. Il décrit maintenant la navigation en arborescence,
le rendu structuré, la pagination, et `/sessions` en deux groupes.

Trois passages tiennent une observation plutôt qu'une intention, et c'est
volontaire — ce sont ceux qu'on aurait envie de « corriger » plus tard :
une case à cocher n'est pas dessinée par les clients actuels, un tableau
en chasse fixe se disloque sur un téléphone, et les écrans de navigation
vivent en mémoire, donc un redémarrage périme les boutons d'un vieux
message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
L'accesseur `arrete` datait du client à la main, où la boucle de long-polling
était la nôtre et où `index.ts` avait besoin d'en interroger l'état. Depuis que
`Bot` la tient, plus personne ne le lit — `stopped` ne sert qu'en interne, à
taire l'erreur d'une requête abandonnée par `stop()`.

Un accesseur public que rien n'appelle laisse croire qu'il répond à une
question que quelqu'un se pose.

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

Quatre pertes de sens à la traduction, toutes constatées sur un document réel —
`docs/projet/mise-en-recette.md`, qui les porte toutes les quatre.

**Une citation se relit entièrement**, comme un document à elle seule. Traitée
ligne à ligne, une citation de six lignes donnait six cadres empilés, et la
consigne numérotée qu'elle contenait perdait sa numérotation. La récursion
s'arrête d'elle-même : chaque tour retire un chevron.

**Les listes suivent l'indentation de la source.** Aplatir remontait un
sous-point au rang de son parent, ce qui inverse le sens d'une consigne : « à
vérifier, dans cet ordre » suivi de quatre sous-points devenait cinq points de
même rang.

**La suite d'un élément long lui reste attachée.** Une deuxième ligne rentrée
fermait la liste, et le point suivant repartait à « 1 » dans un nouveau bloc.

**L'alignement des colonnes est lu dans la ligne de séparation.** Les deux-points
du Markdown portent une intention — une colonne de nombres alignée à droite dans
la source doit l'être à l'écran. C'est précisément ce que `align` exprime, et le
champ qu'on avait pris la peine de rendre optionnel contre les types de la
bibliothèque.

Le commentaire d'en-tête disait deux fois la même chose sur `is_header` ; il ne
le dit plus qu'une.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`code` seul est rendu par le client en chasse fixe teintée. Sur un grand écran
la teinte suffit à distinguer un chemin du texte qui l'entoure ; sur un
téléphone elle se perd, et un document dense en chemins redevient un mur.

`marked` ajoute un fond — une différence de **surface**, non de couleur, et
c'est ce qui traverse la réduction d'échelle. Les deux entités se cumulent :
mesuré contre l'API, `marked(code)` porte bien la chasse fixe, la teinte et le
fond, et l'ordre de l'emboîtement est sans effet.

Cela valait d'être mesuré plutôt que déduit : le client ignore en silence ce
qu'il ne sait pas rendre — `has_checkbox` l'avait déjà montré — et la spec seule
n'aurait rien prouvé.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deux défauts qui se voyaient sur la même ligne d'un document réel :
« la marche à suivre est dans [`docs/livraison/0.livraison.md`](…) ».

**Le libellé d'un lien n'était jamais analysé**, donc ses accents graves
restaient à l'écran. Il se relit désormais comme le reste, y compris quand la
cible est écartée : un lien relatif reste refusé — il ne mène nulle part depuis
une messagerie — mais son texte ne doit pas s'en trouver appauvri. La même
relecture vaut pour le gras, l'italique et le barré, qui avaient le même trou.

**Telegram fabriquait des liens dans notre dos.** `.md` est un domaine de
premier niveau — la Moldavie —, si bien que `0.livraison.md` partait vers un
site qui n'existe pas. Le piège est propre à ce que la Passerelle affiche : un
dépôt est plein de `.py`, `.pl`, `.sh`, `.io`. `skip_entity_detection` le ferme,
et laisse intactes les entités qu'on déclare nous-mêmes.

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

Le piège de cette API n'a pas de garde possible : elle accepte en silence les
champs qu'elle ne connaît pas, et les types de la bibliothèque déclarent leurs
valeurs `string`. Le compilateur ne dira jamais que `pull_quotation` aurait dû
s'écrire `pullquote`. Les chaînes exactes ont donc besoin d'un endroit où vivre.

`blocs-riches.md` recense les vingt-et-un blocs et les vingt-cinq entités en
ligne, dit lesquels on émet et pourquoi les autres sont écartés — et surtout
note, à côté de chaque cas, ce que l'observation contredit dans la spec.

Une correction en découle. Le commentaire des listes numérotées accusait le
champ `label` : c'est faux, `label` n'existe que sur les blocs *reçus*. Le champ
d'envoi est `value`, secondé par `type`. Mesuré : `type` seul déclenche la
numérotation, `value` seul ne fait rien, et la forme décimale décale d'un rang —
le premier point s'affiche « 0. », là où les lettres et les romains sont justes.
Garder le numéro dans le texte reste donc le bon choix, mais pour la vraie
raison.

Le fichier note aussi ce que la spec offre et qu'on n'exploite pas : `markdown`
en entrée directe — écarté, on y perdrait la main sur les cas où le rendu par
défaut est faux —, le bloc repliable `details`, et la borne des 500 blocs, la
seule limite qu'on ne garde pas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entre l'envoi d'un message et la réponse, il pouvait se passer dix minutes sans
un octet. À l'écran, la ligne d'activité comble ce silence ; dans une
conversation il ne restait rien — ni signe que le message avait été reçu, ni
moyen de distinguer « ça travaille » de « c'est tombé ».

Le support est le **brouillon** (`sendRichMessageDraft`), pas un message. Il est
éphémère, ne persiste pas dans le fil, et s'anime au lieu de s'empiler. C'est ce
qui le rend compatible avec la règle du manuel : AURA n'envoie pas le flux
d'activité, et un brouillon n'est pas envoyé — il est montré, puis il disparaît.

Deux contraintes de l'API dictent le reste. Un brouillon expire au bout de
trente secondes : d'où un battement de vingt. Une phase d'outil se remplace
toutes les deux ou trois secondes : d'où un pas minimal de deux, en dessous
duquel un libellé qui change se lit comme un scintillement.

Deux réserves mesurées, et l'action de saisie qui en découle : le brouillon ne
vaut qu'en conversation privée, et le client web ne le rend pas — seuls les
clients mobiles le font, vérifié. `sendChatAction` l'accompagne donc : elle ne
dit que « quelque chose se passe », mais elle passe partout.

Le libellé reprend celui de l'Atelier — mêmes phases, mêmes noms d'outils —
parce qu'une session lue de deux endroits ne doit pas raconter deux histoires.
La durée affichée est celle du tour, jamais de la phase : c'est la seule qu'on
se demande vraiment.

Le battement s'arrête sur trois événements, et jamais sur une échéance : fin de
tour, demande de permission, question. Sur les deux derniers, la balle est dans
le camp de l'utilisateur — laisser battre la bulle ferait croire le contraire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Un agent écrit du Markdown — titres, listes, tableaux, chemins entre accents
graves. On l'envoyait en texte nu, si bien qu'un tableau arrivait avec ses
barres verticales et ses tirets, à côté d'une traduction en messages riches
qu'on ne servait qu'aux fichiers.

C'était le rendu le plus souvent lu de toute la Passerelle : on lit une réponse
à chaque tour, un fichier de temps en temps.

Deux différences avec l'envoi d'un fichier, et elles tiennent à ce qu'une
réponse n'en est pas un. Pas d'en-tête — on sait qui parle. Pas de pagination :
tourner la page suppose de pouvoir relire la source, or celle-ci ne vit que dans
la session. Une réponse trop longue reste donc coupée, mais la coupe passe de
4 000 à près de 23 000 caractères, la borne du riche étant huit fois celle d'un
message ordinaire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deux choses visibles ont changé, et le manuel les taisait.

La bulle d'activité y gagne sa propre section, avec les deux réserves qu'il vaut
mieux lire avant de s'étonner : elle demande une conversation privée et un
client mobile. Sur le web ou dans un groupe, il ne reste que le « en train
d'écrire… » de l'en-tête.

Et la section « ce que je ne vous envoie pas » disait le contraire de ce que la
Passerelle fait maintenant : elle promettait de ne pas envoyer la ligne
d'activité. La promesse tenait à ce qu'une messagerie n'est pas une timeline —
elle tient toujours, mais l'exception doit se dire : une bulle qui s'efface
n'empile rien.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le barreau intermédiaire de `envoieRendu` rendait le document en HTML.
Mesuré sur 162 pages de deux dépôts réels, il ne pouvait partir que sur
20 % d'entre elles : le HTML pèse 10 à 16 % de plus que sa source pour
une limite huit fois plus basse, si bien qu'il échouait précisément sur
les documents qui en avaient besoin — et retombait alors sur du texte nu
coupé à 4 000 caractères.

Il ne protégeait pas non plus de ce qu'on croyait : un bloc mal formé par
`riche.ts` produit une structure légale qui s'affiche mal, l'API répond
`ok: true`, et le repli ne se déclenche jamais. Il ne couvrait que le
refus de charge, où le HTML est le plus faible des deux.

Le repli est désormais le même appel avec un bloc unique : le document
tel quel dans un `paragraph`. Mesuré contre l'API — les retours à la
ligne et les lignes vides sont conservés, en texte proportionnel. On
perd la mise en forme, on garde la mise en page, les 32 768 caractères
et le repli « Afficher plus ». `pre` conservait autant mais enfermait la
prose dans un encadré de code.

Ce qui disparaît avec : un second analyseur Markdown qui dérivait du
premier — la fusion des citations corrigée dans `riche.ts` n'y avait
jamais été portée.

`markdown.ts` se réduit à la pagination, et son défaut de bloc vide est
corrigé au passage : la clôture d'un bloc de code reste sur la page qui
l'a ouvert, au lieu d'ouvrir un bloc aussitôt refermé sur la suivante.
Les bornes de Telegram passent dans `telegram.ts`, qui est le seul à
avoir à les connaître.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`tranche` était synchrone et lançait ses branches en `void`. Le `void`
détachait la promesse : elle sortait du `try` de l'appelant, et du crochet
d'erreur de la bibliothèque avec. Or `navigue` appelle `listProjects()`
hors de tout `try` — un `~/.claude` déplacé, un disque qui répond mal, et
c'était un rejet non traité. Il n'y a pas de `unhandledRejection` dans
`server/`, donc c'est le défaut de Node : le process se termine. Un clic
sur un bouton emportait le BFF et les sessions de l'Atelier avec lui.

Le chemin des messages n'avait pas le défaut — `await traite(...)` est
bien dans son `try`. C'est l'asymétrie entre les deux portes qui faisait
le trou.

Au passage, deux points relevés à la relecture :

Un bouton de descente dont le rang ne désigne plus un dossier ne disait
rien. Partout ailleurs le fichier répond `navigationPerimee`, avec pour
raison écrite qu'un bouton sans effet passe pour une panne — ce cas-là
contredisait sa propre règle.

Et `applique` traite `waiting` comme une fin de tour, ce qui n'en est pas
une : le commentaire le disait mal. C'est voulu, et c'est maintenant
écrit — ce que l'agent a rédigé avant de demander part tout de suite, la
demande suit avec ses boutons, et l'on voit ce qu'il veut faire avant
d'avoir à le trancher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trois listes devaient s'accorder à la main : le `switch` de `routage.ts`,
le texte de l'aide dans les deux catalogues, et ce que Telegram propose
sous le `/`. Cette dernière était saisie dans `@BotFather`, donc hors du
dépôt — invisible à toute relecture, et libre d'annoncer une commande
retirée depuis.

`commandes.ts` porte désormais la table. L'aide s'en compose, et
`setMyCommands` en dérive au démarrage, une fois par langue : la langue de
référence tient la liste par défaut, celle que voit un client dont la
langue n'a pas la sienne.

Le routage garde son `switch` — il traduit une commande en intention, ce
qui n'est pas mécanique. C'est un test qui tient les deux listes égales,
dans les deux sens : la lecture inverse est celle qui attrape le vrai
oubli, une commande ajoutée au routage et jamais annoncée. Vérifié en le
mettant en défaut.

Les descriptions changent de forme parce qu'elles changent d'usage : la
liste de Telegram les affiche seules, hors de l'aide. « son arborescence »
ne veut plus rien dire quand il n'y a pas de phrase avant.

La déclaration ne bloque rien : un refus ne coûte que l'autocomplétion,
et les commandes restent reconnues puisque c'est `routage.ts` qui en juge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le mobile n'affichait aucune commande là où le web les affichait toutes.
La cause se lit dans `getMyCommands` : la liste par défaut répondait, la
même interrogée en `fr` était vide. N'ayant posé que le défaut pour le
français — en jugeant qu'une liste `fr` explicite serait redondante —, un
client réglé en français demandait `fr`, ne trouvait rien à aucun scope,
et n'affichait rien. Le web retombait sur le défaut, le mobile non.

Plutôt que de départager les deux clients, on ne laisse plus de repli à
prendre : le défaut est posé, puis chaque langue reçoit la sienne, la
référence comprise. La redondance est mesurée, pas décorative.

C'était un raisonnement là où il fallait une mesure — le même défaut que
`riche.ts` documente pour les blocs : l'API accepte sans erreur ce qui ne
s'affichera pas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`/start` était un alias d'`/aide` : le geste que Telegram propose de
lui-même à qui ouvre la conversation répondait par la liste des huit
commandes. C'est une référence, pas une porte d'entrée — elle arrive
avant qu'on sache quoi demander.

Il a donc son intention à lui. Deux lignes et trois boutons : ce que je
suis, ce que je vois — le nombre de projets, la session déjà ouverte ici,
ce qui tourne ailleurs — et par où entrer. `docs/voix.md` range l'accueil
parmi les surfaces où le « je » doit porter ce que la formulation
impersonnelle ne portait pas ; l'état du parc est cette information, un
sommaire des boutons ne l'aurait pas été.

L'accueil prend la place du message de navigation : « Projets » le
réécrit au lieu d'empiler, si bien qu'il devient la première marche du
même parcours que l'arborescence. « Sessions » et « Aide » produisent du
contenu, pas un pas : elles partent dans leur propre message et laissent
l'accueil en place — la même règle que `/voir`.

Les boutons sont en rangée et non en `solo` : `solo` détache une action
d'une liste, et ici il n'y a pas de liste. Leurs libellés perdent leur
article pour tenir sur une rangée, et parce que `docs/voix.md` veut les
boutons nominaux — `Projets`, comme `Autoriser`.

`/aide` est inchangée et reste la référence. Le listage des sessions
devient une fonction, les deux entrées qui y mènent devant aboutir au
même endroit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deux silences de la Passerelle, relevés en revue et laissés en suspens.

`AskUserQuestion` n'était traitée qu'au quart : la première option de la
première question, le reste jeté, et un refus dès qu'il y en avait
plusieurs. Deux pertes étaient muettes — `multiSelect` était ignoré, si
bien qu'une question à choix multiples se répondait d'un seul clic ; et
`description` et `preview` disparaissaient, alors qu'`ask.ts` s'était
donné du mal pour que la seconde traverse son schéma zod.

`passerelle/questions.ts` porte désormais le formulaire de
`AskPrompt.vue`, sans rien y ajouter : une question par message, le choix
multiple qui se coche, la réponse écrite quand aucune option ne convient.
Même doctrine que `routage.ts` — il décide sans réseau, donc il se teste.

Deux mesures le dessinent. Un message riche ne se réécrit pas : la
bibliothèque expose `sendRichMessage` et aucun `editMessageRichText`.
Cocher ne touche donc qu'au clavier, par `editMessageReplyMarkup` — ce
qui est de toute façon ce qu'on veut, la question n'ayant aucune raison
de bouger sous les doigts. Et l'écran quitté est désarmé avant le
suivant : ses boutons ne portent qu'un rang, un clic tardif cocherait une
option de la question d'après. Vu à l'écran, corrigé.

La réponse écrite est interceptée dans le seul cas `parler`, après
`parseIntention` : sous une question, `/stop` reste `/stop`.

`snapshot` avait deux sites d'émission et un commentaire qui n'en nommait
qu'un. L'abonnement rejoue l'historique — on le passe, et le premier reçu
est le sien par construction, `subscribe` émettant avant d'enregistrer.
L'autre est `resetConversation` : le contexte vient d'être vidé et la
conversation n'en apprenait rien, continuant de parler à quelqu'un qui a
tout oublié. Elle relaie maintenant les mots mêmes de l'écran. Rien ne
peut le provoquer d'ici — `/clear` n'est pas une commande servie —, ce
qui est la raison de le dire plutôt que l'inverse.

Vérifié de bout en bout sur Telegram : trois écrans enchaînés, un
`multiSelect` coché puis validé, deux maquettes ASCII rendues en blocs
préformatés, une réponse tapée à la main acceptée, et un `/clear` lancé
du dehors annoncé dans la conversation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
De loin, on ne savait pas où l'on en était. La conversation dit ce que l'agent
répond, jamais la place qu'il lui reste — et une compaction, qui vide la fenêtre
en plein travail, ne laissait aucune trace : `applique` ne relaie que les
événements `assistant`.

`/etat` rend la fenêtre, et le chiffre ne peut pas diverger de la page Contexte.
Le runner le relève sur `message.message.usage` comme `input + cache_read +
cache_creation`, la somme même dont `transcript.ts` ancre un tour. Pas de
transcript à relire : c'est en mémoire, et c'est exact. Le cache y est compté —
ce qui est relu occupe la fenêtre comme le reste, seul le prix diffère.

Le runner relève et ne juge pas. La limite vient de `contextLimitFor`, qui
existe pour une raison : un modèle à fenêtre longue s'enregistre sans son
suffixe `[1m]`, et seules deux preuves la révèlent — un contexte observé
au-dessus de 200 k, ou les réglages. `pre_tokens` d'une compaction est la plus
précoce des deux.

Deux messages qu'AURA avance d'elle-même, et le manuel les nomme désormais : la
compaction avec ses chiffres, et le franchissement des 80 % dit une seule fois.
Ce seuil est le garde-fou de `contextFill`, recopié faute de pouvoir l'importer,
et un test tient les deux nombres égaux.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`30 527 / 1 000 000` demande de compter les chiffres des deux côtés pour saisir
le rapport ; `31 k / 1 M` le donne. Sur un téléphone, où cette ligne est lue en
une œillade, c'est la différence entre lire et déchiffrer.

`Intl` plutôt qu'un formatage à la main : la forme compacte est une affaire de
langue — `31 k` en français, `31K` en anglais, séparateur décimal compris.
Aucune décimale, parce que les deux seules limites qui existent — 200 k et 1 M —
sont exactes, et qu'un `1,0 M` laisserait croire à un arrondi qui n'a pas eu
lieu.

La compaction passait ses chiffres bruts, relevé à l'essai : la conversation
annonçait « 31 k » sur une ligne et « 30549 » sur la suivante, pour la même
fenêtre. Elle emprunte désormais le même formateur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deux manques que `/etat` avait rendus visibles : on voyait la fenêtre se remplir
sans pouvoir rien y faire, et la compaction s'annonçait sans dire ce qu'elle
avait gardé.

`/compacter` est la seule commande de Claude Code que la Passerelle relaie, et
la surface le justifie — de loin, il n'y avait aucun autre geste. Elle passe par
la file d'entrée comme un tour : c'est le CLI qui compacte.

Le résumé, lui, n'arrive pas avec la frontière. Mesuré en instrumentant le flux
SDK : il suit immédiatement, dans un message `user` marqué `isSynthetic` dont le
contenu est une chaîne — et le `<local-command-stdout>` qui vient après porte
`isReplay`, ce qui les sépare. Le runner le capte et le pose sur l'événement de
compaction par un `replace-event`, si bien que la compaction s'annonce tout de
suite : un résumé qui ne viendrait pas ne l'emporterait pas dans son silence.

Il se rend replié. Le bloc riche `details` existe — `blocs-riches.md` le donnait
pour « la piste la plus sérieuse » —, il a été essayé et il rend : titre visible,
corps qui s'ouvre au clic, formatage conservé et les 32 768 caractères du riche.
L'« Afficher plus » automatique ne le remplace pas, ne s'étant pas déclenché sur
sept mille caractères.

Un défaut relevé à l'essai, et il dépasse ce chantier : les événements d'un fil
partaient de front, si bien que le résumé — un gros message — doublait
l'annonce qui le précédait. Ils se traitent désormais à la file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ExitPlanMode arrivait comme une permission ordinaire : deux boutons et le nom
de l'outil. Le plan lui-même vit dans `input.plan` et n'était pas montré — on
approuvait donc un texte qu'on n'avait pas lu.

C'est le seul appel dont l'argument *est* la décision. Il passe désormais par
`envoieRendu`, comme un document : le markdown entier, mis en forme, et les
deux boutons sous lui.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Les boutons d'une permission restaient pressables après la réponse : on ne
savait pas si le clic avait porté, et un fil relu donnait à croire qu'une
question attendait encore.

Le désarmement se fait sur `permission-settled`, jamais sur le clic — c'est le
seul point de passage qui voie aussi les réponses données dans l'Atelier et les
refus du garde-fou du quart d'heure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La liste était déclarée sur la portée par défaut, donc visible de quiconque
ouvre le bot — et un bot est adressable de toute la Terre. Un inconnu n'obtenait
déjà aucune réponse, mais il lisait le menu : ouvrir une session, lancer une
commande, lire un fichier. C'est de l'obscurité et non de la sécurité, mais elle
ne coûte rien.

`declare` accepte désormais une portée de conversation, et la liste est posée
pour chacune des conversations autorisées, langue par langue comme avant. Le
défaut est effacé au même moment : ce qui a été posé chez Telegram y reste tant
qu'on ne le retire pas, et une version antérieure l'avait posé.

L'effacement passe sur chaque langue. Telegram range ses listes par langue
autant que par portée, si bien qu'effacer le défaut sans langue laisserait
debout celle qu'un client français avait reçue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ce qui transite passe par les serveurs de la messagerie, sans chiffrement de
bout en bout. Le manuel, SECURITY et le README le disent maintenant avant
qu'on configure le jeton, et nomment la piste cherchée pour s'en passer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Une capacité nouvelle, éteinte par défaut : la Passerelle. Mineure, donc,
au sens que le journal donne aux trois nombres.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Shaenn
Shaenn merged commit 2e47b67 into main Aug 20, 2026
1 check passed
@Shaenn
Shaenn deleted the chantier/passerelle branch August 20, 2026 10:39
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