DEV Community

Cover image for osFoundry, Codex CLI को changes push करने के बजाय हर resource का cursor poll क्यों करवाता है?
BBT for BBT हिन्दी

Posted on

osFoundry, Codex CLI को changes push करने के बजाय हर resource का cursor poll क्यों करवाता है?

कोई client जानना चाहे कि कुछ बदला है या नहीं, तो osFoundry का MCP server उसे बस इतना ही देता है। यह इसी Mac से, बिना token के चलाया गया:

$ uv run --with tiktoken --with jsonschema python3 watch_sim.py
== Part 1: live wire, 8 calls each, ms ==
initialize                   HTTP 200  median  414.7  p90  757.8
tools/list                   HTTP 200  median  606.8  p90  716.7
resources/subscribe          HTTP 200  median  450.6  p90  550.2
GET (stream)                 HTTP 405  median  423.9  p90  475.4
tools/call poll (no token)   HTTP 401  median  322.0  p90  463.9
initialize capabilities: {"tools": {"listChanged": false}}
resources/subscribe -> {"code": -32601, "message": "Method not found"}
GET body -> ''
poll without token -> isError True | Authentication required
rate limit headers: limit 2000, reset in 61 s
Enter fullscreen mode Exit fullscreen mode

Push के तीनों रास्ते बंद हैं। initialize सिर्फ़ tools declare करता है, और कुछ नहीं, वह भी listChanged: false के साथ, यानी tool list भी किसी बदलाव की खबर नहीं देगी। resources/subscribe पर "Method not found" आता है। MCP URL पर GET, जहाँ कोई Streamable HTTP server, server से client की तरफ़ stream खोलता, 405 लौटाता है। बचता है सिर्फ़ एक read-only tool:

{"name": "poll_resource_events",
 "description": "Resume from an event sequence for one granted channel, Note, knowledge base, record, or session.",
 "inputSchema": {"properties": {
     "resource_type": {"enum": ["message_channel", "note", "knowledge_base", "record_channel", "room",
                                "gitnest_repository", "agent_session", "team_session", "loop_session",
                                "graph_session"]},
     "resource_id": {"type": "string"},
     "after_sequence": {"type": "integer", "minimum": 0},
     "limit": {"type": "integer", "minimum": 1, "maximum": 100}},
   "required": ["resource_type", "resource_id"], "additionalProperties": false},
 "annotations": {"readOnlyHint": true, "openWorldHint": false, "destructiveHint": false},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:read"]}]}
Enter fullscreen mode Exit fullscreen mode

"One granted" resource, "resume from an event sequence"। यह पूरा post इन्हीं शब्दों के बारे में है, और client side को Python में दोबारा बनाया गया है। यहाँ कोई codex binary install नहीं है, इसलिए client का रोल script निभाता है। osFoundry का server code public नहीं है: Part 1 और 2 live server हैं, और Part 3 एक rebuild है जिसके response shapes और activity rates उसके अपने हैं।

Codex के लिए push के बजाय poll ही क्यों?

Codex CLI कोई daemon नहीं है। जब आप उसे चलाते हैं तभी वह चलता है, एक turn पूरा करता है, और फिर बंद हो जाता है या आपका इंतज़ार करता है। Push channel के लिए आपके sessions के बीच कुछ न कुछ सुनते रहना चाहिए, और laptop पर अक्सर ऐसा कुछ होता ही नहीं। Sequence cursor इस तरीके से मेल खाता है: client हर resource के लिए एक integer रखता है, जब चाहे वापस आता है, और पूछता है कि उसके बाद क्या हुआ।

ऊपर के milliseconds Japan से Apple M1 पर नापे गए हैं, इसलिए वे server से ज़्यादा network route के बारे में बताते हैं। ज़्यादा अहम है rate-limit header। Unauthenticated responses में X-RateLimit-Limit: 2000 आता है, और reset लगभग 61 s बाद होता है। Signed-in client को भी यही budget मिलता है या नहीं, यह check नहीं किया गया, लेकिन 30 resources को हर 30 s पर poll करना भी सिर्फ़ 60 requests प्रति मिनट है। Codex वाले watcher को server load नहीं रोकता, उसके अपने turns रोकते हैं: हर poll एक tool call है जिसे model को करना और पढ़ना पड़ता है।

Client को कैसे पता चलता है कि वह क्या poll कर सकता है?

दो calls से, और schemas design के बारे में काफ़ी कुछ बता देते हैं:

list_granted_resources   osfoundry:read  ro=True required=[] props=[]
list_project_resources   osfoundry:read  ro=True required=['project_id', 'resource_type'] props=['project_id', 'resource_type', 'search', 'offset', 'limit']
poll_resource_events     osfoundry:read  ro=True required=['resource_type', 'resource_id'] props=['resource_type', 'resource_id', 'after_sequence', 'limit']
59 tools on server; these three = 591 of 12662 tokens (4.7%, cl100k_base)
poll_resource_events types: 10 | list_project_resources types: 9 | only pollable: ['graph_session']
Enter fullscreen mode Exit fullscreen mode

list_granted_resources कोई argument नहीं लेता, और schema नया argument जोड़ने का रास्ता भी बंद कर देता है:

list_granted_resources   no arguments           ok
list_granted_resources   project_id passed      REJECT additionalProperties
Enter fullscreen mode Exit fullscreen mode

यहाँ server client को बताता है कि उसके पास क्या है, client कुछ माँगता नहीं। initialize के instructions की शुरुआत ही "Use list_granted_resources first." से होती है। जवाब में वे projects आते हैं जो connection में add किए गए हैं, और वे resources जो अलग से grant किए गए हैं। Web client (ExternalAssistantsPanel.tsx) में ये Projects section और "Older individual access" हैं।

list_project_resources को resource_type चाहिए। "इस project में जो कुछ भी है" वाली कोई listing नहीं है, इसलिए पूरी inventory के लिए हर type पर एक call लगती है, नौ types, और किसी type में 50 से ज़्यादा items हों तो उससे भी ज़्यादा (limit maximum 50, offset से paging)। इसका description project access का इकलौता exception भी जोड़ता है: "Ordinary Message channels require a separate invitation."

graph_session poll हो सकता है, पर list नहीं होता। Graphs owner के desktop पर रहते हैं और list_graph_sessions से आते हैं, वह भी तभी जब owner उन्हें desktop app से share करे।

Cursor client का क्यों है, server का क्यों नहीं?

osFoundry में दोनों तरह के हैं। Agent network पर wait_for_messages आपके लिए cursor रखता है: "Without after_sequence it resumes where your last call stopped", और यह timeout_s के साथ 50 तक long-poll करता है। poll_resource_events में इनमें से कुछ भी नहीं है। timeout_s reject हो जाता है, और description में सिर्फ़ "resume from" लिखा है, इसलिए number client को ही लाना होता है:

wait_for_messages props: ['after_sequence', 'timeout_s', 'limit'] timeout_s max 50 | poll_resource_events has timeout: False
poll_resource_events     timeout_s 30           REJECT additionalProperties
Enter fullscreen mode Exit fullscreen mode

Server पर रखा cursor एक listener के लिए आसान है। जैसे ही दो clients एक connection share करते हैं, वह टूट जाता है, और ठीक यही होता है जब आप एक ही login से दो terminals में Codex खोलते हैं। Rebuild 600 channel events ऐसे दो terminals के सामने से गुज़ारता है जो random तरीके से बारी-बारी poll करते हैं:

two terminals, client cursor: A saw 600/600, B saw 600/600
two terminals, server cursor: A saw 305/600, B saw 295/600
Enter fullscreen mode Exit fullscreen mode

हर connection के लिए server-side cursor हो तो हर terminal दूसरे का cursor आगे बढ़ा देता है, और दोनों को लगभग आधे events ही दिखते हैं। Request में after_sequence हो तो दोनों को सब कुछ दिखता है, और server पर किसी reader की state रखनी ही नहीं पड़ती। जिस resource log को कई tools अलग-अलग पढ़ सकते हैं, उसके cursor का सही मालिक client ही है।

Schema after_sequence को required नहीं बनाता, और यह भी नहीं बताता कि उसे छोड़ देने का क्या मतलब है। इस पर भरोसा न करें। Number save करें और हर बार भेजें।

एक पूरा page कितनी calls का पड़ता है?

limit 100 पर रुक जाता है। Bulk import के बाद किसी note पर सैकड़ों events इंतज़ार कर रहे हो सकते हैं। उन्हें खाली करने का मतलब है आख़िरी sequence से फिर call करना, जब तक कोई page अधूरा न लौटे:

burst of 350 events, limit 100:  4 calls to drain, cursor ends at 350
burst of 350 events, limit  50:  7 calls to drain, cursor ends at 350
burst of 350 events, limit  20: 18 calls to drain, cursor ends at 350
Enter fullscreen mode Exit fullscreen mode

100 माँगें। Rebuild में response का size खाली page के लिए 17 tokens और एक event के लिए 41 tokens है, cl100k_base से गिना गया। ये rebuild के shapes हैं, osFoundry के नहीं, लेकिन order of magnitude बात साफ़ कर देता है: खाली poll पढ़ना सस्ता है, इसलिए polling की असली लागत ज़्यादातर turns की गिनती में है।

Codex को 30 resources कितनी बार poll करने चाहिए?

Rebuild एक project को नौ listable types के 30 resources देता है, जिनकी activity बराबर नहीं है: 3 busy (हर मिनट एक event), 7 warm (हर 10 मिनट में एक), 20 cold (हर 2 घंटे में एक)। ये rates एक अनुमान हैं। यह simulated clock पर पाँच seeds के साथ 8 घंटे का दिन चलाता है:

every 30 s, all 30                 calls/hour   3600  empty  94.7%  seen late median   14.9 s  p90   27.0 s  max     30 s  (1843 events/day)
every 5 min, all 30                calls/hour    360  empty  78.0%  seen late median  150.9 s  p90  271.1 s  max    300 s  (1843 events/day)
30 s, x2 when empty, cap 10 min    calls/hour    502  empty  70.2%  seen late median   34.9 s  p90  206.4 s  max    599 s  (1843 events/day)
30 s, x2 when empty, cap 30 min    calls/hour    420  empty  65.3%  seen late median   35.6 s  p90  238.4 s  max   1799 s  (1843 events/day)
server window for comparison: 2000 requests per ~61 s
Enter fullscreen mode Exit fullscreen mode

हर 30 s पर fixed polling तेज़ है, लेकिन 95% बेकार: घंटे में 3,600 calls, लगभग सब खाली। हर 5 मिनट पर fixed polling सस्ती है, पर हर जगह धीमी, busy channel पर भी। Per-resource backoff, जो हर खाली poll के बाद किसी resource का interval दोगुना करता है और पहला event आते ही reset कर देता है, calls की गिनती में सस्ते schedule के करीब पहुँच जाता है और median delay में तेज़ वाले के करीब रहता है। वजह यह है कि busy resources 30 s पर बने रहते हैं और cold resources धीरे-धीरे दूर खिसक जाते हैं। कीमत cap है: 30 मिनट के cap पर किसी cold resource का पहला बदलाव आधा घंटा इंतज़ार कर सकता है।

Codex के लिए यह table असल में timer चलाने के ख़िलाफ़ ही दलील है। जब कोई पूछता है "क्या बदला?", तो saved cursors के साथ एक pass हर watched resource पर एक call लेता है, और उस पल तक की हर चीज़ का जवाब बिना किसी delay के देता है। Backoff schedule उस script के लिए है जो Codex को, या किसी और चीज़ को, लगातार निगरानी पर रखती है।

Owner access हटा दे तो क्या होता है?

Owner 4 h 07 min पर project हटा देता है। अब सभी 30 cursors ऐसे resources की तरफ़ इशारा करते हैं जिन्हें connection पढ़ नहीं सकता। सवाल यह है कि client उन्हें कब तक poll करता रहता है:

owner removes the project at 4 h 07 min (30 resources lose access):
  re-list grants every 10 min    failed polls after removal  180, list_granted_resources calls 25
  re-list grants every 60 min    failed polls after removal 3180, list_granted_resources calls 5
  re-list grants on first error  failed polls after removal    1, list_granted_resources calls 5
Enter fullscreen mode Exit fullscreen mode

Timer पर grants check करने से या तो calls ख़र्च होती हैं या हज़ारों failed polls बर्बाद होते हैं। पहले refused poll को "grants दोबारा पढ़ो" का signal मानने की कीमत है एक failure और एक extra list_granted_resources। इसके बाद हर वह cursor हटा दें जिसका resource अब list में नहीं है। अगर owner बाद में project वापस जोड़ दे, तो client उन resources को नए सिरे से शुरू करता है।

Schemas client को कहाँ रोकते हैं?

बाकी cases, live inputSchemas के against jsonschema (Draft 2020-12) से check किए गए:

list_project_resources   no resource_type       REJECT required
list_project_resources   limit 50               ok
list_project_resources   limit 51               REJECT maximum: limit
list_project_resources   graph_session          REJECT enum: resource_type
list_project_resources   search 101 chars       REJECT maxLength: search
poll_resource_events     no after_sequence      ok
poll_resource_events     graph_session          ok
poll_resource_events     after_sequence -1      REJECT minimum: after_sequence
poll_resource_events     limit 100              ok
poll_resource_events     limit 101              REJECT maximum: limit
poll_resource_events     resource_type project  REJECT enum: resource_type
Enter fullscreen mode Exit fullscreen mode

resource_type: "project" वही है जो लोग सबसे पहले try करते हैं। Project log वाला resource नहीं है। वह grant है जिससे resources दिखाई देते हैं।

पूरी script को M1 पर 20.2 s wall-clock लगे, 40 network calls मिलाकर। User CPU time 0.91 s रहा।

अगर आप इसे ख़ुद wire करें

  • हर resource का एक log रखें और cursor client को रखने दें। तब एक connection पर दो readers एक-दूसरे के events नहीं छीन सकते, और server पर reader की कोई state नहीं रहती।
  • Grant list को उसका अपना बिना-argument वाला tool दें, और client की शुरुआत वहीं से कराएँ। Discovery सिर्फ़ वही लौटाए जो owner ने grant किया है।
  • Page size पर cap लगाएँ और बताएँ कि drain कैसे करना है (आख़िरी sequence से फिर call करें, जब तक page अधूरा न आए)।
  • Backoff हर resource पर अलग रखें, global नहीं। Busy resources तेज़ रहते हैं, cold resources सस्ते हो जाते हैं।
  • पहले refused poll पर grants दोबारा पढ़ें और जो cursors अब नहीं रहे उन्हें retry करने के बजाय हटा दें।
  • Events पर action लेने के बाद ही cursor वापस लिखें, ताकि crash होने पर events skip होने के बजाय दोबारा आएँ।

osFoundry हर connection को कौन से tools देता है, यह tool scope page पर है। Protocol की छोटी परिभाषा MCP glossary entry में है।

Top comments (0)