DEV Community

Cover image for Pourquoi osFoundry sépare-t-il la progression de Claude Code en une note de session et un statut ?
BBT for BBT Français

Posted on

Pourquoi osFoundry sépare-t-il la progression de Claude Code en une note de session et un statut ?

Voici les deux outils d'écriture qu'un agent de code utilise pour rendre compte de sa progression à osFoundry, tels que tools/list les renvoie à n'importe quel client, connecté ou non (réduits aux champs qui comptent) :

{"name": "set_status",
 "description": "Tell the agents you share a project with which repository, branch and task you are on, so they can ask you instead of colliding with your work. Self-reported and labelled as such; shown only to agents seated in a project with you, never workspace-wide. Omitted fields keep their value; null clears one. Claim a profile first.",
 "inputSchema": {"properties": {
     "repo":   {"type": ["string", "null"], "maxLength": 300},
     "branch": {"type": ["string", "null"], "maxLength": 200},
     "task":   {"type": ["string", "null"], "maxLength": 280}},
   "additionalProperties": false},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}

{"name": "post_session_note",
 "description": "Post an attributed note in an invited session as a contributor or runner. Other agents can reply to the returned external sender slug; this does not trigger an automatic run.",
 "inputSchema": {"properties": {
     "session_type": {"enum": ["agent_session", "team_session", "loop_session"]},
     "session_id": {"type": "string"},
     "subject": {"type": "string", "maxLength": 200},
     "body": {"type": "string", "maxLength": 10000},
     "idempotency_key": {"type": "string"}},
   "required": ["session_type", "session_id", "subject", "body", "idempotency_key"],
   "additionalProperties": false},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}
Enter fullscreen mode Exit fullscreen mode

On dirait deux façons de dire « voilà ce que je fais ». Ce n'est pas le cas. set_status n'a ni session ni champ obligatoire. C'est une ligne par agent, que chaque appel écrase. post_session_note exige une session et une clé, et chaque appel ajoute une ligne que quelqu'un va lire. Cet article démonte la paire, avec read_session_mail côté réception, et reconstruit le côté client en Python. Aucun binaire claude n'est installé sur ce Mac, donc c'est le script qui joue Claude Code. Tout ce qui suit vient d'une seule exécution.

Que voit un client avant de se connecter ?

Les schémas, et un refus net. Le script appelle initialize, tools/list et set_status dix fois chacun, sans token :

$ uv run --with tiktoken --with jsonschema python3 session_report.py
== Part 1: live wire, 10 calls each, ms ==
initialize             HTTP 200  median  417.7  p90  673.8
tools/list             HTTP 200  median  521.4  p90  888.6
set_status (no token)  HTTP 401  median  410.8  p90  485.1
set_status without token: Authentication required | isError True
  www-authenticate error_description: "Connect osFoundry to continue"
Enter fullscreen mode Exit fullscreen mode

Mesuré depuis le Japon sur un Apple M1 : les chiffres en disent donc autant sur la route que sur le serveur. Le refus arrive par deux canaux à la fois : un HTTP 401 avec un en-tête WWW-Authenticate qui pointe vers les métadonnées de la ressource protégée, et un résultat JSON-RPC avec isError: true. Le corps reprend aussi l'en-tête sous _meta, en tant que mcp/www_authenticate, pour les clients qui ne regardent jamais les en-têtes.

La boucle de reporting utilise cinq des 59 outils :

tools 59, list tokens 12662, these five 1121 (8.9 %)
  claim_connected_profile  scope osfoundry:write  readOnly False openWorld False tokens 281
  set_status               scope osfoundry:write  readOnly False openWorld False tokens 221
  read_session_state       scope osfoundry:read   readOnly True  openWorld False tokens 169
  read_session_mail        scope osfoundry:read   readOnly True  openWorld False tokens 207
  post_session_note        scope osfoundry:write  readOnly False openWorld True  tokens 243
Enter fullscreen mode Exit fullscreen mode

Les tokens sont comptés avec cl100k_base, ce sont donc des approximations pour le tokenizer de Claude. Remarquez la seule différence dans les annotations : post_session_note est openWorldHint: true, set_status ne l'est pas. Une note atteint des personnes et des agents hors du contrôle de l'appelant. Un statut ne fait que mettre à jour un champ.

Pourquoi set_status ne prend-il pas de session ?

Parce qu'il ne concerne pas une session. La description dit qui le lit : les agents « seated in a project with you ». Un autre agent le voit via list_agents, qui renvoie pour chaque agent son handle, son niveau de présence et, « where you share a project », ce sur quoi il travaille. Le but est d'éviter les collisions. Si deux agents s'apprêtent à toucher fix/orders-total, l'un des deux doit s'en rendre compte.

Le schéma impose cette forme. Quinze cas testés contre les schémas en ligne avec jsonschema (Draft 2020-12) :

set_status               empty call {}              ok
set_status               task 280 chars             ok
set_status               task 281 chars             REJECT maxLength: task
set_status               branch null                ok
set_status               field 'session_id'         REJECT additionalProperties
set_status               field 'state': 'blocked'   REJECT additionalProperties
claim_connected_profile  handle 'alice-claude'      ok
claim_connected_profile  handle 'Alice'             REJECT pattern: handle
claim_connected_profile  no display_name            REJECT required
post_session_note        subject 200, body 10,000   ok
post_session_note        subject 201                REJECT maxLength: subject
post_session_note        no idempotency_key         REJECT required
read_session_mail        before_id + limit 20       ok
read_session_mail        after_id                   REJECT additionalProperties
read_session_state       graph_session              REJECT enum: session_type
Enter fullscreen mode Exit fullscreen mode

Pas de champ session_id ni state, donc aucun drapeau « blocked » ou « done » à positionner. Si Claude Code est bloqué, il le dit dans task (280 caractères) ou, mieux, dans une note que l'équipe lira. « Claim a profile first » est l'autre prérequis : le statut est rattaché au handle obtenu via claim_connected_profile, qui doit respecter ^[a-z0-9][a-z0-9-]{1,30}$.

Que veut dire concrètement « omitted fields keep their value » ?

Cela veut dire que set_status est une mise à jour partielle, et qu'un appel vide est valide et ne change rien. Le script reconstruit la règle telle que la description l'énonce. Le code serveur d'osFoundry n'est pas public, il s'agit donc d'une reconstruction, pas d'une capture :

{"repo": "shop-api", "branch": "fix/orders-total", "task": "orders total rounding"}
   -> {"repo": "shop-api", "branch": "fix/orders-total", "task": "orders total rounding"}
{"task": "orders total rounding: writing tests"}
   -> {"repo": "shop-api", "branch": "fix/orders-total", "task": "orders total rounding: writing tests"}
{"branch": "fix/orders-total-2"}
   -> {"repo": "shop-api", "branch": "fix/orders-total-2", "task": "orders total rounding: writing tests"}
{}
   -> {"repo": "shop-api", "branch": "fix/orders-total-2", "task": "orders total rounding: writing tests"}
{"repo": null, "branch": null, "task": null}
   -> {"repo": null, "branch": null, "task": null}
Enter fullscreen mode Exit fullscreen mode

Deux conséquences pour le client. Les mises à jour en cours de tâche peuvent n'envoyer que task, ce qui les garde légères. Et d'après le schéma, un statut n'expire jamais tout seul : pas de champ TTL, pas d'horodatage fixé par le client. Si Claude Code s'arrête sans envoyer les trois null, les autres agents continuent de le voir sur cette branche. Intégrez la remise à zéro aux instructions de fin de tâche de l'agent, au lieu d'y penser après coup.

Pourquoi idempotency_key est-il obligatoire sur une note mais absent d'un statut ?

Répéter un appel de statut est sans danger : il réécrit les mêmes valeurs. Répéter une note, c'est un deuxième message sous les yeux de l'équipe. Les appels MCP passent par HTTP, des réponses se perdent, et un client qui réessaie sans clé peut publier deux fois la même note.

Le script le modélise. 200 notes, 10 % des réponses perdues alors que le serveur avait déjà enregistré la note, jusqu'à trois nouvelles tentatives. Le taux de perte comme la règle de dédoublonnage (une ligne par connexion et par clé) sont des hypothèses. La clé obligatoire est ce qui appuie la seconde :

== Part 4: 200 notes, 10 % responses lost after commit, up to 3 retries ==
stable key per note    calls 226  rows in Team mail 200  duplicates 0  gave up 0
new key per attempt    calls 226  rows in Team mail 226  duplicates 26  gave up 0
Enter fullscreen mode Exit fullscreen mode

Même nombre d'appels, 26 notes en double. La clé n'aide que si le client la dérive du sens de la note, par exemple cc-fix-orders-total-pr, et non d'un nouvel UUID à chaque tentative.

Combien de notes un agent qui rend compte de sa progression devrait-il publier ?

Moins qu'il ne le pense. Côté propriétaire, les notes arrivent dans l'onglet Team mail de la session. AgentMailInbox.tsx, dans le client web, les liste avec l'expéditeur, un libellé « outside assistant », le destinataire et le type de message, et fait du polling avec refetchInterval: 5_000. Il n'y a pas de push : une note apparaît au fetch suivant son arrivée.

Le script exécute une tâche d'exemple de 30 étapes (durées tirées d'une loi log-normale, médiane 30 s, une hypothèse) selon deux stratégies et mesure, sur une horloge simulée, quand chaque note devient visible pour un propriétaire dont l'onglet est ouvert :

== Part 5: one Example task, 30 steps, owner's Team mail refetch every 5 s ==
note per step              Team mail rows 30  write calls 30  note visible after: mean 2.51 s, max 5.00 s
status per step + 4 notes  Team mail rows  4  write calls 34  note visible after: mean 2.54 s, max 4.99 s
task length 17.2 min (simulated), median step 30 s
Enter fullscreen mode Exit fullscreen mode

La visibilité ne dépend pas de la stratégie. Elle est bornée par le polling de 5 secondes dans les deux cas. Ce qui change, c'est la boîte aux lettres : 30 lignes contre 4 (démarré, une question, PR ouverte, terminé), pour quatre appels d'écriture de plus. Le bavardage à chaque étape va dans le statut, qui s'écrase lui-même. La boîte ne garde que ce que quelqu'un doit lire.

Comment Claude Code remarque-t-il une réponse ?

read_session_mail renvoie les notes adressées à la connexion et celles qu'elle a publiées, sans l'analyse interne des agents. La pagination se fait avec before_id et limit (de 1 à 20). Il n'y a pas d'after_id ; la partie 2 montre le schéma le refuser. Savoir « ce qui est nouveau depuis mon dernier passage » revient donc au client : retenir l'id le plus récent déjà vu, lire depuis le haut, et s'arrêter dès qu'une page contient un id connu.

En supposant un ordre du plus récent au plus ancien, ce que la pagination par before_id laisse entendre sans que la description le dise :

== Part 6: catching up on replies with before_id paging (newest first, assumed) ==
 0 new replies  limit  5: 1 call(s),  5 rows  |  limit 10: 1 call(s), 10 rows  |  limit 20: 1 call(s), 20 rows
 3 new replies  limit  5: 1 call(s),  5 rows  |  limit 10: 1 call(s), 10 rows  |  limit 20: 1 call(s), 20 rows
12 new replies  limit  5: 3 call(s), 15 rows  |  limit 10: 2 call(s), 20 rows  |  limit 20: 1 call(s), 20 rows
30 new replies  limit  5: 7 call(s), 35 rows  |  limit 10: 4 call(s), 40 rows  |  limit 20: 2 call(s), 40 rows
Enter fullscreen mode Exit fullscreen mode

Un agent qui vérifie à ses jalons a rarement plus d'une poignée de nouvelles réponses, donc limit: 10 tient en un appel. Si vous voulez un vrai curseur, le serveur en a un ailleurs : poll_resource_events prend agent_session, team_session ou loop_session avec after_sequence.

Le script complet a pris 19.4 s en temps réel sur le M1, 30 appels réseau compris. Le temps CPU utilisateur était de 0.42 s.

Si vous câblez ça vous-même

  • Gardez « ce sur quoi je suis » et « ce que je vous dis » dans deux outils distincts. L'un est une ligne par agent qui s'écrase, l'autre un journal en ajout seul que quelqu'un lit.
  • Faites du statut une mise à jour partielle avec null explicite, et décidez de ce qui le remet à zéro. Un statut sans expiration a besoin que le client le remette à zéro.
  • Exigez une clé d'idempotence sur tout ce qu'une personne va lire, et dites aux clients de la dériver du contenu, pas de chaque tentative.
  • Limitez le statut à ceux qui peuvent agir dessus (ici, les agents du même projet), et indiquez qu'il est autodéclaré.
  • Donnez à la boîte aux lettres un curseur vers l'avant, ou documentez l'ordre pour que les clients puissent arrêter de paginer sur un id connu.

La façon dont les notes circulent entre agents dans une session est décrite sur la page Agent Mail, et les sessions elles-mêmes se trouvent sous agent sessions.

Top comments (0)