DEV Community

Cover image for Perché osFoundry invia lo schema del record con ogni pagina che Codex elenca?
BBT for BBT Italiano

Posted on

Perché osFoundry invia lo schema del record con ogni pagina che Codex elenca?

Questi sono i tool di record channel che una connessione Codex CLI riceve da osFoundry, così come li elenca l'endpoint MCP pubblico. Sono stati recuperati con un tools/list non autenticato il 2026-11-09 e ridotti ai campi che contano qui:

{"name": "list_record_items",
 "description": "List the schema and a bounded page of item previews from one granted record channel. Use read_record_item for the complete values.",
 "inputSchema": {"required": ["record_channel_id"],
   "properties": {"record_channel_id": {"type": "string"}, "before_id": {"type": "string"},
                  "limit": {"type": "integer", "minimum": 1, "maximum": 50}}},
 "annotations": {"readOnlyHint": true}}

{"name": "read_record_item",
 "description": "Read one item by ID from a granted record channel and return its current version.",
 "inputSchema": {"required": ["record_channel_id", "item_id"]}}

{"name": "edit_record_item",
 "description": "Update validated fields in one record item. Supply the version from read_record_item; concurrent changes are rejected.",
 "inputSchema": {"required": ["record_channel_id", "item_id", "values", "expected_version", "idempotency_key"],
   "properties": {"values": {"type": "object"}}},
 "annotations": {"readOnlyHint": false, "destructiveHint": true},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}
Enter fullscreen mode Exit fullscreen mode

Guardate values. È {"type": "object"} e nient'altro. Lo schema del tool non può dire quali chiavi ha un channel, quali sono campi Select o cosa può contenere Points, perché queste cose cambiano da channel a channel e cambiano quando qualcuno modifica la tab Schema. L'unico posto da cui Codex può impararle è la prima riga della descrizione del tool di listing: lo schema torna insieme alla pagina. Il resto del post misura cosa porta questa scelta, su una ricostruzione, e altre due scelte negli stessi quattro tool: un cursore before_id al posto di un offset, e un log di eventi a cui tornare.

Che cos'è un valore valido?

I tipi di campo sono fissi. packages/shared/src/types/record.ts nel frontend di osFoundry definisce RecordChannelFieldType con 16 tipi: text, number, date, boolean, email, phone, url, select, multi_select, currency, percentage, rating, relation, file, json e formula. Ogni campo ha is_required, is_unique e una config specifica per tipo: max_length per text, min/max per number, max_stars per rating, options per i due tipi select, expression per formula.

Select è quello su cui gli agenti inciampano. Un'opzione è {value, label, color}. Viene salvato il value, e il label è ciò che vedono le persone. RecordDataGrid.tsx renderizza una cella select con options.find((o) => o.value === params.value) e, se nulla corrisponde, ripiega su String(params.value) in un chip spento. Quindi, se un controllo permissivo lasciasse passare "In progress" in un campo il cui valore è in_progress, la grid mostrerebbe qualcosa che sembra giusto ed è sbagliato: niente colore, e invisibile a tutto ciò che raggruppa o filtra per valore. Un modello che ha visto solo i label della grid, o il messaggio di una persona, manderà dei label. Qualcuno deve dire di no.

Come dovrebbe essere un rifiuto?

L'API dei record di osFoundry risponde a una chiamata fallita con {ok: false, error: {message}}. I tool per i record dell'app desktop, usati dagli agenti interni (src-tauri/src/record_tools.rs), passano quel messaggio al modello senza modificarlo, e il commento spiega perché: il messaggio dell'API "names the field that failed validation, which is usually enough for a self-correcting retry." Il suo unit test usa values.status is not a valid option come esempio.

Quel "usually enough" si può misurare. La ricostruzione prova quattro livelli di dettaglio in un rifiuto:

  • terse: {"error": "validation_failed"}
  • field: quali chiavi sono fallite
  • values: chiavi, motivo e i valori di opzione ammessi o l'intervallo
  • labels: lo stesso, più il label di ogni opzione

Ecco il livello "values", così come è uscito dall'esecuzione:

{"error": "validation_failed", "fields": [
  {"key": "status", "message": "values.status must be one of the option values, got \"In progress\"",
   "allowed": ["new", "triaged", "in_progress", "fixed", "wont_fix"]},
  {"key": "owner", "message": "values.owner must be one of the option values, got \"Aiko\"",
   "allowed": ["aiko", "mika", "ren"]},
  {"key": "points", "message": "values.points must be between 0 and 13, got 20", "min": 0, "max": 13},
  {"key": "due", "message": "values.due must be a date as YYYY-MM-DD, got \"Friday\""},
  {"key": "age_days", "message": "values.age_days is a formula field and is computed, not written"}]}
Enter fullscreen mode Exit fullscreen mode

Queste stringhe sono della ricostruzione. Solo la nomenclatura values.<key> segue quella di osFoundry.

La ricostruzione

Qui non gira nessun binario di Codex. record_store.py implementa list_record_items, read_record_item, edit_record_item e poll_resource_events su sqlite in memoria, con gli stessi nomi di tool, nomi di argomenti e limiti della versione live. I tipi di campo e le chiavi di config vengono da record.ts. La lunghezza della preview (120 caratteri), la forma dei risultati e ogni regola di validazione sono della ricostruzione. Una modifica gira dentro BEGIN IMMEDIATE: lookup di idempotenza, confronto di versione, validazione, merge, incremento di versione, append dell'evento, commit. Qualsiasi problema fa il rollback dell'intera modifica.

triage.py è un Codex scriptato. Lavora su un bug tracker con 11 campi (Esempio: ticket, title, description, status, owner, severity, points, area, due, reporter, age_days), 200 item New per esecuzione, seed 1–5. Ogni item riceve un'intenzione di triage con le parole di una persona: "In progress", "Aiko", "S2 Major" o solo "major", points da {1, 2, 3, 5, 8, 13, 20}, due "Friday" o una data ISO, un'area. L'agente o manda le parole così come sono, o prima le mappa attraverso lo schema, abbinando i label ai value. Davanti a un rifiuto corregge ciò che il rifiuto gli permette di correggere, scarta ciò che non può e segnala quel campo a una persona. Non tronca mai un numero. Python 3.9.6, Apple M1.

Lo schema nella pagina fa la differenza?

1,000 item, 6 campi previsti ciascuno:

L'agente ha letto lo schema Il rifiuto dice Campi salvati (su 6,000) Lasciati a una persona Chiamate di modifica per item
no niente 1,363 (22.7%) 4,637 7.00
no quale campo 1,363 (22.7%) 4,637 2.00
no campo, motivo, valori ammessi 5,361 (89.3%) 639 2.00
no campo, motivo, valori e label 5,857 (97.6%) 143 2.00
sì niente 5,857 (97.6%) 143 1.86
sì quale campo 5,857 (97.6%) 143 1.14
sì valori, o valori e label 5,857 (97.6%) 143 1.14

97.6% è il tetto. Gli altri 143 sono gli item in cui la persona ha detto 20 points e il campo si ferma a 13. Quelli vanno rifiutati, e lo sono stati in ogni riga.

Un agente che aveva letto lo schema arrivava al tetto qualunque cosa dicesse il rifiuto. La sua prima modifica falliva solo per i 143 points fuori intervallo. Senza lo schema, ogni prima modifica falliva: status, owner, severity e area erano label, e metà delle date due erano "Friday". Da lì in poi il recupero dipende interamente dall'errore. "Quale campo" è inutile senza i valori ammessi: l'agente sa che status è sbagliato ma non cosa sia giusto, quindi lo scarta. I valori ammessi senza label arrivano all'89.3%. I 639 mancati sono "Won't fix" contro wont_fix e "major" contro s2. Nessuno dei due si può abbinare partendo da una lista di valori. Con i label nel rifiuto, l'agente senza schema recupera, al costo di una chiamata in più per item. Un errore stringato e senza schema ha portato l'agente a spezzare ogni modifica in sei modifiche da un campo, 7 chiamate per item, e nonostante questo ha salvato meno di un quarto.

Quindi lo schema è metà del design della validazione. Il controllo sul server ferma i valori sbagliati. Lo schema che viaggia con ogni pagina è ciò che permette di azzeccare il primo tentativo. Costa 1,705 byte per pagina in questo channel, circa il 10% di una pagina da 16.7 KiB con 50 preview.

Perché le preview, e perché al massimo 50?

Una pagina di 50 preview pesava 16.7 KiB. Leggere quei 50 item per intero faceva 124.6 KiB, perché qui le descrizioni vanno da 300 a 4,000 caratteri. Per il triage, una preview basta per scegliere cosa aprire: lo status, il ticket, l'inizio del titolo. Codex apre quelli che sta per modificare. Una pagina limitata evita anche che un channel grande inondi il contesto. La descrizione del tool dice "bounded" e lo schema dice maximum: 50.

Perché before_id invece di un offset?

Le persone continuano ad aprire bug mentre Codex legge. La ricostruzione pagina 600 item 50 alla volta, dal più recente, e modifica il channel dopo ogni pagina. Cinque esecuzioni:

Modifica dopo ogni pagina Paginazione Pagine Letture di item Letti due volte Mai visti
2 nuovi bug aperti offset 65 3,120 120 0
2 nuovi bug aperti before_id 60 3,000 0 0
2 item già letti eliminati offset 60 2,890 0 110
2 item già letti eliminati before_id 60 3,000 0 0

I nuovi item in cima spostano tutto in basso, quindi le pagine a offset si sovrappongono e Codex fa il triage di alcuni item due volte. Eliminare qualcosa di già letto sposta tutto in alto, quindi la pagina a offset successiva parte dopo item che nessuno ha letto. 110 bug che sono esistiti dall'inizio alla fine non sono mai stati visti. Un cursore indica l'ultimo item letto, e un inserimento o un'eliminazione altrove non possono spostarlo. Gli item aperti durante il passaggio non compaiono in nessuna pagina. Per questo c'è il log degli eventi.

Come trova Codex cosa è cambiato dall'ultimo passaggio?

Dopo un passaggio su 400 item, le persone hanno modificato 30 item, ne hanno aperti 12 e ne hanno eliminati 3. Due modi per trovare quei 45:

Strategia Chiamate Dati restituiti Trovati
rielencare tutte le pagine, confrontare le versioni 9.0 135.8 KiB 45 su 45
poll_resource_events dalla sequenza salvata 1.0 4.6 KiB 45 su 45

Lo script verifica con un assert che entrambi abbiano trovato esattamente i 45. Il polling costa quanto è cambiato. Rielencare costa la dimensione del channel, ogni volta. Lo stato da conservare tra un'esecuzione e l'altra è un solo intero, l'ultima sequenza.

E il controllo di versione?

In breve, perché funziona come per Notes e le knowledge base. Su 1,000 modifiche in cui una persona ha spostato 1 item su 8 tra la lettura e la scrittura di Codex, usare la versione della lettura ha dato 131 conflitti e ha sovrascritto 0 modifiche della persona. Rileggere la versione subito prima di scrivere ha dato 0 conflitti e le ha sovrascritte tutte e 131. Dopo un conflitto, il Codex scriptato rilegge, vede che l'item è già in progress e aggiunge solo severity.

Quanto costa la validazione?

2,000 modifiche valide da sei campi, tre esecuzioni: mediana di 72.8–75.1 µs per modifica con validazione, 41.9–42.4 µs con validate() sostituito da uno stub. La differenza è la query dello schema e i controlli per campo dentro la transazione. Rispetto a un turno del modello di diversi secondi, non si nota.

Se lo collegate voi

  • Restituite lo schema con il listing, a ogni pagina, non tramite una chiamata separata che l'agente può saltare. Il divario misurato è una modifica rifiutata per item.
  • Mettete nel rifiuto i valori di opzione ammessi e i loro label. Il solo nome del campo non permette all'agente di correggere nulla.
  • Rifiutate l'intera modifica quando un campo qualsiasi fallisce, e dite quali campi. Un salvataggio parziale che segnala successo nasconde il campo che l'agente ha sbagliato.
  • Non convertite mai label e non troncate mai numeri sul server. "major" e "20 points" sono domande per una persona, non per un matcher.
  • Paginate con un cursore su un ID stabile, e date all'agente una sequenza di eventi per l'esecuzione successiva.
  • Tenete fuori dai tool dell'agente le modifiche allo schema, le creazioni e le eliminazioni finché non vi servono. La validazione ha senso solo se l'agente non può allargare il campo.

La definizione breve di osFoundry del protocollo sottostante è nella sua voce di glossario su MCP. La schermata del record channel da cui viene lo schema è nella pagina Records.

Top comments (0)