DEV Community

Cover image for Perché osFoundry divide i progressi di Claude Code tra una nota di sessione e uno stato?
BBT for BBT Italiano

Posted on

Perché osFoundry divide i progressi di Claude Code tra una nota di sessione e uno stato?

Questi sono i due tool di scrittura che un agente di coding usa per comunicare i propri progressi a osFoundry, così come tools/list li restituisce a qualsiasi client, autenticato o no (ridotti ai campi che contano):

{"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

Sembrano due modi per dire "ecco cosa sto facendo". Non lo sono. set_status non ha una sessione né campi obbligatori. È una riga per agente che ogni chiamata sovrascrive. post_session_note richiede una sessione e una chiave, e ogni chiamata aggiunge una riga che qualcuno leggerà. Questo post smonta la coppia, con read_session_mail dal lato di chi riceve, e ricostruisce il lato client in Python. Su questo Mac non c'è nessun binario claude installato, quindi è lo script a fare la parte di Claude Code. Tutto quello che segue viene da una singola esecuzione.

Cosa vede un client prima di autenticarsi?

Gli schemi, e un rifiuto chiaro. Lo script chiama initialize, tools/list e set_status dieci volte ciascuno, senza 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

Misurato dal Giappone su un Apple M1, quindi i numeri dicono tanto del percorso di rete quanto del server. Il rifiuto arriva in due modi contemporaneamente: un HTTP 401 con un header WWW-Authenticate che punta ai metadati della risorsa protetta, e un risultato JSON-RPC con isError: true. Il body riporta anche l'header sotto _meta come mcp/www_authenticate, per i client che gli header non li guardano mai.

Il ciclo di reporting usa cinque dei 59 tool:

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

I token sono contati con cl100k_base, quindi per il tokenizer di Claude sono approssimativi. Nota l'unica differenza nelle annotazioni: post_session_note è openWorldHint: true, set_status no. Una nota raggiunge persone e agenti fuori dal controllo di chi la invia. Uno stato aggiorna solo un campo.

Perché set_status non accetta una sessione?

Perché non riguarda una sessione. La descrizione dice chi lo legge: gli agenti "seated in a project with you". Un altro agente lo vede tramite list_agents, che restituisce per ogni agente l'handle, il livello di presenza e, "where you share a project", ciò su cui sta lavorando. Lo scopo è evitare collisioni. Se due agenti stanno per mettere mano a fix/orders-total, uno dei due dovrebbe accorgersene.

Lo schema impone questa forma. Quindici casi contro gli schemi live con 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

Niente campo session_id né state, quindi non esiste un flag "blocked" o "done" da impostare. Se Claude Code è bloccato, lo dice in task (280 caratteri) o, meglio, in una nota che il team leggerà. "Claim a profile first" è l'altra precondizione: lo stato è legato all'handle ottenuto da claim_connected_profile, che deve rispettare ^[a-z0-9][a-z0-9-]{1,30}$.

Cosa significa in pratica "omitted fields keep their value"?

Significa che set_status è un aggiornamento parziale, e che una chiamata vuota è valida e non cambia nulla. Lo script ricostruisce la regola così come la formula la descrizione. Il codice server di osFoundry non è pubblico, quindi questa è una ricostruzione, non una cattura:

{"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

Due conseguenze per il client. Gli aggiornamenti a metà task possono inviare solo task, e così restano leggeri. E per lo schema uno stato non scade mai da solo: nessun campo TTL, nessun timestamp impostato dal client. Se Claude Code si ferma senza inviare i tre null, gli altri agenti continuano a vederlo su quel branch. Metti l'azzeramento nelle istruzioni di fine task dell'agente, non lasciarlo come ripensamento.

Perché idempotency_key è obbligatorio su una nota e assente su uno stato?

Ripetere una chiamata di stato non fa danni: riscrive gli stessi valori. Ripetere una nota significa un secondo messaggio davanti al team. Le chiamate MCP passano per HTTP, le risposte si perdono, e un client che ritenta senza chiave può pubblicare la stessa nota due volte.

Lo script lo simula. 200 note, il 10% delle risposte perso dopo che il server aveva già salvato la nota, fino a tre tentativi. Sia il tasso di perdita sia la regola di deduplicazione (una riga per connessione e chiave) sono ipotesi. La chiave obbligatoria è l'indizio a favore della seconda:

== 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

Stesso numero di chiamate, 26 note duplicate. La chiave serve solo se il client la ricava dal significato della nota, per esempio cc-fix-orders-total-pr, e non da un nuovo UUID a ogni tentativo.

Quante note dovrebbe pubblicare un agente che riporta i progressi?

Meno di quante pensi. Dal lato del proprietario, le note finiscono nella scheda Team mail della sessione. AgentMailInbox.tsx del client web le elenca con mittente, un'etichetta "outside assistant", destinatario e tipo di messaggio, e fa polling con refetchInterval: 5_000. Non c'è push, quindi una nota compare al fetch successivo al suo arrivo.

Lo script esegue un task di esempio da 30 passi (durate estratte da una log-normale con mediana 30 s, un'ipotesi) con due strategie e misura, su un orologio simulato, quando ogni nota diventa visibile a un proprietario che ha la scheda aperta:

== 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à non dipende dalla strategia. In entrambi i casi è limitata dal polling a 5 secondi. Quello che cambia è la casella: 30 righe contro 4 (avviato, una domanda, PR aperta, finito), per quattro chiamate di scrittura in più. Il chiacchiericcio di ogni passo va nello stato, che si sovrascrive da solo. Nella casella resta solo ciò che qualcuno deve leggere.

Come si accorge Claude Code di una risposta?

read_session_mail restituisce le note indirizzate alla connessione e quelle che ha pubblicato lei, escludendo l'analisi interna degli agenti. Pagina con before_id e limit (da 1 a 20). Non c'è after_id; la parte 2 mostra lo schema che lo rifiuta. Quindi "cosa c'è di nuovo dall'ultima volta" tocca al client: ricordare l'id più recente visto, leggere dall'alto e fermarsi quando una pagina contiene un id già noto.

Assumendo un ordine dal più recente, che la paginazione con before_id lascia intendere ma la descrizione non dichiara:

== 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 agente che controlla ai suoi milestone raramente ha più di una manciata di risposte nuove, quindi con limit: 10 basta una chiamata. Se vuoi un vero cursore, il server ne ha uno altrove: poll_resource_events accetta agent_session, team_session o loop_session con after_sequence.

L'intero script ha impiegato 19.4 s di tempo reale sul M1, comprese 30 chiamate di rete. Il tempo CPU utente è stato di 0.42 s.

Se lo implementi tu

  • Tieni "su cosa sto lavorando" e "cosa ti sto dicendo" in due tool separati. Uno è una riga per agente che si sovrascrive, l'altro un log append-only che qualcuno legge.
  • Rendi lo stato un aggiornamento parziale con null esplicito, e decidi cosa lo azzera. Uno stato senza scadenza ha bisogno che sia il client ad azzerarlo.
  • Richiedi una chiave di idempotenza su tutto ciò che una persona leggerà, e di' ai client di ricavarla dal contenuto, non da ogni tentativo.
  • Limita lo stato a chi può agire di conseguenza (qui, gli agenti dello stesso progetto) e segnalalo come autodichiarato.
  • Dai alla casella un cursore in avanti, oppure documenta l'ordine così che i client possano smettere di paginare su un id noto.

Come viaggiano le note tra agenti in una sessione è spiegato nella pagina Agent Mail, e le sessioni vere e proprie sono in agent sessions.

Top comments (0)