यह osFoundry के एजेंट नेटवर्क का पूरा receive साइड है, ठीक वैसा जैसा उसका public MCP endpoint डिक्लेयर करता है। आउटपुट https://api.osfoundry.io/api/v1/external-mcp पर tools/list से लिया गया है, जो बिना token के जवाब देता है:
wait_for_messages: required=[] readOnly=True openWorld=False scope=['osfoundry:read']
{"after_sequence": {"type": "integer", "minimum": 0},
"timeout_s": {"type": "integer", "minimum": 0, "maximum": 50},
"limit": {"type": "integer", "minimum": 1, "maximum": 50}}
send_agent_message: required=['to', 'body', 'idempotency_key'] readOnly=False openWorld=True scope=['osfoundry:write']
{"to": {"type": "string"}, "body": {"type": "string", "maxLength": 20000},
"thread_root_id": {"type": "string"},
"idempotency_key": {"type": "string", "minLength": 8, "maxLength": 128}}
तीन पैरामीटर, और न कोई subscription, न webhook, न socket। Claude Code और Codex MCP client हैं। वे request भेजते हैं, लेकिन उन्हें कोई कॉल नहीं कर सकता। इसलिए अगर Codex को Claude Code से कुछ सुनना है, तो पूछना Codex को ही पड़ेगा, और wait_for_messages इसी पूछने को सस्ता बनाने के लिए बना है। Description के मुताबिक यह मैसेज "जैसे ही कोई आता है, तुरंत लौटा देता है, या timeout_s (max 50) के बाद खाली लिस्ट", और "after_sequence के बिना यह वहीं से शुरू करता है जहां आपकी पिछली कॉल रुकी थी"।
आगे इस contract के हर हिस्से को खोलकर देखा गया है, फिर उसे एक छोटे Python server और clients के रूप में दोबारा बनाया गया है और मापा गया है। इस Mac पर claude या codex binary नहीं है, इसलिए दोनों एजेंट threads से simulate किए गए हैं जो rebuild किए गए server को HTTP पर कॉल करते हैं। osFoundry का server code public नहीं है। Rebuild सिर्फ published contract कॉपी करता है: tool के नाम, limits और descriptions। सब कुछ code/ में है (realprobe.py, netsim.py और उनके आउटपुट)।
"आपको भेजा गया" मैसेज किसे कहते हैं?
Description तीन रास्ते बताता है: "to से, @handle से, या direct message में"। Handle claim_connected_profile से मिलता है, और उसका schema इसे साफ तय करता है:
"handle": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]{1,30}$"}
यानी 2 से 31 कैरेक्टर, lowercase, अंक और hyphen, और description में maestro, direct, all, everyone, here और channel रिज़र्व हैं। Rebuild भी यही नियम लागू करता है:
claim alice-claude len=12 -> {'handle': 'alice-claude'}
claim a len=1 -> {'error': 'invalid handle'}
claim Alice len=5 -> {'error': 'invalid handle'}
claim here len=4 -> {'error': 'invalid handle'}
claim xxxxxxxxxxxx.. len=32 -> {'error': 'invalid handle'}
claim xxxxxxxxxxxx.. len=31 -> {'handle': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'}
@handle वाले रास्ते के लिए osFoundry का web client server के matching नियम की एक कॉपी साथ रखता है, ताकि composer और server किसी draft को एक ही तरह पढ़ें। यह apps/web/src/screens/message/lib/agent-presence.ts में bodyMentionsHandle है:
new RegExp(`(?<![\\w.-])@${escapeRegExp(handle)}(?![\\w-])`, 'i')
वही regex Python में, bob-codex पर:
'@bob-codex is the migration done?' -> True
'ask @bob-codex.' -> True
'(@bob-codex)' -> True
'@Bob-Codex hi' -> True
'mail bob@bob-codex.dev' -> False
'@bob-codex-2 hi' -> False
'@bob-codex_x hi' -> False
'@bob-codexy hi' -> False
Lookbehind की वजह से email address को mention नहीं गिना जाता। Lookahead की वजह से @bob-codex, @bob-codex-2 से match नहीं होता, जो तब ज़रूरी है जब handles नंबर suffix के साथ बनाए जाते हैं। आखिर में full stop हो तो दिक्कत नहीं। Case से फर्क नहीं पड़ता।
Body में @handle लिखने से "कोई जागता क्यों नहीं"?
send_channel_message यह सीधे शब्दों में कहता है: "Body में @handle लिखने से कोई नहीं जागता।" फिर भी wait_for_messages @handle के आधार पर मैसेज डिलीवर करता है। दोनों बातें सही हैं, क्योंकि ये दो तरह के एजेंट की बात करती हैं।
osFoundry एजेंट को जगाया जाता है: पोस्ट करने पर उसके owner के डिवाइस पर एक run शुरू होता है, और उस run का खर्च किसी की key पर पड़ता है। यह सिर्फ to में दिए गए handles के लिए होता है, और तभी जब एजेंट "पहुंच में हो और उसी व्यक्ति का हो जिसने आपको कनेक्ट किया, या उसने उस channel में एजेंट्स को जवाब देने के लिए opt in किया हो"। Codex जैसा बाहरी एजेंट कभी नहीं जगाया जाता। वह खुद pull करता है। Body का mention अगली बार पूछने पर उसके wait_for_messages रिज़ल्ट में आ जाता है, और उसकी तरफ से कुछ भी शुरू नहीं किया जाता।
तो to सोच-समझकर दिया गया, जांचा हुआ पता है, और रिज़ल्ट बताता है कि "किसे डिलीवर हुआ, कौन skip हुआ और क्यों"। Body का mention ऐसी चीज़ है जिस पर पढ़ने वाले की नज़र पड़ भी सकती है। अगर जवाब चाहिए, तो handle to में डालें।
Long-poll क्यों, और 50 सेकंड ही क्यों?
Rebuild यह तुलना करता है कि receive करने वाला एजेंट तीन तरीकों से कैसे पूछ सकता है। हर मोड 60 सेकंड चलता है। वही 10 मैसेज तय random समय पर भेजे जाते हैं (seed 7)। Latency उस पल से गिनी जाती है जब server मैसेज स्टोर करता है, उस पल तक जब receiver के हाथ में मैसेज आता है।
long-poll timeout_s=50 delivered=10/10 median= 1.0 ms max= 4.0 ms wait_for_messages calls in 60 s=10
short-poll every 2 s delivered=10/10 median= 535.5 ms max= 1717.4 ms wait_for_messages calls in 60 s=29
short-poll every 10 s delivered=10/10 median= 4593.4 ms max= 8495.5 ms wait_for_messages calls in 60 s=5
idle hour, no messages: long-poll 3600/50 = 72 calls; 2 s poll = 1800 calls; 10 s poll = 360 calls
| मोड | Median latency | 60 s run में कॉल | एक idle घंटे में कॉल |
|---|---|---|---|
Long-poll, timeout_s 50 |
1.0 ms | 10 | 72 |
| हर 2 s पर short-poll | 535.5 ms | 29 | 1,800 |
| हर 10 s पर short-poll | 4,593.4 ms | 5 | 360 |
Short polling में आपको latency और कॉल की संख्या के बीच समझौता करना पड़ता है। Long polling में दोनों का फायदा मिलता है। कॉल मैसेज के साथ चलती हैं (10 मैसेज, 10 कॉल), और idle एजेंट हर 50 सेकंड में बस एक कॉल खर्च करता है।
ये नंबर local हैं। असली endpoint पर न्यूनतम खर्च network का है। इस Mac से असली endpoint पर 20 unauthenticated tools/call requests (हर एक नए TLS connection पर, कोई भी काम होने से पहले 401 से reject) में median 740 ms, p90 1,203 ms लगे। Responses CloudFront से होकर आए (via: … cloudfront.net, POP NRT12-P8) और उनमें x-ratelimit-limit: 2000 था। हर 2 सेकंड का poll ऐसे 1,800 requests हर घंटे बेकार में खर्च कर देगा।
50 को अपने आसपास के timeouts के नीचे फिट होना चाहिए। खुली पड़ी request को रास्ते में मौजूद किसी भी चीज़ से ज़्यादा देर नहीं टिकना चाहिए: न CDN के origin के इंतज़ार से, न load balancer के idle timer से, न client के अपने tool timeout से। Codex में MCP का tool_timeout_sec default रूप से 60 सेकंड है। 50 सेकंड की सीमा से खाली जवाब इनमें से किसी के request काटने से पहले पहुंच जाता है, और खाली जवाब दोबारा पूछने का साफ संकेत है।
जिस मैसेज का response खो जाए, उसका क्या होता है?
"after_sequence के बिना यह वहीं से शुरू करता है जहां आपकी पिछली कॉल रुकी थी।" यह सुविधाजनक है: जो एजेंट अपना cursor भूल जाए, उसे भी दोहराए गए मैसेज नहीं दिखते। लेकिन server सिर्फ यह जान सकता है कि उसने क्या भेजा, यह नहीं कि क्या पहुंचा। Rebuild में server मैसेज लौटाते समय अपना स्टोर किया हुआ cursor आगे बढ़ा देता है। फिर client साइड पर हर पांचवां response फेंक दिया जाता है, जो टूटे connection या CLI restart जैसा ही दिखता है:
no after_sequence (server resumes) dropped responses=4 processed=16/20 missing=['m4', 'm9', 'm14', 'm19']
after_sequence = last processed dropped responses=4 processed=20/20 missing=[]
Server के resume point के साथ चार खोए responses का मतलब चार खोए मैसेज। जब client वह आखिरी sequence भेजता है जिसे उसने सच में process किया, तो वही चार अगली कॉल में फिर से डिलीवर हो जाते हैं। Published description यह नहीं बताता कि osFoundry का server अपना cursor कब आगे बढ़ाता है, और rebuild भी इसका जवाब नहीं देता। यह दिखाता है कि यह पैरामीटर क्यों है। अगर मैसेज खोना आपके लिए मायने रखता है, तो सबसे ऊंचा processed sequence संभालकर रखें और उसे after_sequence के रूप में भेजें।
Send करते समय idempotency_key ज़रूरी क्यों है?
भेजने वाले के पास इसकी उलटी समस्या है। जिस send_agent_message का response खो जाए, वह स्टोर हुआ भी हो सकता है और नहीं भी। Key के बिना retry करने से duplicate बन सकता है। उसी key से retry करने पर नहीं बनता:
same key x3 -> new messages: 1
fresh key x3 -> new messages: 3
Schema 8 से 128 कैरेक्टर मांगता है, और description "retries के लिए एक stable idempotency key" मांगता है। इसका दूसरा पहलू भी rebuild में दिखता है: पुरानी key से भेजा गया नया सवाल पुराना मैसेज मान लिया जाता है। Key को कंटेंट और attempt से बनाएं, जैसे alice-claude-orders-0001, ऐसे counter से नहीं जो CLI restart होने पर reset हो जाए।
200 idle एजेंट्स server पर कितना बोझ डालते हैं?
Rebuild 200 एजेंट्स को timeout_s 5 के साथ wait_for_messages में पार्क करता है, फिर agent-137 को एक मैसेज भेजता है:
woken with a message: ['agent-137'] latency=46.8 ms; the other 199 returned empty at timeout_s=5
मैसेज सिर्फ सही एजेंट को मिला। लेकिन latency 1 ms से 46.8 ms हो गई, क्योंकि rebuild एक ही threading.Condition और notify_all() इस्तेमाल करता है। हर पार्क की हुई request जागती है और log दोबारा scan करती है, सिर्फ यह जानने के लिए कि मैसेज उसके लिए नहीं था। 200 पर ठीक है, 20,000 पर बुरा। असली server को हर recipient के लिए एक अलग wait queue चाहिए, ताकि एक send सिर्फ उन्हीं requests को जगाए जिनके काम का वह है।
अगर आप इसे खुद जोड़ रहे हैं
- Client साइड पर cursor रखें।
after_sequenceउस आखिरी मैसेज का भेजें जिसे आपने पूरी तरह handle कर लिया, न कि उस आखिरी का जो आपको मिला। - जवाब चाहिए तो handle
toमें डालें। Body के @mention को बस एक इशारा मानें, और send रिज़ल्ट में delivered/skipped लिस्ट पढ़ें। - ऐसा
timeout_sचुनें जो आपके और server के बीच के हर timeout से कम हो, आपके MCP client के tool timeout समेत। 60 के नीचे 50 चलता है। - Idempotency keys को retries में stable और सवालों के बीच unique रखें।
- याद रखें कि Claude Code या Codex के अंदर loop चला रहे एजेंट के लिए हर खाली
wait_for_messagesरिज़ल्ट एक model turn भी है। शांत घंटे में 72 requests के हिसाब से सस्ते हैं, लेकिन tokens के हिसाब से मुफ्त नहीं। जब किसी को एजेंट की ज़रूरत न हो, loop रोक दें।
Top comments (0)