यह वह write tool है जो Claude chat को osFoundry Notes के लिए मिलता है, जैसा public MCP endpoint उसे list करता है। इसे 2026-11-06 को बिना authentication वाले tools/list से fetch किया गया और यहाँ काम के fields तक छोटा किया गया:
{"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"]}]} पाँच required fields में से दो का edit से कोई लेना-देना ही नहीं है। expected_version write को compare-and-swap बना देता है: write तभी लगता है जब Note अभी भी उसी version पर हो जो agent ने पढ़ा था। idempotency_key की वजह से दोबारा भेजना नुकसान नहीं करता। बाकी Note writes, create_note_tab और add_note_source, भी यही जोड़ी माँगते हैं। Reads नहीं माँगते: list_note_tabs, read_note_tab, read_sheet_range (ज़्यादा से ज़्यादा 100 x 100 cells) और list_note_attachments सभी osfoundry:read के तहत readOnlyHint: true हैं।
नीचे दिखाया गया है कि इस contract से असल में क्या मिलता है, एक rebuild पर माप कर। Version check सिर्फ़ आधी बात है। बाकी आधी बात यह है कि reject होने के बाद agent क्या करता है।
Stale write को merge करने के बजाय reject क्यों करें?
replace_text एक tab का पूरा text भेजता है। अगर Claude ने version 7 पढ़ा, किसी व्यक्ति ने version 8 save किया, और अब Claude अपना text भेजता है, तो server के पास दो पूरे documents होते हैं और एक common ancestor जो अब उसके पास है ही नहीं। Server यह नहीं बता सकता कि Claude उस व्यक्ति का नया paragraph delete करना चाहता था या उसने उसे कभी देखा ही नहीं। Last-writer-wins चुपचाप गलत जवाब चुन लेता है। Reject करना ही अकेला ऐसा response है जो अंदाज़ा नहीं लगाता।
यही optimistic concurrency है। जब model बीस सेकंड सोच रहा होता है, तब कोई lock नहीं पकड़े रहता। हर write बताता है कि वह किस version पर आधारित है, और server swap तभी करता है जब वह version अभी भी current हो:
UPDATE notes SET version = :new WHERE id = :note AND version = :expected; -- 0 rows → conflict osFoundry का desktop app इंसान की तरफ़ से भी यही करता है। apps/web/src/screens/notes/context/NotesContext.tsx में Note save एक PATCH /notes/:id है जो expected_updated_at (editor ने जो आख़िरी server revision देखा) साथ ले जाता है, और Connected write के बाद एक expected_version भी, जिसे connected-note-save-guard.ts localStorage में रखता है ताकि reload उसे मिटा न सके। Save fail होने पर editor उस व्यक्ति का content local draft के रूप में रख लेता है और sync error दिखाता है। दोनों editors conditional writers हैं।
Reject होने पर agent को क्या करना चाहिए?
दो reactions समझ में आते हैं, और उनमें से सिर्फ़ एक काम करता है:
- Tab को दोबारा पढ़ें, जो change करना था उसे fresh content पर फिर से apply करें, और fresh version के साथ write करें।
- सिर्फ़ version दोबारा पढ़ें और वही payload फिर से भेज दें।
दूसरा तरीका check पास कर लेता है और फिर भी उस व्यक्ति का edit overwrite कर देता है, क्योंकि payload पुराने text से बना था। Check इसे नहीं देख सकता। Version सिर्फ़ यह साबित करता है कि आपने latest version number देखा है, यह नहीं कि आपने उसका content इस्तेमाल किया।
Rebuild
यहाँ कोई Claude binary नहीं चलती। notes_store.py sqlite पर WAL mode में list_note_tabs, read_note_tab, read_sheet_range, edit_note_tab (replace_text, sheet_set_range) और create_note_tab implement करता है, live tool names और argument names के साथ। हर write BEGIN IMMEDIATE के अंदर चलता है: idempotency table check करता है, version compare करता है, change apply करता है, version बढ़ाता है और result store करता है, सब एक ही transaction में। race.py दो threads चलाता है, दोनों के अपने connections: एक scripted "Claude" और एक scripted व्यक्ति। दोनों read → think → write करते हैं, 100 बार। Think time घटा दिया गया है: Claude के लिए 20–80 ms, व्यक्ति के लिए 5–40 ms, ताकि agent की read-से-write window लंबी हो, जैसा असली model के साथ होता है। हर write एक unique line जोड़ता है (claude-17, human-42), तो बाद में कोई missing line एक lost update है जिसे गिना जा सकता है। Python 3.9.6, Apple M1, seeds 1–5।
Write path, sqlite की plumbing के बिना:
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 और agent का loop, policy switch के साथ:
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(...) जिस attempt पर conflict आया, उसे नई idempotency key मिलती है। Key एक base पर एक intended write को नाम देती है। Reject होने के बाद फिर से apply किया गया write एक अलग write है।
Check के बिना कितने updates खो जाते हैं?
पाँच runs, हर run में हर editor के 100 edits, हर row में कुल 1,000 intended lines:
| Write guard | Conflict पर agent | खोई lines | खोई: Claude / व्यक्ति | Conflicts: Claude / व्यक्ति | Max tries |
|---|---|---|---|---|---|
| कोई नहीं | — | 300 | 125 / 175 | 0 / 0 | 1 |
| Note version | दोबारा पढ़ो, फिर से apply करो | 0 | 0 / 0 | 219 / 133 | 2 |
| Note version | सिर्फ़ version दोबारा पढ़ो, पुराना text फिर भेजो | 304 | 117 / 187 | 213 / 136 | 3 |
Check के बिना सभी edits में से 30% गायब हो गए, और व्यक्ति ने Claude से ज़्यादा खोया। Agent का think time लंबा होने से उसके writes पुराने reads पर आधारित होते हैं, इसलिए वे ज़्यादा overwrite करते हैं। Check और re-apply के साथ कुछ भी नहीं खोया। Claude के 41–48% writes को दूसरी कोशिश लगी, तीसरी कभी नहीं। औसत Claude के लिए 1.41–1.48 tries और व्यक्ति के लिए 1.24–1.30 रहा। Mode कोई भी हो, हर run में 9.6–10.5 s wall time लगा, क्योंकि think time ही हावी रहता है।
तीसरी row याद रखने वाली है। Server ने 300 से ज़्यादा conflicts report किए और कुछ भी protect नहीं किया। Version check यह enforce करता है कि आप दोबारा पढ़ें। यह enforce नहीं कर सकता कि आपने जो पढ़ा उसे इस्तेमाल भी किया।
क्या check की कोई cost है?
bench.py 200-character replace_text के साथ 2,000 sequential edit_note_tab calls का time मापता है, हर mode के लिए तीन repetitions। हर write का median: बिना check 61.9–63.1 µs, Note version के साथ 57.0–58.8 µs, tab version के साथ 55.1–61.1 µs। Comparison बस एक indexed read है, उस transaction के अंदर जो वैसे भी खुला है। फ़र्क run-to-run noise से भी कम है। p99 (0.42–1.33 ms) sqlite का fsync jitter है और mode के साथ नहीं बदलता।
Version Note का क्यों है, और इसकी क्या कीमत है?
osFoundry के tool descriptions एक version को पूरे Note से जोड़ते हैं। create_note_tab कहता है "Use the version from list_note_tabs", जो Note-level listing है। Connected edits के लिए desktop app के apply path (connected-note-application.ts) में expected_version की तुलना Note के updated_at से होती है। यानी Sheet tab में type कर रहा व्यक्ति उस version को आगे बढ़ा देता है जिसके against Claude के text-tab write को check किया जाता है।
Rebuild में इसे मापा जा सकता है। race.py tabs चलाएँ, जिसमें Claude text tab edit करे और व्यक्ति Sheet tab में cells लिखे, 3 runs, हर एक में 100 edits:
| Version scope | Claude conflicts | व्यक्ति conflicts | खोई |
|---|---|---|---|
| Note | 300 में से 130 writes (43%) | 300 में से 87 (29%) | 0 |
| Tab | 0 | 0 | 0 |
ये सभी 217 conflicts झूठे हैं, क्योंकि दोनों editors ने कभी एक ही tab को छुआ ही नहीं। हर एक की cost बस एक re-read है, और कुछ नहीं। Note-level version वह देता है जो tab version नहीं दे सकता: हर write को Note की हर चीज़ के against check किया जाता है, तो जो agent Brief लिखने के लिए Sheet पढ़ता है, वह ऐसा Brief नहीं लिख सकता जो बीच में बदल चुकी Sheet values पर आधारित हो। Tab-level versions झूठे conflicts हटा देते हैं और ठीक यही cross-tab guarantee खो देते हैं। जो agent एक tab पढ़कर दूसरे में लिखता है, उसके लिए coarse version ही safer default है।
Millisecond timestamp version check के साथ क्या करता है?
Timestamp version तभी काम करता है जब दो saves एक ही string न बना सकें। race.py fast think time को शून्य कर देता है, हर editor के 2,000 edits, 5 runs, और milliseconds तक कटे version की तुलना microsecond timestamp और 6-hex-digit suffix वाले version से करता है:
| Version format | खोई lines (20,000 में से) | Loss वाले runs |
|---|---|---|
| milliseconds | 136 (0.68%) | 5 में से 5 |
| microseconds + suffix | 0 | 5 में से 0 |
जब दो writes एक ही millisecond में commit होते हैं, तो दूसरे को वही version मिलता है जो पहले को मिला था। जिस editor ने पहले write से पहले पढ़ा था, वह फिर दूसरे के against check पास कर लेता है। osFoundry Note versions को PostgreSQL timestamp की पूरी precision पर रखता है। अगर आप ख़ुद बना रहे हैं, तो counter इस्तेमाल करें, या tie-breaker वाला timestamp।
Create को अपनी अलग key की ज़रूरत क्यों है?
create_note_tab कुछ replace नहीं करता, इसलिए अकेला version check duplicates नहीं रोक सकता। दोबारा चलाया गया call नए version पर एक नया tab बन जाता है। race.py create 50 Sheet tabs बनाता है जिनमें 30% replies drop कर दिए जाते हैं, ताकि client वैसे ही retry करे जैसे timeout के बाद host करता है:
| भेजे गए calls | बने tabs | Duplicates | |
|---|---|---|---|
| बिना key, 3 runs | 71 / 61 / 63 | 71 / 61 / 63 | 21 / 11 / 13 |
| retry पर वही key | 71 / 61 / 63 | 50 / 50 / 50 | 0 |
Key के साथ retry पहला result लौटाता है, उसके tab ID समेत, इसलिए agent का अगला edit_note_tab उसी tab को target करता है जो मौजूद है। osFoundry के tool description में "A repeated idempotency key returns the first tab" वाली line का यही मतलब है।
अगर आप इसे ख़ुद wire up करें
- हर write tool पर
expected_versionको schema में required रखें, optional field के रूप में नहीं। Optional check उसी पल skip हो जाता है जब model पहली बार जल्दी में होता है। - Conflict को एक normal tool result की तरह लौटाएँ जिस पर model action ले सके, और tool description में लिखें कि क्या करना है: दोबारा पढ़ो, फिर change दोबारा करो। "Re-apply करो, सिर्फ़ version refresh मत करो" वाला rule सिर्फ़ आपके instructions में ही रह सकता है।
- Version का scope सोच-समझकर चुनें। Per-document versions झूठे conflicts देते हैं और cross-tab reads को protect करते हैं। Per-tab versions झूठे conflicts से बचाते हैं और वह protection छोड़ देते हैं।
- Versions को collision-free बनाएँ: counter, या full precision वाला timestamp tie-breaker के साथ। Milliseconds तक round किया गया timestamp कभी नहीं।
- Creates और appends पर ऐसी idempotency key लगाएँ जो एक base पर एक intent को नाम दे। Key और result को write वाले transaction में ही store करें।
- Human editor को भी guard करें। अगर conflict checks सिर्फ़ agent पर हों, तो एक stale browser tab फिर भी agent का काम overwrite कर सकता है।
इसके नीचे के protocol की osFoundry वाली छोटी definition उसकी MCP glossary entry में है। osFoundry किसी Note की पुरानी states कैसे रखता और restore करता है, यह Note versions page पर है।

