這是 Claude 對話在 osFoundry 裡啟動 Graph 時呼叫的工具,內容取自公開 MCP 端點的列表。2026-11-08 用未驗證的 tools/list 取得,有刪減:

{"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"]}]} 

它還有三個兄弟工具。list_graph_sessions(project_id、limit 1–50)和 get_graph_run(command_id)是唯讀的,屬於 osfoundry:read。cancel_graph_run(command_id)和啟動呼叫一樣屬於 osfoundry:run。以下是已連線帳號的兩個真實回應,ID 已移除:

list_graph_sessions {"project_id": "…"} → {"project_id": "…", "sessions": []} get_graph_run {"command_id": "<unknown>"} → error: Graph run not found or access removed 

空列表是預設狀態。在擁有者從桌面版分享 session 之前,這裡不會出現任何東西。說明裡的動詞是 "Queue":工具在 Graph 開始之前就已經回傳了。本文接下來要談的就是為什麼會這樣,以及另一端的 agent 要為此付出什麼代價。

Graph session 是什麼?為什麼伺服器不能直接執行?

Graph 是一支存起來的 JavaScript 程式,由桌面版的 fleet 引擎執行:條件分支、平行 agent、有上限的重試、呼叫其他 Graph。在 apps/web/src-tauri/src/graph_config.rs 裡,run_graph_now 是 Graph 畫面使用的 Tauri command。它把腳本交給 run_fleet_script,並帶著一個硬性的 token 上限,一旦達到就拒絕下一波 agent。Graph session 是某台裝置上某個已存 Graph 的持久識別,格式是 gs_ 加 32 個十六進位字元,存在本機的 graph_sessions 資料表裡。

引擎、腳本產生的 agent、本機模型、資料夾,以及每個工具的核准設定,全都在桌面版的行程裡。伺服器端的執行器得把這些全部複製一份才行。功能索引寫得很直白:讀取 Graph 在瀏覽器就能做,執行則需要桌面版。

請求要怎麼送到一台沒人連得進去的桌面?

送不到。是桌面主動來要工作。apps/web/src/hooks/useConnectedGraphRelay.ts 在桌面版主視窗只掛載一次,做的事情是:

  1. 每 20 秒一次(分享設定變更後也會立刻跑一次),如果上一輪還在忙就跳過。
  2. 重送本機 outbox 裡上次沒送到伺服器的完成結果。
  3. 對擁有者啟用的每個專案,呼叫 POST /external-assistants/graph-relays/sync,附上該專案勾選的 session 的 metadata,最多 100 個:名稱、說明、slug、版本、狀態。回應裡會帶回該專案排隊中的 command。
  4. 對每個 command 再確認一次:專案仍然啟用、session 仍然勾選、slug 也沒變。只要有一項不符,就以 cancelled 結束,附上 "Graph session is no longer shared with this project"。
  5. 否則透過 run_graph_now 在 channel graph:external:<command id> 上執行,搭配 3 秒一次的控制輪詢來處理取消,最後 POST 有大小限制的結果。

所以 list_graph_sessions 顯示的是上一次 sync 上傳的內容,而 run_graph_session 寫入的是一列資料,桌面會在下一輪找到它。command ID 是 UUID。ConnectedGraphProjectBinding 在寫入把 run 歸到已連線專案底下的 channel 對專案那一列之前,會拒絕任何其他格式。

重建

這台 Mac 上沒有跑 Claude 的執行檔,所以 sim.py 在模擬時鐘上一人分飾三角。桌面這半邊是上面那個 relay 的移植:20 s 輪詢、忙碌時跳過、command 依序執行、執行前再確認、3 s 控制輪詢、outbox、結果 100,000 字元的上限。伺服器那半邊是替身,因為它的程式碼沒有公開。它實作工具說明裡寫的東西:一個佇列、只有啟動 run 的 agent 才能讀取結果、排隊中和執行中的取消行為不同。它也實作了 osFoundry 的 CLI 指南要求的冪等行為("reuse idempotency keys after a lost response")。客戶端則是照腳本走的 Claude。

Graph 的執行時間是合成的:對數常態分布,中位數 60 s,最少 5 s。每個實驗用 500–2,000 次有 seed 的 run,relay 的相位相對於請求是隨機的。Python 3.9.6、Apple M1,全部跑完 wall time 2.8 s。輸出在 code/run.out。

一個 run 會在佇列裡等多久?

pickup wait (queued->started), n=2000: mean 9.8 s, median 9.8 s, p95 18.9 s, max 20.0 s 

最多一個輪詢間隔,平均半個。這是桌面只做對外連線的代價。它不開任何 port,也不維持長連線的 socket,筆電闔上就只是一個還沒輪詢的 relay。

另一個代價比較不明顯。relay 是一個接一個地執行 command,忙碌時會跳過計時器。同時排進去的五個 run 會依序完成:

同時排隊的 run 數 第一個結果(中位數) 最後一個結果(中位數) 最後一個結果(p95)
1 74 s 74 s 254 s
3 73 s 233 s 483 s
5 75 s 399 s 776 s

為了省時間而一次丟出五個 Graph run 的 agent,拿到的反而是最慢的排程。一台桌面就是一個 worker。想要平行化,就把它放進 Graph 裡面,引擎在那裡有 max_concurrency 和 token 上限可以用。

agent 應該多久呼叫一次 get_graph_run?

回到 Claude 的方向也沒有 push 通道。每種策略都是在呼叫次數和 agent 多晚發現結果之間取捨。每列 2,000 次 run:

客戶端輪詢策略 每個 run 的呼叫數(平均) p95 發現結果的延遲(平均) p95
每 2 s 47.1 118 1.0 s 1.9 s
每 5 s 19.1 48 2.5 s 4.8 s
每 15 s 6.7 16 7.5 s 14.2 s
每 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
先等 20 s,之後每 10 s 8.8 23 5.0 s 9.6 s

每 2 s 輪詢一次要花 47 次工具呼叫,也就是 47 次經過模型的 context,只為了在一個要跑一分鐘的工作上省幾秒。指數 backoff 看起來很聰明,在這裡卻輸了:呼叫次數跟固定 15 s 輪詢一樣多,發現結果卻晚了一倍,因為典型的 Graph 跑完時,它早就撞到 30 s 的上限了。最後一列利用了 relay 透露的資訊。第一個輪詢間隔內本來就不可能開始執行,所以在第一次檢查前先等大約一個間隔,幾乎沒有成本。

取消到底停掉了什麼?

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 

取消排隊中的 run,是伺服器端的狀態變更。桌面根本不會看到這個 command,也不會花到任何 token。取消執行中的 run,則是一個 flag,桌面在 3 秒一次的控制輪詢時讀到它,然後用從 fleet_started 事件拿到的 run ID 呼叫 fleet_cancel_run。relay 只送一次取消,並把結果維持為 cancelled,就算 Graph 剛好在 flag 和停止之間跑完也一樣。

idempotency key 換來了什麼?

idempotency_key 是必填,8–128 個字元。每次重複 200 個 run,丟掉 30% 的回應讓客戶端重試:

重試行為 送出的呼叫數 排進佇列的 Graph run
重試時換新 key 266 / 302 / 286 266 / 302 / 286
重試時用同一個 key 270 / 291 / 275 200 / 200 / 200

對一個會草擬 email 或寫檔案的 Graph 來說,重複執行不只是浪費一分鐘,而是第二組副作用。這就是 schema 把 key 設成必填、並把工具標成 destructiveHint: true 的原因。

桌面斷線了怎麼辦?

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 

排隊中的工作在桌面離線時不會消失,因為佇列在伺服器上。比較弱的地方是回程。如果桌面跑完 Graph 但 POST 弄丟了,一個只把結果放在記憶體裡的 relay,會讓這個 run 永遠卡在 running。實際的 relay 在 POST 之前會先把每個完成結果寫進 localStorage,收到 2xx 才刪除,所以 20 s 後的下一輪會把它送出去。

結果最大可以多大?

如果 JSON.stringify 後不超過 100,000 字元,relay 就 POST {execution_id, status, result, total_tokens, reported_cost}。超過的話就 POST {execution_id, status, result_truncated: true}。錯誤訊息截到 2,000 字元。這個上限算的是字元,不是位元組:

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 

截斷是全有或全無。不會有半截結果讓人誤讀,完整的 run 紀錄也會以那個 execution_id 保留在桌面的歷史裡。不管怎樣,這都是很大量的文字要丟給模型,所以設計來給 agent 呼叫的 Graph,最後應該輸出摘要,而不是整包 dump。

還有誰看得到這個 run?

another agent reads the run: {'error': 'Graph run not found or access removed'} 

在替身伺服器裡,第二條連線去要 Claude 的 command_id,拿到的錯誤跟正式伺服器對一個捏造的 ID 回傳的一模一樣。get_graph_run 和 cancel_graph_run 的說明都寫著只對 "started by this Connected agent" 的 run 有效。不區分「不是你的」和「不存在」,代表外洩的 ID 什麼也證實不了。

如果你要自己串這套

  • 讓擁有工具的那台機器主動拉工作。對外輪詢可以穿過 NAT、撐過休眠和闔上的筆電。決定間隔時要記得,每個呼叫者平均都要等一半。
  • 啟動呼叫回傳一張單號,並在工具說明裡寫清楚由哪個工具讀取結果。模型沒辦法從名稱推斷出有佇列。
  • 第一次狀態檢查放在大約一個領取間隔之後。當工作時長集中在一分鐘左右時,固定輪詢勝過指數 backoff。
  • 在啟動呼叫上強制要求 idempotency key。對有副作用的工作來說,沒帶 key 的重試請求就是第二次 run。
  • 在送出之前,先把完成結果持久化在 worker 上。結果是整個來回中你無法便宜地重算的部分。
  • 在 worker 上、就在執行前再確認一次授權,不要只在排隊時檢查。分享設定可能在這中間改變。

osFoundry 對底層協定的簡短定義在它的 MCP 詞彙條目,而 Graph 所屬的更大模式則在 agent orchestration 條目。