ये वे दो write tools हैं जिनसे कोई coding agent osFoundry को अपनी प्रगति बताता है, ठीक वैसे जैसे tools/list इन्हें किसी भी client को लौटाता है, चाहे वह sign in हो या नहीं (सिर्फ़ ज़रूरी fields रखे गए हैं):
{"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"]}]} देखने में ये "मैं अभी यह कर रहा हूँ" कहने के दो तरीके लगते हैं। पर ऐसा नहीं है। set_status में न कोई session है, न कोई required field। यह हर agent की एक row है जिसे हर call overwrite कर देती है। post_session_note को एक session और एक key चाहिए, और हर call एक नई row जोड़ती है जिसे कोई पढ़ेगा। यह पोस्ट इस जोड़ी को खोलकर देखती है, receiving side पर read_session_mail के साथ, और client side को Python में दोबारा बनाती है। इस Mac पर कोई claude binary installed नहीं है, इसलिए script ही Claude Code की भूमिका निभाती है। नीचे की हर चीज़ उसी script के एक run से आई है।
Sign in करने से पहले client को क्या दिखता है?
Schemas, और एक साफ़ इनकार। Script बिना token के initialize, tools/list और set_status को दस-दस बार call करती है:
$ 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" यह माप जापान से Apple M1 पर लिया गया है, इसलिए ये numbers server के साथ-साथ network route के बारे में भी उतना ही बताते हैं। इनकार एक साथ दो रास्तों से आता है: एक HTTP 401, जिसका WWW-Authenticate header protected-resource metadata की ओर इशारा करता है, और एक JSON-RPC result जिसमें isError: true है। Body में भी यही header _meta के अंदर mcp/www_authenticate के रूप में होता है, उन clients के लिए जो headers कभी नहीं देखते।
Reporting loop 59 में से पाँच tools इस्तेमाल करता है:
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 Tokens cl100k_base से गिने गए हैं, इसलिए Claude के tokenizer के लिए ये अनुमानित हैं। Annotations में एकमात्र फ़र्क पर ध्यान दें: post_session_note openWorldHint: true है, set_status नहीं। Note ऐसे लोगों और agents तक पहुँचता है जो caller के नियंत्रण से बाहर हैं। Status सिर्फ़ एक field update करता है।
set_status कोई session क्यों नहीं लेता?
क्योंकि इसका संबंध session से है ही नहीं। Description बताता है कि इसे कौन पढ़ता है: वे agents जो "seated in a project with you" हैं। दूसरा agent इसे list_agents के ज़रिए देखता है, जो हर agent का handle, presence tier और, "where you share a project", वह किस काम पर है, लौटाता है। मकसद टकराव से बचना है। अगर दो agents fix/orders-total को छूने वाले हैं, तो उनमें से एक को पता चल जाना चाहिए।
Schema इसी ढाँचे को लागू करता है। jsonschema (Draft 2020-12) से live schemas पर पंद्रह cases:
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 न session_id field है, न state, इसलिए "blocked" या "done" जैसा कोई flag set करने को नहीं है। अगर Claude Code अटक जाए, तो वह यह बात task (280 characters) में कहता है, या बेहतर है कि ऐसे note में कहे जिसे टीम पढ़ेगी। "Claim a profile first" दूसरी शर्त है: status claim_connected_profile से मिले handle से जुड़ा होता है, जिसे ^[a-z0-9][a-z0-9-]{1,30}$ से match करना ज़रूरी है।
"omitted fields keep their value" का व्यवहार में क्या मतलब है?
मतलब यह कि set_status एक partial update है, और खाली call भी valid है और कुछ नहीं बदलती। Script इस नियम को वैसे ही दोबारा बनाती है जैसे description में लिखा है। osFoundry का server code public नहीं है, इसलिए यह reconstruction है, असली capture नहीं:
{"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} Client के लिए इसके दो नतीजे हैं। Task के बीच के updates सिर्फ़ task भेज सकते हैं, जिससे वे छोटे रहते हैं। और schema के हिसाब से status अपने-आप कभी expire नहीं होता: न कोई TTL field, न client की ओर से set किया गया कोई timestamp। अगर Claude Code तीनों null भेजे बिना रुक जाए, तो बाकी agents उसे उसी branch पर देखते रहेंगे। Status clear करने को agent के end-of-task instructions का हिस्सा बनाइए, बाद में याद आने वाली चीज़ नहीं।
Note पर idempotency_key ज़रूरी क्यों है, जबकि status में यह है ही नहीं?
Status call दोहराने से कोई नुकसान नहीं: वह वही values फिर से लिख देती है। Note दोहराने का मतलब है टीम के सामने दूसरा message। MCP calls HTTP पर जाती हैं, responses खो जाते हैं, और बिना key के retry करने वाला client एक ही note दो बार post कर सकता है।
Script इसी को model करती है। 200 notes, 10% responses server के note save कर लेने के बाद खो जाते हैं, अधिकतम तीन retries। Loss rate और dedupe rule (हर connection और key पर एक row), दोनों मान्यताएँ हैं। Required key दूसरी मान्यता का सबूत है:
== 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 Calls उतनी ही, पर 26 duplicate notes। Key तभी काम आती है जब client उसे note के अर्थ से बनाए, जैसे cc-fix-orders-total-pr, न कि हर attempt पर नया UUID बनाकर।
प्रगति बताने वाले agent को कितने notes post करने चाहिए?
जितना वह सोचता है, उससे कम। Owner की तरफ़ notes session के Team mail tab में पहुँचते हैं। Web client का AgentMailInbox.tsx उन्हें sender, "outside assistant" label, recipient और message type के साथ दिखाता है, और refetchInterval: 5_000 से poll करता है। कोई push नहीं है, इसलिए note पहुँचने के बाद अगली fetch में दिखता है।
Script 30 steps वाला एक उदाहरण task (step की लंबाई log-normal से ली गई, median 30 s, यह एक मान्यता है) दो policies के तहत चलाती है, और एक simulated clock पर मापती है कि tab खुला रखने वाले owner को हर note कब दिखता है:
== 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 Visibility policy पर निर्भर नहीं करती। दोनों तरह से इसकी सीमा 5-second poll ही तय करता है। जो बदलता है वह mailbox है: 30 rows बनाम 4 (शुरू हुआ, एक सवाल, PR खुला, पूरा हुआ), बदले में चार write calls ज़्यादा। हर step की बातचीत status में जाती है, जो खुद को overwrite करता है। Mailbox में सिर्फ़ वही रहता है जिसे किसी को पढ़ना ज़रूरी है।
Claude Code को reply का पता कैसे चलता है?
read_session_mail उस connection को भेजे गए notes और उसके खुद post किए notes लौटाता है, internal agent analysis छोड़कर। यह before_id और limit (1 से 20) से paging करता है। कोई after_id नहीं है; Part 2 में दिखता है कि schema उसे reject करता है। इसलिए "पिछली बार देखने के बाद क्या नया आया" यह client का काम है: सबसे नया देखा गया id याद रखे, ऊपर से पढ़े, और जिस page में कोई जाना-पहचाना id मिले, वहीं रुक जाए।
यह मानते हुए कि order newest-first है, जिसका इशारा before_id paging से मिलता है पर description में लिखा नहीं है:
== 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 जो agent अपने milestones पर check करता है, उसके पास शायद ही कभी कुछ से ज़्यादा नए replies होते हैं, इसलिए limit: 10 में एक ही call काफ़ी है। अगर आपको असली cursor चाहिए, तो server में वह कहीं और है: poll_resource_events agent_session, team_session या loop_session को after_sequence के साथ लेता है।
पूरी script को M1 पर 19.4 s wall-clock लगे, 30 network calls मिलाकर। User CPU time 0.42 s था।
अगर आप इसे खुद जोड़ रहे हैं
- "मैं किस पर काम कर रहा हूँ" और "मैं आपको क्या बता रहा हूँ" को दो अलग tools रखें। एक हर agent की overwrite होने वाली row है, दूसरा append-only log जिसे कोई पढ़ता है।
- Status को explicit null वाला partial update बनाएँ, और तय करें कि उसे clear क्या करेगा। जिस status की expiry नहीं, उसे client को ही clear करना होगा।
- जिस चीज़ को कोई इंसान पढ़ेगा, उस पर idempotency key ज़रूरी करें, और clients से कहें कि उसे content से बनाएँ, हर attempt पर नहीं।
- Status को उन्हीं तक सीमित रखें जो उस पर कुछ कर सकते हैं (यहाँ, उसी project के agents), और उस पर self-reported का label लगाएँ।
- Mailbox को आगे बढ़ने वाला cursor दें, या order document करें ताकि clients किसी जाने-पहचाने id पर paging रोक सकें।
Session में agents के बीच notes कैसे आते-जाते हैं, यह Agent Mail page पर है, और sessions खुद agent sessions में हैं।


