Ini tool yang dipanggil chat Claude untuk memulai Graph di osFoundry, seperti yang tercantum di endpoint MCP publik. Diambil lewat tools/list tanpa autentikasi pada 2026-11-08, lalu dipangkas:
{"name": "run_graph_session", "description": "Queue a Graph run on the owner's online desktop in a connected project. Retrieve progress and result with get_graph_run. The desktop can go offline, and local tool approvals still apply.", "inputSchema": { "properties": { "graph_session_id": {"type": "string"}, "arguments": {"type": "object"}, "idempotency_key": {"type": "string", "minLength": 8, "maxLength": 128}}, "required": ["graph_session_id", "idempotency_key"], "additionalProperties": false}, "annotations": {"readOnlyHint": false, "destructiveHint": true}, "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:run"]}]} Tool ini punya tiga saudara. list_graph_sessions (project_id, limit 1–50) dan get_graph_run (command_id) bersifat read-only di bawah osfoundry:read. cancel_graph_run (command_id) ada di bawah osfoundry:run, sama seperti panggilan start. Dua respons asli dari akun yang terhubung, ID-nya dihapus:
list_graph_sessions {"project_id": "…"} → {"project_id": "…", "sessions": []} get_graph_run {"command_id": "<unknown>"} → error: Graph run not found or access removed List kosong itu kondisi default. Tidak ada yang muncul di sana sampai pemiliknya membagikan sebuah sesi dari desktop. Kata kerja di deskripsinya adalah "Queue": tool ini sudah mengembalikan respons sebelum Graph mulai berjalan. Sisa tulisan ini membahas kenapa begitu, dan apa harganya bagi agent di sisi seberang.
Apa itu sesi Graph, dan kenapa server tidak bisa langsung menjalankannya?
Graph adalah program JavaScript tersimpan yang dijalankan oleh fleet engine di desktop: jalur kondisional, agent paralel, retry terbatas, panggilan ke Graph lain. Di apps/web/src-tauri/src/graph_config.rs, run_graph_now adalah command Tauri yang dipakai layar Graph. Command ini meneruskan script ke run_fleet_script dengan batas token keras yang menolak gelombang agent berikutnya begitu batasnya tercapai. Sesi Graph adalah identitas permanen untuk satu Graph tersimpan di satu perangkat, gs_ ditambah 32 karakter hex, di tabel lokal graph_sessions.
Engine, agent yang di-spawn script, model lokal, folder, dan pengaturan approval per tool semuanya ada di proses desktop. Runner di sisi server butuh salinan semuanya. Indeks fitur menyebutnya terang-terangan: membaca Graph bisa di browser, menjalankannya butuh desktop.
Bagaimana request sampai ke desktop yang tidak bisa dihubungi siapa pun?
Tidak sampai. Desktop-lah yang meminta pekerjaan. apps/web/src/hooks/useConnectedGraphRelay.ts, yang di-mount sekali di jendela utama desktop, melakukan ini:
- Setiap 20 detik (dan tepat setelah pengaturan berbagi berubah), lewati putaran ini kalau putaran sebelumnya masih sibuk.
- Kirim ulang completion dari outbox lokal yang terakhir kali tidak sampai ke server.
- Untuk setiap project yang diaktifkan pemilik,
POST /external-assistants/graph-relays/syncdengan metadata sesi yang dicentang untuk project itu, maksimal 100: nama, deskripsi, slug, versi, status. Responsnya berisi command yang diantrekan untuk project itu. - Untuk setiap command, cek ulang bahwa project masih aktif, sesi masih dicentang, dan slug-nya masih sama. Kalau tidak, selesaikan sebagai
cancelleddengan "Graph session is no longer shared with this project". - Kalau lolos, jalankan lewat
run_graph_nowdi channelgraph:external:<command id>, dengan control poll 3 detik untuk pembatalan, lalu post hasil yang sudah dibatasi.
Jadi list_graph_sessions menampilkan apa yang diunggah sync terakhir, dan run_graph_session menulis baris yang akan ditemukan desktop di putaran berikutnya. ID command berupa UUID. ConnectedGraphProjectBinding menolak format lain sebelum menulis baris channel-ke-project yang mencatat run di bawah project yang terhubung.
Rekonstruksinya
Tidak ada binary Claude yang berjalan di Mac ini, jadi sim.py memerankan ketiga pihak di atas jam simulasi. Bagian desktop adalah port dari relay di atas: poll 20 s, skip saat sibuk, command serial, cek ulang sebelum menjalankan, control poll 3 s, outbox, batas hasil 100,000 karakter. Bagian server adalah pengganti, karena kodenya tidak publik. Bagian ini mengimplementasikan apa yang tertulis di deskripsi tool: antrean, hasil yang hanya bisa dibaca agent yang memulai run, pembatalan yang berbeda untuk status queued dan active. Bagian ini juga mengimplementasikan perilaku idempotensi yang diminta panduan CLI osFoundry ("reuse idempotency keys after a lost response"). Client-nya adalah Claude yang di-script.
Durasi run Graph-nya sintetis: lognormal, median 60 s, minimal 5 s. Setiap eksperimen memakai 500–2,000 run dengan seed, dan fase relay acak relatif terhadap request. Python 3.9.6 di Apple M1, 2.8 s wall time untuk semuanya. Output-nya ada di code/run.out.
Berapa lama sebuah run menunggu di antrean?
pickup wait (queued->started), n=2000: mean 9.8 s, median 9.8 s, p95 18.9 s, max 20.0 s Paling lama satu interval poll, rata-rata setengahnya. Itu harga dari desktop yang hanya membuat koneksi keluar. Desktop tidak membuka port dan tidak memegang socket jangka panjang, dan laptop yang ditutup cuma relay yang belum melakukan poll.
Biaya lainnya kurang kelihatan. Relay menjalankan command satu per satu dan melewati timer-nya selama sibuk. Lima run yang diantrekan bersamaan selesai secara berurutan:
| Run yang diantrekan bersamaan | Hasil pertama (median) | Hasil terakhir (median) | Hasil terakhir (p95) |
|---|---|---|---|
| 1 | 74 s | 74 s | 254 s |
| 3 | 73 s | 233 s | 483 s |
| 5 | 75 s | 399 s | 776 s |
Agent yang memecah pekerjaan jadi lima run Graph supaya lebih cepat justru dapat jadwal paling lambat. Satu desktop berarti satu worker. Kalau kamu butuh paralelisme, taruh di dalam Graph, tempat engine punya max_concurrency dan batas token untuk itu.
Seberapa sering agent sebaiknya memanggil get_graph_run?
Tidak ada juga channel push balik ke Claude. Setiap strategi menukar jumlah panggilan dengan seberapa telat agent menyadari hasilnya. 2,000 run per baris:
| Strategi poll client | Panggilan per run (rata-rata) | p95 | Telat menyadari hasil (rata-rata) | p95 |
|---|---|---|---|---|
| tiap 2 s | 47.1 | 118 | 1.0 s | 1.9 s |
| tiap 5 s | 19.1 | 48 | 2.5 s | 4.8 s |
| tiap 15 s | 6.7 | 16 | 7.5 s | 14.2 s |
| tiap 30 s | 3.6 | 8 | 14.9 s | 28.4 s |
| backoff 2, 4, 8 … 30 s | 6.6 | 11 | 14.8 s | 28.4 s |
| tunggu 20 s, lalu tiap 10 s | 8.8 | 23 | 5.0 s | 9.6 s |
Poll tiap 2 s menghabiskan 47 panggilan tool, artinya 47 kali lewat context model, demi menghemat beberapa detik pada job yang makan waktu semenit. Exponential backoff kelihatan pintar tapi kalah di sini: jumlah panggilannya sama dengan poll tetap 15 s, dan hasilnya disadari dua kali lebih telat, karena saat Graph biasa selesai, backoff-nya sudah mentok di 30 s. Baris terakhir memanfaatkan informasi dari relay. Tidak ada yang bisa mulai di interval poll pertama, jadi menunggu kira-kira satu interval sebelum cek pertama hampir tidak ada ruginya.
Apa yang sebenarnya dihentikan oleh cancel?
cancel at a random 0-40 s after queueing, n=2000: still queued 523, already running 1370 queued : terminal after 0.0 s (graph never started) running: terminal after mean 2.0 s, p95 3.3 s, max 3.5 s Cancel saat masih di antrean adalah perubahan state di server. Desktop tidak pernah melihat command-nya dan tidak ada token yang terpakai. Cancel saat sedang berjalan adalah flag yang dibaca desktop di control poll 3 detiknya, lalu desktop memanggil fleet_cancel_run dengan run ID yang didapat dari event fleet_started. Relay mengirim cancel itu sekali dan menyimpan hasilnya sebagai cancelled, meskipun Graph sempat selesai di antara flag dan stop.
Apa gunanya idempotency key?
idempotency_key wajib, 8–128 karakter. 200 run per pengulangan, dengan 30% balasan dibuang supaya client mencoba ulang:
| Perilaku retry | Panggilan terkirim | Run Graph yang diantrekan |
|---|---|---|
| key baru saat retry | 266 / 302 / 286 | 266 / 302 / 286 |
| key yang sama saat retry | 270 / 291 / 275 | 200 / 200 / 200 |
Untuk Graph yang membuat draf email atau menulis file, duplikat bukan sekadar semenit yang terbuang, tapi efek samping yang terjadi dua kali. Karena itulah key-nya wajib di schema dan tool-nya ditandai destructiveHint: true.
Bagaimana kalau desktop-nya offline?
desktop offline 600 s: statuses ['queued'], executed 0 back online: first start +3 s, all 5 ['completed'] by +131 s, executed 5 completion POST lost once, outbox=on : status completed, delivered 40 s after start completion POST lost once, outbox=off: status running, never delivered Pekerjaan yang diantrekan tetap aman walaupun desktop offline, karena antreannya ada di server. Titik lemahnya justru jalan pulang. Kalau desktop sudah menyelesaikan Graph dan POST-nya hilang, relay yang hanya menyimpan hasil di memori akan membuat run tertahan di running selamanya. Relay yang asli menulis setiap completion ke localStorage sebelum mem-post-nya dan baru menghapusnya setelah dapat 2xx, jadi putaran berikutnya, 20 s kemudian, yang mengirimkannya.
Seberapa besar hasilnya boleh?
Relay mem-post {execution_id, status, result, total_tokens, reported_cost} kalau hasil JSON.stringify-nya paling banyak 100,000 karakter. Kalau lebih, yang di-post adalah {execution_id, status, result_truncated: true}. Error dipotong jadi 2,000 karakter. Batasnya menghitung karakter, bukan byte:
result text that still fits (ascii): 99,899 chars = 99,899 UTF-8 bytes result text that still fits (japanese): 99,899 chars = 299,697 UTF-8 bytes Potongannya all-or-nothing. Tidak ada setengah hasil yang bisa disalahpahami, dan catatan run lengkapnya tetap ada di riwayat desktop di bawah execution_id itu. Bagaimanapun juga, itu teks yang banyak untuk disodorkan ke model, jadi Graph yang dirancang untuk dipanggil agent sebaiknya diakhiri dengan ringkasan, bukan dump.
Siapa lagi yang bisa melihat run-nya?
another agent reads the run: {'error': 'Graph run not found or access removed'} Di server pengganti, koneksi kedua yang meminta command_id milik Claude mendapat error yang sama dengan yang dikembalikan server asli untuk ID karangan. get_graph_run dan cancel_graph_run sama-sama dideskripsikan bekerja pada run yang "started by this Connected agent". Karena "bukan milikmu" dan "tidak ada" tidak dibedakan, ID yang bocor tidak mengonfirmasi apa-apa.
Kalau kamu membangun ini sendiri
- Biarkan mesin pemilik tool yang menarik pekerjaan. Poll keluar tetap jalan melewati NAT, mode sleep, dan laptop yang ditutup. Tentukan interval dengan sadar bahwa setiap pemanggil rata-rata menunggu setengahnya.
- Kembalikan tiket dari panggilan start, dan tulis di deskripsi tool, tool mana yang membaca hasilnya. Model tidak bisa menebak ada antrean hanya dari namanya.
- Taruh cek status pertama kira-kira satu interval pickup setelahnya. Poll tetap mengalahkan exponential backoff kalau durasi job berkisar di sekitar semenit.
- Wajibkan idempotency key di panggilan start. Untuk job yang punya efek samping, request yang di-retry tanpa key berarti run kedua.
- Simpan completion secara persisten di worker sebelum mengirimnya. Hasil adalah bagian dari round trip yang tidak bisa kamu hitung ulang dengan murah.
- Cek ulang consent di worker tepat sebelum menjalankan, bukan hanya saat job diantrekan. Pengaturan berbagi bisa berubah di antaranya.
Definisi singkat osFoundry untuk protokol di bawahnya ada di entri glosarium MCP, dan pola yang lebih luas tempat Graph berada ada di entri agent orchestration.
![Localization Mapping and SLAM — Ch.20: Map Server Architecture [KR]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F25xyjqac9fxdq03fcrwv.png)
![Localization Mapping and SLAM — Ch.25: Future of SLAM [KR]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mc9rptwgplvcf7jil7b.png)