DEV Community

Cover image for Claude chat बिना push events के MCP पर osFoundry tasks को कैसे claim करती है?
BBT for BBT हिन्दी

Posted on

Claude chat बिना push events के MCP पर osFoundry tasks को कैसे claim करती है?

>> {"jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": {"name": "claim_linked_chat_task",
    "arguments": {"chat_link_id": "3f6c0b1e-link", "task_id": "d97fd414-3916-4b13-937d-d78de6b3b053"}}}
<< {"jsonrpc": "2.0", "id": 7, "result": {"content": [{"type": "text",
    "text": "{\"claim_id\": \"8ffc774f-0a76-4846-9bbd-d66493ca08f4\", \"lease_until\": 1793942663.8222258}"}]}}

>> {"jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": {"name": "submit_linked_chat_result",
    "arguments": {"chat_link_id": "3f6c0b1e-link", "task_id": "d97fd414-…", "claim_id": "8ffc774f-…",
                  "status": "completed", "result": "1. Who signs off the pricing copy?"}}}
<< {"jsonrpc": "2.0", "id": 7, "result": {"content": [{"type": "text", "text": "{\"ok\": true}"}]}}

>> (same submit again, after a "lost" response)
<< {"jsonrpc": "2.0", "id": 7, "result": {"content": [{"type": "text", "text": "{\"ok\": true, \"replayed\": true}"}]}}
Enter fullscreen mode Exit fullscreen mode

ऊपर एक claim, एक submit और एक retried submit है। ये एक Python rebuild (180 lines का sqlite server) से capture किए गए हैं, जो उस hand-off को दोबारा बनाता है जिससे osFoundry किसी linked Claude या ChatGPT conversation को काम भेजता है। Tool names असली connector वाले ही हैं। Server, lease field और replay flag rebuild के अपने हैं। कोई Claude binary या Claude account इस्तेमाल नहीं हुआ: दूसरी तरफ़ की "chat" असल में एक Python script है, जो local sqlite file के against वही JSON-RPC calls करती है जो कोई host करता।

यहाँ problem का shape थोड़ा अलग है। Worker एक chat conversation है, जो तभी चलती है जब कोई इंसान उसे खोलकर कुछ type करे। Server उसे जगा नहीं सकता, देख नहीं सकता, और यह भी नहीं जान सकता कि इंसान ने task के बीच में tab बंद कर दिया या नहीं।

osFoundry असल में क्या expose करता है?

osFoundry के MCP connector पर चार tools यह hand-off संभालते हैं, और ज़्यादातर ज़रूरी बातें उनके schemas से ही पता चल जाती हैं:

Tool ज़रूरी arguments Description क्या वादा करता है
link_current_chat project_id, label, idempotency_key हर conversation के लिए अलग link; label और reference self-reported हैं
read_linked_chat_tasks chat_link_id Queued और हाल के tasks, OAuth connection और project तक scoped
claim_linked_chat_task chat_link_id, task_id "Repeating the claim returns the same claim while the task remains active."
submit_linked_chat_result chat_link_id, task_id, claim_id, status, result completed या failed, result 20,000 characters तक; cancelled, expired, revoked या mismatched claims complete नहीं किए जा सकते

Owner वाली side web app में है, apps/web/src/screens/settings/ConnectedChatTasks.tsx में। इसका status union queued | claimed | completed | failed | cancelled | expired है, और labels हैं "Waiting for host chat" (या "Waiting for Claude"), "Claimed by host chat", "Completed", "Failed", "Cancelled" और "Expired"। Assign करते समय goal के साथ crypto.randomUUID() से बना एक idempotency_key post होता है, और जब तक goal text नहीं बदलता, component वही key रखे रहता है। इसलिए double-click हो या timeout के बाद retry, key वही जाती है। Panel अपनी list setInterval(..., 15000) से refresh करता है। Tasks 24 घंटे बाद expire हो जाते हैं।

पाँचवाँ tool, upload_linked_chat_artifact, retry पर एक stable artifact_id लेता है। वही idea, बस files के लिए।

Server task को Claude तक push क्यों नहीं कर सकता?

क्योंकि host side पर कोई सुन ही नहीं रहा। osFoundry, Claude custom connector को request-driven मानता है: active conversation tools call कर सकती है, लेकिन ऐसा कोई general event trigger नहीं है जो नया turn शुरू कर दे। ChatGPT में subscribe करने वाली Work chats के लिए MCP Events हैं; owner panel सिर्फ़ उसी case में "An event subscription is active for this chat" दिखाता है, वरना "Open the host conversation to start each task."

यानी scheduler एक इंसान है। इससे यह बदल जाता है कि कौन-से failures normal माने जाएँ। छह घंटे से queued पड़ा task outage नहीं है। Claim हो चुका task जिसकी chat बंद हो गई, वह edge case नहीं, common case है। और retry सिर्फ़ network की बात नहीं है: model एक ही turn में tool को दो बार भी call कर सकता है।

एक ही task पर दो claims दोनों कैसे जीत जाते हैं?

सीधा handler row पढ़ता है, status == 'queued' check करता है, और claimed लिख देता है। Rebuild में यह version --naive 1 के पीछे है, और default में conditional version चलता है:

cur = c.execute("UPDATE tasks SET status='claimed', claim_id=?, lease_until=?, updated_at=? "
                "WHERE id=? AND chat_link_id=? AND status='queued'",
                (cid, now + self.lease, now, task_id, link))
if cur.rowcount == 1:
    return {"claim_id": cid, "lease_until": now + self.lease}
Enter fullscreen mode Exit fullscreen mode

Test: 200 tasks, और हर task के लिए आठ threads, जिन्हें एक threading.Barrier एक साथ छोड़ता है, HTTP पर ठीक एक ही पल में claim_linked_chat_task call करते हैं।

Handler 2+ winners वाले trials प्रति task औसत winners
पहले read, फिर write (बीच में 2 ms) 200 / 200 7.40
पहले read, फिर write (कोई extra gap नहीं) दो runs में 97 / 200 और 172 / 200 1.82 और 3.36
Conditional UPDATE … WHERE status='queued' 0 / 200 1.00

2 ms का gap दोनों statements के बीच किसी network hop या ORM की जगह रखा गया है। इसके बिना भी race लगभग आधे या उससे ज़्यादा trials में होती है, और यह number machine load के साथ ऊपर-नीचे होता रहा। SQLite का write lock दोनों writes को serialise तो करता है, पर इससे कुछ नहीं बचता: दोनों readers पहले ही फ़ैसला कर चुके होते हैं।

एक ही linked chat से आठ simultaneous claims बनावटी लगते हैं। दो नहीं: user chat से tasks check करने को कहता है, इंतज़ार नहीं करता, फिर से कहता है, और host दोनों turns चला देता है। इसी वजह से osFoundry का claim उसी link को error की जगह मौजूदा claim लौटा देता है। Rebuild भी उसी chat से repeat आने पर यही करता है:

"claim_replay": {"first": "28a03379", "repeat_same_chat": "28a03379", "same_claim": true,
                 "other_link": {"error": "not claimable: missing"}}
Enter fullscreen mode Exit fullscreen mode

कोई दूसरा link उसी task ID को माँगे तो उसे कुछ नहीं मिलता, यह confirmation भी नहीं कि task मौजूद है।

जिस chat ने task claim किया, वह गायब हो जाए तो क्या होना चाहिए?

यहाँ rebuild उससे आगे जाता है जो osFoundry docs linked chats के बारे में बताते हैं। Docs में 24 घंटे की task expiry लिखी है, और उस desktop dispatcher के लिए, जिससे कोई osFoundry agent linked chat का इंतज़ार कर सकता है, 30 मिनट की runtime limit। Rebuild इसमें एक per-claim lease जोड़ता है: जो claim lease_until तक complete नहीं होता, वह वापस queued में चला जाता है और फिर से claim किया जा सकता है।

Expiry lazy है। कोई timer thread नहीं है; हर read, claim और submit पहले एक sweep चलाता है:

c.execute("UPDATE tasks SET status='queued', claim_id=NULL, lease_until=NULL, updated_at=? "
          "WHERE status='claimed' AND lease_until<=?", (now, now))
Enter fullscreen mode Exit fullscreen mode

1-second lease के साथ test: chat A claim करती है और चुप हो जाती है। Chat B हर 100 ms पर claim करने की कोशिश करती है।

"lease": {"lease_s": 1.0, "takeover_after_s": 1.064, "failed_claims_before_takeover": 10,
          "late_submit_A": {"error": "rejected: stale claim, task was re-claimed"},
          "submit_B": {"ok": true}}
Enter fullscreen mode Exit fullscreen mode

Lease खत्म होने के 64 ms बाद B ने task ले लिया, यानी एक poll interval और request time। ज़्यादा अहम तीसरी line है: जब A वापस आई और अपने पुराने claim_id से submit किया, तो server ने उसे reject कर दिया। Claim ID यहाँ fencing token का काम करता है। अगर वह WHERE clause में न हो, तो A का देर से आया जवाब B के जवाब को overwrite कर देता, और owner को वही दिखता जो आख़िर में finish हुआ।

इंसान से चलने वाले worker के साथ lease की length तय करना सबसे मुश्किल हिस्सा है। बहुत छोटी रखें तो धीमा लेकिन ज़िंदा Claude turn जवाब के बीच में ही task खो देता है। बहुत लंबी रखें तो एक बंद tab घंटों तक काम पकड़े रहता है। Lease तभी फ़ायदेमंद है जब कोई और task उठा सके; अगर हर task के लिए ठीक एक ही linked chat है, तो 24 घंटे की expiry और owner का "Cancel" button ही ईमानदार design हो सकता है।

Submit का response खो जाए तो क्या होता है?

Host result भेजता है, server commit कर देता है, response लौटते हुए रास्ते में खो जाता है, और host retry करता है। Test एक ही submit चार बार भेजता है, फिर पाँचवीं बार अलग result के साथ:

Handler 4 एक जैसे sends के replies बदले हुए result का reply Stored result
हर call append करता है (message post करने जैसा) ok, ok, ok, ok ok 4\n4\n4\n4\n5
Claim-fenced, result hash compare करता है ok, फिर 3 × ok + replayed rejected: task is completed 4

Idempotent version row पर sha256(status + result) store करता है। उसी claim और उसी hash वाले retry को success मिलता है; terminal task के against बाकी सब refuse होता है। osFoundry अपने Graph relay पर completions के लिए भी यही rule लगाता है: lost response के बाद identical completion फिर से accept होता है, बदला हुआ refuse। Retry को success ही लौटाना चाहिए, error नहीं। Error मिलने पर model उस result को "fix" करने में लग जाएगा जो पहले ही save हो चुका है।

क्या cancelled task फिर भी complete हो सकता है?

नहीं, अगर cancel एक state change है और submit के लिए status='claimed' ज़रूरी है:

"cancel": {"cancel": {"cancelled": true}, "submit_after_cancel": {"error": "rejected: task is cancelled"}}
Enter fullscreen mode Exit fullscreen mode

दिलचस्प बात यह है कि chat को क्या दिखता है। Claude turn ने काम पूरा कर दिया; जवाब वापस लौट आया। osFoundry का tool description model को पहले से बता देता है कि cancelled claims complete नहीं हो सकते, और model को loop में retry करने से रोकने का यही सबसे सस्ता तरीका है।

Polling की cost कितनी है?

यहाँ दो अलग pollers हैं। Owner का browser panel हर 15 seconds पर poll करता है, यानी हर खुले panel से दिन में 5,760 requests। Host chat poll करती ही नहीं; जब भी आप कहते हैं, वह एक बार पढ़ लेती है। दोनों एक ही read query पर जाते हैं।

Rebuild पर measurement: 500 links में फैले 10,000 पुराने tasks, एक link के 2,000 reads, localhost HTTP:

Request Response Round trip p50 Server p50
खाली inbox 137 B 87 B 0.72 ms 0.26 ms
एक queued task 137 B 294 B 0.89 ms 0.30 ms

Bandwidth असली cost नहीं है। पहला version server पर लगभग नौ गुना धीमा था, और वजह lazy sweep थी: हर read पर दो UPDATE, जो कुछ भी match न करें तब भी SQLite का write lock ले लेते हैं। 2,000 empty polls के तीन runs:

हर read से पहले sweep Server p50 (3 runs)
दो UPDATE, table scan 2.17 / 2.02 / 1.96 ms
दो UPDATE, active rows पर partial index 1.33 / 1.33 / 1.20 ms
पहले partial index पर SELECT 1 … LIMIT 1, UPDATE तभी जब कुछ due हो 0.24 / 0.21 / 0.22 ms
कोई sweep नहीं 0.19 / 0.19 / 0.22 ms

Read-only guard लगाने पर poll का समय no-sweep के लगभग बराबर आ जाता है, फ़र्क बस noise जितना। सिर्फ़ index से यह नहीं होता, क्योंकि cost scan की नहीं, write transaction की है।

जो scripted client सच में poll करता है, उसके लिए trade-off वही जाना-पहचाना है। 40 tasks, जो 0.2 से 1.2 s के अंतर पर आते हैं:

Poll interval औसत pickup delay Max Polls Empty polls
0.1 s 0.058 s 0.107 s 244 204
0.5 s 0.226 s 0.484 s 54 17
2.0 s 1.103 s 2.006 s 15 2

औसत pickup लगभग interval के आधे पर आता है। Host chat के लिए असली interval है "जब भी user उसे खोले", और प्रति read जो cost मायने रखती है वह milliseconds नहीं, context है: हर read result conversation में जाता है। इस format में खाली inbox के लिए 87 bytes और हर queued task के लिए लगभग 210 bytes और लगते हैं, तो छोटी task list रखने से check इतना सस्ता रहता है कि आप बेझिझक कहें।

सारे numbers: Apple M1, Python 3.9.6, WAL mode में SQLite 3.43.2, 127.0.0.1 पर ThreadingHTTPServer, और machine पर साथ में दूसरे render jobs भी चल रहे थे (runs के दौरान load average 8 से 22 के बीच)। इसलिए p50s पर भरोसा करें और tails को noisy मानें।

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

  • Claim को एक single conditional write बनाइए और उसी caller को मौजूदा claim लौटाइए; double-click और double tool call normal case हैं।
  • Submit के WHERE clause में claim ID रखिए, ताकि देर से आया worker नए वाले को overwrite न कर सके।
  • Terminal result का hash store कीजिए; identical retry को success दीजिए, बदले हुए को refusal।
  • तय कीजिए कि lease से कुछ मिलता भी है या नहीं। अगर task कभी एक ही chat ले सकती है, तो expiry और दिखने वाला cancel शायद काफ़ी है।
  • Expiry checks तब तक read-only रखिए जब तक सच में कुछ due न हो; जो poll write lock लेता है, वह उस query से ज़्यादा महँगा पड़ता है जिसकी वह रखवाली करता है।
  • Tool description में साफ़ लिखिए कि क्या refuse होगा, ताकि model cancelled task पर retry न करता रहे।

Top comments (0)