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"]}]}
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"}]}
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)