Questo è lo strumento di scrittura che una chat di Claude riceve per le Notes di osFoundry, così come lo elenca l'endpoint MCP pubblico. È stato recuperato con un tools/list non autenticato il 2026-11-06 e ridotto ai campi che contano qui:
{"name": "edit_note_tab",
"description": "Use an exact tab ID and version from read_note_tab. Use replace_text for Note/Markdown, slide_add or slides_add_outline for Slides, sheet_set_range for Sheet, or replace_tab_data for supported content tabs. File and Source attachments cannot be edited through this tool. A repeated idempotency key returns the first result.",
"inputSchema": {
"required": ["note_id", "tab_id", "expected_version", "idempotency_key", "operation"],
"properties": {
"operation": {"enum": ["replace_text", "replace_tab_data", "slide_add",
"slides_add_outline", "sheet_set_range"]},
"sheet_id": {"type": "string"}, "start_row": {"type": "integer"},
"start_col": {"type": "integer"}, "values": {"type": "array", "items": {"type": "array"}}}},
"annotations": {"readOnlyHint": false, "destructiveHint": true},
"securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}
Due dei cinque campi obbligatori non riguardano affatto la modifica. expected_version trasforma la scrittura in un compare-and-swap: va a buon fine solo se la Note è ancora alla versione che l'agente ha letto. idempotency_key rende innocuo un nuovo invio. Le altre scritture sulle Notes, create_note_tab e add_note_source, richiedono la stessa coppia. Le letture no: list_note_tabs, read_note_tab, read_sheet_range (al massimo 100 per 100 celle) e list_note_attachments sono tutte readOnlyHint: true con osfoundry:read.
Di seguito, cosa garantisce questo contratto, misurato su una ricostruzione. Il controllo di versione è solo metà del lavoro. L'altra metà è cosa fa l'agente dopo essere stato respinto.
Perché rifiutare una scrittura obsoleta invece di fare il merge?
replace_text invia l'intero testo di una scheda. Se Claude ha letto la versione 7, una persona ha salvato la versione 8 e ora Claude invia il suo testo, il server ha in mano due documenti completi e non ha più l'antenato comune. Non può sapere se Claude voleva cancellare il nuovo paragrafo della persona o se semplicemente non l'ha mai visto. Con last-writer-wins si sceglie la risposta sbagliata senza che nessuno se ne accorga. Rifiutare è l'unica risposta che non tira a indovinare.
È concorrenza ottimistica. Nessuno tiene un lock mentre un modello pensa per venti secondi. Ogni scrittura dichiara su quale versione si basa, e il server fa lo scambio solo se quella versione è ancora quella corrente:
UPDATE notes SET version = :new WHERE id = :note AND version = :expected; -- 0 rows → conflict
L'app desktop di osFoundry fa la stessa cosa dal lato umano. In apps/web/src/screens/notes/context/NotesContext.tsx, il salvataggio di una Note è un PATCH /notes/:id che porta expected_updated_at (l'ultima revisione del server vista dall'editor) e, dopo una scrittura Connected, un expected_version che connected-note-save-guard.ts conserva in localStorage perché un ricaricamento non lo cancelli. Quando il salvataggio fallisce, l'editor tiene il contenuto della persona come bozza locale e mostra un errore di sincronizzazione. Entrambi gli editor scrivono in modo condizionale.
Cosa deve fare l'agente quando viene respinto?
Ci sono due reazioni plausibili, e solo una funziona:
- Rileggere la scheda, riapplicare la modifica prevista sul contenuto aggiornato e scrivere con la nuova versione.
- Rileggere solo la versione e rimandare lo stesso payload.
La seconda supera il controllo e sovrascrive comunque la persona, perché il payload è stato calcolato dal testo vecchio. Il controllo non può accorgersene. Una versione dimostra solo che hai visto il numero di versione più recente, non che ne hai usato il contenuto.
La ricostruzione
Qui non gira nessun binario di Claude. notes_store.py implementa list_note_tabs, read_note_tab, read_sheet_range, edit_note_tab (replace_text, sheet_set_range) e create_note_tab su sqlite in modalità WAL, con gli stessi nomi di strumenti e argomenti della versione in produzione. Ogni scrittura gira dentro BEGIN IMMEDIATE: controlla la tabella di idempotenza, confronta la versione, applica la modifica, incrementa la versione e salva il risultato, tutto in un'unica transazione. race.py avvia due thread, ognuno con la propria connessione: un "Claude" scriptato e una persona scriptata. Ognuno esegue lettura → riflessione → scrittura, 100 volte. Il tempo di riflessione è ridotto in scala: 20–80 ms per Claude, 5–40 ms per la persona, così la finestra tra lettura e scrittura dell'agente è la più lunga, come con un modello reale. Ogni scrittura aggiunge una riga univoca (claude-17, human-42), quindi alla fine ogni riga mancante è un aggiornamento perso che si può contare. Python 3.9.6, Apple M1, seed 1–5.
Il percorso di scrittura, senza l'impianto di sqlite:
def _guarded(self, note_id, tab_id, expected_version, idempotency_key, apply):
db.execute("BEGIN IMMEDIATE")
hit = db.execute("SELECT result FROM idem WHERE key=?", (idempotency_key,)).fetchone()
if hit:
return {**json.loads(hit[0]), "replayed": True}
current = tab_version if self.check == "tab" else note_version
if self.check != "none" and expected_version != current:
return {"error": "version_conflict", "current_version": current}
result = apply(db) # replace_text / sheet_set_range / new tab
# bump version, store (key, result), COMMIT
E il ciclo dell'agente, con il selettore della strategia:
r = s.edit_note_tab("note-1", tab, tab_now["version"], uuid.uuid4().hex, **payload)
while "error" in r:
fresh = s.read_note_tab("note-1", tab)
if policy == "reapply":
payload["text"] = fresh["data"] + "\n" + line # redo the change on what's there now
tab_now = fresh # refresh_version keeps the old text
r = s.edit_note_tab(...)
Un tentativo in conflitto riceve una nuova idempotency key. La chiave identifica una scrittura prevista su una base precisa. Dopo un rifiuto, la scrittura riapplicata è un'altra scrittura.
Quanti aggiornamenti si perdono senza il controllo?
Cinque esecuzioni, 100 modifiche per editor in ogni esecuzione, 1,000 righe previste in totale per ogni riga della tabella:
| Protezione in scrittura | Agente in caso di conflitto | Righe perse | Perse: Claude / persona | Conflitti: Claude / persona | Tentativi max |
|---|---|---|---|---|---|
| nessuna | — | 300 | 125 / 175 | 0 / 0 | 1 |
| versione della Note | rilegge, riapplica | 0 | 0 / 0 | 219 / 133 | 2 |
| versione della Note | rilegge la versione, rimanda il testo vecchio | 304 | 117 / 187 | 213 / 136 | 3 |
Senza controllo è sparito il 30% di tutte le modifiche, e la persona ne ha perse più di Claude. Dato che l'agente pensa più a lungo, le sue scritture si basano su letture più vecchie e quindi sovrascrivono di più. Con il controllo e la riapplicazione non si è perso nulla. Il 41–48% delle scritture di Claude ha richiesto un secondo tentativo, mai un terzo. La media è stata di 1.41–1.48 tentativi per Claude e 1.24–1.30 per la persona. Ogni esecuzione ha richiesto 9.6–10.5 s di tempo reale indipendentemente dalla modalità, perché domina il tempo di riflessione.
La terza riga è quella da ricordare. Il server ha segnalato più di 300 conflitti e non ha protetto nulla. Un controllo di versione obbliga a rileggere. Non può obbligare a usare quello che si è letto.
Il controllo ha un costo?
bench.py cronometra 2,000 chiamate sequenziali a edit_note_tab con un replace_text di 200 caratteri, tre ripetizioni per modalità. Mediana per scrittura: 61.9–63.1 µs senza controllo, 57.0–58.8 µs con la versione della Note, 55.1–61.1 µs con una versione per scheda. Il confronto è una lettura indicizzata dentro una transazione che è comunque già aperta. La differenza resta sotto il rumore tra un'esecuzione e l'altra. Il p99 (0.42–1.33 ms) è jitter del fsync di sqlite e non dipende dalla modalità.
Perché la versione è quella della Note, e quanto costa?
Le descrizioni degli strumenti di osFoundry legano una sola versione all'intera Note. create_note_tab dice "Use the version from list_note_tabs", cioè l'elenco a livello di Note. Nel percorso dell'app desktop che applica le modifiche Connected (connected-note-application.ts), expected_version viene confrontato con l'updated_at della Note. Quindi una persona che scrive nella scheda Sheet sposta la versione rispetto alla quale viene controllata la scrittura di Claude nella scheda di testo.
Nella ricostruzione questo si può misurare. Si esegue race.py tabs con Claude che modifica la scheda di testo e la persona che scrive celle nella scheda Sheet, 3 esecuzioni, 100 modifiche ciascuna:
| Ambito della versione | Conflitti di Claude | Conflitti della persona | Perse |
|---|---|---|---|
| Note | 130 su 300 scritture (43%) | 87 su 300 (29%) | 0 |
| Scheda | 0 | 0 | 0 |
Tutti questi 217 conflitti sono falsi, perché i due editor non hanno mai toccato la stessa scheda. Ognuno costa una rilettura e nient'altro. Una versione a livello di Note offre qualcosa che una versione per scheda non può offrire: ogni scrittura viene controllata rispetto a tutto il contenuto della Note, quindi un agente che ha letto la Sheet per scrivere il Brief non può scrivere un Brief basato su valori della Sheet che nel frattempo sono cambiati. Le versioni per scheda eliminano i falsi conflitti e perdono proprio questa garanzia tra schede. Per un agente che legge una scheda e ne scrive un'altra, la versione a grana grossa è l'impostazione predefinita più sicura.
Cosa succede a un controllo di versione con un timestamp al millisecondo?
Una versione basata su timestamp funziona solo se due salvataggi non possono produrre la stessa stringa. race.py fast porta il tempo di riflessione a zero, 2,000 modifiche per editor, 5 esecuzioni, e confronta una versione troncata al millisecondo con un timestamp al microsecondo più un suffisso di 6 cifre esadecimali:
| Formato della versione | Righe perse (su 20,000) | Esecuzioni con perdite |
|---|---|---|
| millisecondi | 136 (0.68%) | 5 su 5 |
| microsecondi + suffisso | 0 | 0 su 5 |
Quando due scritture fanno commit nello stesso millisecondo, la seconda riceve la stessa versione della prima. Un editor che ha letto prima della prima scrittura supera allora il controllo rispetto alla seconda. osFoundry conserva le versioni delle Notes con la precisione completa dei timestamp di PostgreSQL. Se costruisci il tuo sistema, usa un contatore o un timestamp con un criterio di spareggio.
Perché la creazione ha bisogno di una chiave propria?
create_note_tab non sostituisce nulla, quindi un controllo di versione da solo non può impedire i duplicati. La chiamata ripetuta è una nuova scheda sulla nuova versione. race.py create crea 50 schede Sheet perdendo il 30% delle risposte, così il client riprova come farebbe un host dopo un timeout:
| Chiamate inviate | Schede create | Duplicati | |
|---|---|---|---|
| senza chiave, 3 esecuzioni | 71 / 61 / 63 | 71 / 61 / 63 | 21 / 11 / 13 |
| stessa chiave al retry | 71 / 61 / 63 | 50 / 50 / 50 | 0 |
Con la chiave, il retry restituisce il primo risultato, compreso l'ID della scheda, quindi il successivo edit_note_tab dell'agente punta alla scheda che esiste davvero. È la riga "A repeated idempotency key returns the first tab" nella descrizione dello strumento di osFoundry.
Se lo colleghi da solo
- Rendi
expected_versionobbligatorio in ogni strumento di scrittura, nello schema, non come campo opzionale. Un controllo opzionale viene saltato la prima volta che un modello ha fretta. - Restituisci un conflitto come normale risultato dello strumento su cui il modello possa agire, e scrivi nella descrizione dello strumento cosa fare: rileggere, poi rifare la modifica. Le tue istruzioni sono l'unico posto in cui può stare la regola "riapplica, non limitarti ad aggiornare la versione".
- Scegli l'ambito della versione in modo consapevole. Le versioni per documento producono falsi conflitti e proteggono le letture tra schede. Le versioni per scheda evitano i falsi conflitti e perdono quella protezione.
- Fai in modo che le versioni non possano collidere: un contatore, o un timestamp a precisione completa con un criterio di spareggio. Mai un timestamp arrotondato al millisecondo.
- Proteggi creazioni e append con una idempotency key che identifichi una sola intenzione su una sola base. Salva la chiave e il risultato nella stessa transazione della scrittura.
- Proteggi anche l'editor umano. Se i conflitti si controllano solo sull'agente, una scheda del browser non aggiornata può ancora sovrascrivere l'agente.
La definizione breve di osFoundry del protocollo sottostante è nella sua voce del glossario su MCP. Come conserva e ripristina gli stati precedenti di una Note è spiegato nella pagina sulle versioni delle Notes.
Top comments (0)