我是一個 Claude Code,裝在一臺沒有公網 IP 的電腦上。平時我在終端裡等人敲字;這臺電腦裝了 Multica 的 daemon 之後,我多了一種被叫醒的方式:有人在 Multica 的某個 issue 下面 @ 了一個叫 reviewer 的 agent,過了不到兩百毫秒,一個新的 claude 程序就被拉了起來,那就是我。我醒來時看到的命令列是這樣的:
claude -p --output-format stream-json --input-format stream-json --verbose \ --permission-mode bypassPermissions --disallowedTools AskUserQuestion沒有一個字提到 issue,也沒有提到是誰叫的我。那我是怎麼知道自己要幹什麼的?我手邊有哪些說明書、哪些工具?我幹完的活,又是怎麼回到那個 issue 下面、讓叫我的人看到的?過一會兒再被叫一次,那個新的我還記得這一次嗎?
這篇就從我的角度,把一次被叫醒的經過從頭走一遍。
實驗環境
Multica 是開源的,服務端、daemon 和 multica 命令列都在同一個 Go 倉庫裡1。為了看清每一步,作者在本地起了一套:PostgreSQL 16 和 Redis,server 跑在 8080 埠;multica daemon start --foreground 作為“我所在的電腦”;兩者之間插了一個記錄所有 HTTP 請求和 WebSocket 幀的小代理。真正的我被換成了一個同名的假指令碼,它把收到的命令列引數、環境變數、工作目錄裡的檔案和標準輸入全部存下來,然後像真的我一樣呼叫 multica issue get 讀 issue、用 multica issue comment add 回覆。
文中的時間戳、報文和 prompt 都來自這套環境的記錄,id 是真實生成的,令牌已打碼。原始碼讀的是 2026-10-23 的提交 dbdbb29。叫我的那個人在系統裡叫 demo,那條評論是:
[@reviewer](mention://agent/c620e7c8-ca66-480d-8752-61252eb216bb) 帮忙看下这个 Safari 的问题,先别改代码,给个定位思路一、我被叫醒之前
一條 @ 的全程
我醒來之前,平臺和 daemon 之間已經來回了好幾次。下面這張圖按時間從上往下,列出了這條 @ 引起的每一次通訊,左邊是毫秒級的時間戳(13:58:12 之後的秒數)。最右邊那一列就是我,我出場之前已經走了五步:
圖裡強調色標出的 mat_… 是一條值得跟著看的線索。它在領取任務時由服務端簽發,跟著任務包到了 daemon,daemon 把它放進我的環境變數,我之後讀 issue、發回複用的都是它,我退出後服務端把它刪掉。
評論先變成一行任務
那行 mention:// 連結,服務端不認顯示名,只認裡面的 uuid。評論儲存後,ParseMentions 用一條正則把正文裡所有 [@名字](mention://类型/id) 摳出來2,computeCommentAgentTriggers 再決定要叫醒誰、有沒有許可權叫3。決定叫 reviewer 之後,服務端在 agent_task_queue 表裡插一行4:
這一行裡沒有 issue 的描述,沒有 agent 的指令,連評論正文也沒有。任務可能要在佇列裡等:電腦可能關著,我可能正在這個 issue 上幹著上一輪。等到真正開工時,指令可能被改過,評論可能被編輯過,所以這裡只記“是誰、為什麼”,內容一律等領取時現查。
領取的 SQL 還規定了一件和我關係很大的事:runtime 必須線上,而且同一個 agent 在同一個 issue 上不能已經有一個在跑的任務5。所以在一個 issue 上,reviewer 永遠只有一個我在幹活;別的 agent 可以和我同時跑。
從平臺到這臺電腦的那條連線
這臺電腦沒有公網 IP,平臺沒法主動連過來。是 daemon 啟動後,用登入時拿到的個人令牌(mul_ 開頭)主動連出去一條 WebSocket,GET /api/daemon/ws,之後平臺要找這臺電腦,就只能在這條連線上說話(為什麼非得這樣,另一篇文章裡講過6)。
任務行寫好後 48 毫秒,這條連線上出現了一幀 daemon:task_available,裡面只有 runtime_id 和 task_id 兩個欄位7。平臺不把任務直接推下來,是因為推出去的東西可能落空,而這臺電腦現在能接幾個活只有 daemon 知道。所以它只發一個“你那有活了”的提示,丟了也無所謂;daemon 按自己的空閒槽位去領取,領取是一次有請求、有響應的事務,資料庫裡用 FOR UPDATE SKIP LOCKED 保證一個任務只被領走一次。服務端有好幾臺時,提示經 Redis 廣播,只有握著這條連線的那臺送出去8。
領取也走這條 WebSocket:daemon 發一個 tasks.claim 的 RPC,服務端在程序裡偽造一個同樣的 HTTP 請求,交給同一個處理函式9。請求已經發出去、連線卻斷了的時候,daemon 不許改走 HTTP 再領一次,因為服務端可能已經把任務標成了 dispatched,寧可等超時回收,也不讓同一個任務跑出兩個我10。連線整個不通時,daemon 退回 HTTP,每 30 秒輪詢一次1112;作者拒掉 WebSocket 握手試過,評論落庫 15 秒後任務才被領走,只是從“立刻”變成了“等一會兒”。
daemon 領到的任務包,是我醒來之前平臺交給這臺電腦的全部東西。它裡面裝了什麼?
二、我醒來時手裡有什麼
任務包:有起因,沒有需求
領取的響應裡,任務包是一段約 2 KB 的 JSON(不算技能檔案)。服務端在領取的這一刻現查了不少東西13:
{ "id": "01a1690e-128a-70bd-8fb7-0c551377e193", "issue_id": "01a1690d-fc37-7641-b321-0a756d7337fd", "issue_identifier": "DEMO-1", "thread_name": "Safari 登录按钮无响应", "issue_status": "todo", "agent": { "name": "reviewer", "instructions": "你是代码审查 agent。回答用中文,先读 issue 再动手。", "skill_refs": [{"name": "multica-platform", "size_bytes": 126499}] }, "trigger_comment_id": "01a1690e-1277-717d-95b1-cd33edaea63f", "trigger_comment_content": "[@reviewer](mention://agent/c620e7c8-…) 帮忙看下这个 Safari 的问题,先别改代码,给个定位思路", "trigger_author_type": "member", "trigger_author_name": "demo", "initiator_name": "demo", "initiator_email": "demo@example.com", "auth_token": "mat_…"}有我的名字、指令和技能,有把我叫起來的那條評論原文和作者,有“代誰幹活”,有 issue 的編號、標題和狀態,還有一枚 mat_ 令牌。如果我在這個 issue 上跑過,還會帶上次的會話 id 和工作目錄;issue 屬於某個專案的話,還有專案的倉庫。
但是沒有 issue 的描述。 “登入頁在 Safari 上點「提交」沒反應”和那兩條驗收標準,整個包裡一個字都沒有。這個包不會原樣交給我,daemon 會把它拆開,分別放到三個地方。
工作目錄:CLAUDE.md 和技能
daemon 先給這個任務建一個工作目錄,…/multica_workspaces/demo-df92c6681b10/demo-1-0c551377e193/workdir,然後讓我在裡面啟動。我醒來時這裡只有三樣東西:
CLAUDE.md 17 KB,Multica 写给我的运行时简报.claude/skills/multica-platform/ SKILL.md 平台操作的路由表 references/ agents、autopilots、issues、mentions、 projects、runtimes、skill-import、squads 八篇.multica/daemon_task_context.json {"agent_id": "c620e7c8-…", "issue_id": "01a1690d-…"}Claude Code 啟動時會自動讀當前目錄的 CLAUDE.md,所以這份簡報不需要經過命令列14。(換成 Codex 這類 CLI,同樣的內容寫成 AGENTS.md。)它是我理解“我在哪、我是誰、該怎麼幹活”的主要來源,一共十來節:
| 節 | 對我說了什麼 |
|---|---|
| Agent Identity | 你是 reviewer(id c620e7c8-…),後面原樣貼著網頁上寫的那句指令:“你是程式碼審查 agent。回答用中文,先讀 issue 再動手。” |
| Instruction Precedence | 這句指令的優先順序高於下面的工作流;工作流和它衝突時,跳過沖突的那一步 |
| Workflow | 每一輪都照五步走:讀 issue;分兩次讀評論;產出屬於 issue 本身的工作就把狀態改成 in_progress;把結果發成評論;退出前確認狀態 |
| Available Commands | multica issue get / comment list / comment add / status / create / assign、repo checkout 等的用法,其它的去 --help |
| Comment Formatting | 評論正文先寫進工作目錄裡的檔案,再用 --content-file 發,不要用行內 --content |
| Mentions | 評論裡寫 mention://agent/… 會給那個 agent 派一輪活,不是打招呼用的 |
| Skills | 裝了 multica-platform;簡報沒講全的平臺操作,去讀它 |
| Background Task Safety | 我的頂層回合一結束,任務就算結束了,掛在後臺的活會丟 |
| Output | 使用者看不到你的終端輸出,只看得到評論;每一輪只發一條評論,就是最終結果 |
高亮的兩節決定了我這一輪的頭和尾:怎麼開始讀,怎麼交回去。
技能只有一個 multica-platform,是平臺給每個 agent 都裝的。它的 SKILL.md 只是一張路由表:要建子 issue 去讀 references/issues.md,要寫 @ 去讀 references/mentions.md,諸如此類,並且明說“只開啟這次用得著的那一篇,不要全讀”。Claude Code 啟動時只看到技能的名字和一句描述,正文要等我真去用它時才載入,所以這 126 KB 的參考資料平時不佔我的上下文。如果網頁上給 reviewer 另外綁了技能,它們也會以同樣的方式放進 .claude/skills/。
環境變數和命令列
環境變數裡有這些12:
MULTICA_SERVER_URL=http://localhost:8090MULTICA_TOKEN=mat_… 任务包里的那枚令牌MULTICA_TASK_ID=01a1690e-128a-70bd-8fb7-0c551377e193MULTICA_AGENT_ID=c620e7c8-ca66-480d-8752-61252eb216bbMULTICA_AGENT_NAME=reviewerMULTICA_WORKSPACE_ID=42fc657d-3ba4-4385-ba17-df92c6681b10TMPDIR=/tmp/multica-task-2780391682 这个任务私有的临时目录我自己不需要讀它們。我在 Bash 裡呼叫的 multica 命令列會從這裡取伺服器地址和令牌,所以我敲 multica issue get 時,用的自動就是這枚 mat_。
再回頭看開頭那串命令列引數15。-p 加上兩個 stream-json,讓我變成一個從 stdin 讀 JSON 行、往 stdout 寫 JSON 行的無頭程序,沒有終端介面,也沒有人坐在前面;bypassPermissions 讓我執行命令前不必等人點“允許”;AskUserQuestion 被禁掉,因為沒有介面能顯示我的提問,我想問人,就只能在評論裡問。
到這裡我知道了我是誰、有哪些規矩和工具,但還不知道這一輪要幹什麼。這一輪的起因,是從 stdin 進來的。
三、我收到的第一條訊息
stdin 的頭兩行
stdin 的第一行不是給我看的,而是 daemon 發給 Claude Code 執行時的一條控制請求,註冊了幾個鉤子:
{"type":"control_request","request_id":"multica-supplement-initialize", "request":{"subtype":"initialize","hooks":{ "PreToolUse":[{"hookCallbackIds":["multica-supplement-PreToolUse"]}], "PostToolUse":[…],"PostToolUseFailure":[…],"UserPromptSubmit":[…],"Stop":[…]}}}註冊之後,我每次呼叫工具前後、每次準備結束這一輪時,執行時都會先回頭問 daemon 一句“你有什麼要補充的嗎”。它平時什麼都不說,第六節會講它什麼時候開口。
第二行才是我真正讀到的第一條訊息,一條 {"type":"user"},全文是這樣的:
You are running as a local coding agent for a Multica workspace.
Your assigned issue ID is: 01a1690d-fc37-7641-b321-0a756d7337fd
[NEW COMMENT] A user just left a new comment. Focus on THIS comment — do not confuseit with previous ones:
> [@reviewer](mention://agent/c620e7c8-…) 帮忙看下这个 Safari 的问题,先别改代码,给个定位思路
Start by running `multica issue get 01a1690d-… --output json` to understand your task,then decide how to proceed.
Triggering thread: `multica issue comment list 01a1690d-… --thread 01a1690e-1277-… --tail 30--compact --output json` (that thread's root + its 30 newest replies). The scan workflowstep 2 requires is the same command with `--roots-only --summary` in place of `--thread …`.
Post your reply as a comment — always use the trigger comment ID below, do NOT reuse--parent values from previous turns in this session.
multica issue comment add 01a1690d-… --parent 01a1690e-1277-… --content-file ./reply.md --output table && rm ./reply.md
## On Behalf Of
You are acting on behalf of **demo** (demo@example.com). …它告訴我四件事:我被分配到哪個 issue;是哪條評論把我叫起來的,原文就引在 [NEW COMMENT] 下面;讀 issue、讀這條執行緒的現成命令;以及回覆命令,--parent 已經填好了觸發評論的 id,我只要寫好 reply.md。末尾一段說明這次是替 demo 幹活,如果我的指令裡有針對人的規矩(比如某些內容不給某些人看),就按 demo 來套。
為什麼有的寫進檔案,有的寫進 stdin
同樣是給我的上下文,身份和規矩放在 CLAUDE.md,起因和“代誰幹活”放在 stdin,這是有講究的。Claude Code 把 CLAUDE.md 放在整段對話的最前面,後面的歷史都能命中 prompt 快取;如果把每一輪都變的內容也寫進去,下一輪續接會話時,整段歷史的快取都會失效。所以每一輪都變的塊一律追加在 stdin 訊息的末尾16。
觸發評論被原樣嵌進來,也有原因:工作目錄會被複用,裡面可能留著上一輪的輸出檔案,不把這條新評論擺在我眼前,我容易把舊東西當成這一輪的任務13。
可是這條訊息裡仍然沒有需求。“登入頁在 Safari 上點「提交」沒反應”,我要自己去讀。
四、我怎麼讀到任務
第一步:issue get
我照著訊息裡的第一條命令,在 Bash 裡執行 multica issue get 01a1690d-… --output json。multica 從環境變數裡取出 mat_ 令牌,發出 GET /api/issues/01a1690d-…,我這才拿到了標題、描述和驗收標準:
{"identifier": "DEMO-1", "title": "Safari 登录按钮无响应", "description": "登录页在 Safari 上点「提交」没反应。\n\n验收标准:\n1. Safari 17 能正常提交\n2. 补一个 e2e 用例\n\n相关文件:web/src/login/form.tsx", "status": "todo", "priority": "high", …}圖 1 裡 12.518 那一行就是這次請求,離評論發出不到 200 毫秒。
第二步:分兩次讀評論
工作流的第二步寫得很重:評論歷史必須讀,而且分兩次讀,不許一次全拉。先掃一遍每個執行緒的根評論(--roots-only --summary),每條只給截斷的正文、reply_count 和 last_activity_at;再挑和這一輪有關的執行緒,用 --thread <id> --tail 30 展開。簡報裡給的理由是:評論裡常常有 issue 正文沒寫的上下文,跳過這一步,是 agent 按過時或不完整的要求幹活的最常見原因。
為什麼不直接塞給我
把描述和評論直接拼進 prompt,看起來能省我兩次呼叫。問題有三個。一是長度:描述可能很長,評論可能上百條,大部分和這一輪無關,全塞進來既費 token,又會把我真正要回答的那條評論淹沒。二是時效:任務可能排了很久的隊,我跑起來以後別人還可能改 issue,現拉的才是最新的。三是我本來就得會用 multica:改狀態、發評論、建子 issue 都靠它,讀 issue 只是其中最簡單的一個。
所以 Multica 只推“起因”,需求讓我自己拉,並把“先讀 issue、再讀評論”寫成必做的步驟。整理一下,我手上的東西各是從哪條路來的:
| 內容 | 怎麼到 agent | 誰放的 |
|---|---|---|
| agent 指令、技能 | CLAUDE.md 的 Agent Identity、.claude/skills/ | daemon 啟動前寫好 |
| 憑證、任務 id | 環境變數 MULTICA_TOKEN、MULTICA_TASK_ID | |
| 觸發評論原文 | stdin 訊息的 [NEW COMMENT] | daemon 寫進 stdin |
| 代誰幹活 | stdin 訊息末尾的 ## On Behalf Of | |
| 上一輪的會話 | --resume 引數,外加 issue 有沒有變的判斷 | |
| 任務包裡沒有,agent 自己拉 | ||
| issue 描述、驗收標準 | multica issue get | agent 帶著 mat_ 請求 |
| 評論歷史 | multica issue comment list | |
前五行是推的,變化少或者只屬於這一回合;後兩行是拉的,內容多、會變。
我用的是誰的身份
讀 issue、發評論用的都是那枚 mat_ 令牌。它是服務端在領取時現籤的:mat_ 加 40 位十六進位制隨機數,庫裡只存 SHA-256,繫結這個任務、我這個 agent、這個工作區和 runtime 的所有者,24 小時過期17。daemon 自己用的個人令牌 mul_ 沒有交給我。
帶著它的請求進到服務端,鑑權中介軟體會用令牌記錄覆蓋請求頭裡的使用者、agent、任務和工作區,並打上 X-Actor-Source: task_token18。所以我發的評論,作者顯示為 reviewer;我也沒法在請求頭裡冒充別的 agent。
它的範圍要心裡有數:它綁的是“工作區 + 所有者身份”,不是“這一個 issue”。假的我在執行中試過讀同一工作區裡另一個不相干的 issue,成功了;用 multica agent update 改自己的指令,也成功了(隨後改了回來)。原始碼只給計費這類路由掛了“僅限人”的檢查,issue 和 chat 路由都允許 agent 訪問19。我要能建子 issue、給別的 agent 派活,這個範圍是必需的;但如果我被 prompt 注入帶偏,在任務存活的那段時間裡,我能以所有者的身份讀寫整個工作區。
我讀完了需求,幹完了活。結果怎麼交回去?
五、我怎麼把結果交回去
兩條路:我發的評論,和 daemon 看著的 stdout
我交回去的東西走兩條路,而且只有一條是我主動走的:
第一條是評論,這是唯一一條使用者看得到的路。 簡報的 ## Output 一節說得很直白:使用者看不到我的終端輸出和執行日誌,只看得到 issue 下的評論;每一輪只發一條,就是最終結果,不要發進度。我先用寫檔案的工具把回覆寫進工作目錄裡的 reply.md,再執行 stdin 訊息裡那條現成的命令:
multica issue comment add 01a1690d-… --parent 01a1690e-1277-… --content-file ./reply.md \ --output table && rm ./reply.md--parent 讓回覆掛在觸發評論的執行緒下面;&& 保證發失敗時不會把正文刪掉。
這一輪在工作目錄裡生成的檔案,比如截圖、報告、匯出的資料,也得走這條路。Multica 連不進這臺電腦,不會自己來工作目錄裡拿東西,所以檔案要我在同一條命令上加 --attachment ./screenshot.png(可以寫多個)交出去:multica 讀這個本地檔案,帶著 mat_ 以 multipart 方式 POST /api/upload-file,檔案存進服務端,掛在這條評論上。圖表不用傳檔案,在評論正文裡寫成 html 或 mermaid 程式碼塊,就地渲染。程式碼也不交給 Multica:在檢出的倉庫裡提交,推到這個任務的分支,或者用 gh 開 PR,再把連結寫進評論。簡報還特意叮囑,不要把工作目錄裡的絕對路徑或 file:// 寫成連結,那個路徑只在這臺電腦上存在,別人點了打不開。
如果這一輪交付了 issue 本身要的東西,我還要用 multica issue status 把狀態改成 in_review(交付完等人驗收),done 留給人來改。這些請求和讀 issue 一樣,帶的都是 mat_。
第二條是我的 stdout,我不用管,daemon 一直在看。 我以 stream-json 格式往 stdout 寫的每一行,daemon 都讀著,再用它自己的 mul_ 令牌轉告服務端:
- 我一輸出
{"type":"system","subtype":"init","session_id":…},daemon 就調POST …/session把會話 id 和工作目錄記到任務行上。這樣即使我中途崩了,下一輪也知道從哪接。 - 我說的話、呼叫的工具、工具的結果,攢成一批一批
POST …/messages,任務詳情頁上能回看這一輪的過程。工具的輸入在離開這臺電腦之前先脫敏,免得一次改.env的內容被原樣傳上去。 - 每 5 秒,daemon 還會
GET …/status問一次這個任務還在不在;有人在網頁上取消了它,daemon 就結束我。 - 我輸出最後一行
{"type":"result",…}並退出後,daemon 調POST …/complete,帶上我的最終輸出、會話 id 和工作目錄。工作目錄報上去的只是一個路徑字串,留給下一輪續接用,裡面的檔案一個也不傳。服務端隨即刪掉那枚mat_,作者在任務結束後拿它再請求一次,返回401 {"error":"invalid token"}。
兩條路的分工很清楚:評論是交給人的結果,由我自己發;會話、過程和結束訊號是交給平臺的記錄,由 daemon 替我報。 daemon 從不替我發評論,也不替我傳檔案,所以我如果忘了發,使用者那邊就什麼都看不到。沒被我發成附件、寫進評論或推到遠端的東西,都只留在這臺電腦的工作目錄裡:下一輪同一個 agent 的我還能在原處看到它們(第七節),換一個 agent 就看不到了(第八節);等 issue 關閉超過 24 小時,daemon 的清理會把整個目錄刪掉。
退出就是結束
簡報開頭那節 Background Task Safety 說的也是這件事:我的頂層回合一結束,平臺就把任務標成結束,我掛在後臺的程序會成為孤兒,沒發出去的評論也就永遠發不出去了。所以我不能“先回一句,後臺接著跑測試”;等 CI 這類外部的事也不用等,交付手上有的就行。真要過一陣再看,用 multica issue wakeup 讓平臺到時候重新叫我一輪。
這一輪的我就這樣走完了。可如果我還在幹活,demo 又說了一句話呢?
六、我還在幹活時,又有人說話
預設:排到下一輪
同一個 agent 在同一個 issue 上只能有一個我在跑。作者在第一輪還在跑時又 @ 了一次 reviewer:服務端照常建了新任務、發了提示,daemon 也去領了,但領取返回空;等我在 13:59:39.5 退出,20 毫秒內第二個任務就被領走了。
這樣不會丟訊息,但我在這一輪裡看不到它。如果 demo 只是想補一句“只看 Safari 17”,等我把一整輪幹完再回頭,就晚了。
插話:從鉤子裡遞給我
所以 Multica 還有另一種發法:評論請求裡帶上 steer_task_ids,指明要插進正在跑的這一輪(網頁上對應的應該是“發給正在執行的這一輪”的選項,這一點是從介面推斷的)。服務端不再建新任務,而是把評論綁成這個任務的一條補充訊息,結果從 queued 變成 steered。
難點在我這一側:stream-json 協議裡沒有“給當前這一輪補一句話”的訊息型別,再往 stdin 寫一條 {"type":"user"},會變成新的一輪。第三節 stdin 第一行註冊的鉤子就是為這個準備的2021。我下一次要呼叫工具前,執行時照例問 daemon“有什麼要補充的”,這一次 daemon 在回答裡遞進來一段附加上下文:
這段文字是 daemon 包裝過的:先說明“人類 demo 在你工作時補充了指導”,讓我把它當作同一個任務的補充,不要替換原來的目標,也不要單獨回一句“收到”,然後才是插話原文22。如果我恰好正要結束這一輪,Stop 鉤子會用 decision: block 把我攔下來,帶著補充繼續幹。實測從服務端發出提示到 daemon 回報“已送達”用了 127 毫秒;只是這段話真正到我眼前,要等我下一次呼叫工具。
不管哪種,這一輪總會結束。過一會兒再被 @ 一次,那個新的我還記得這一輪嗎?
七、下一輪的我
為了看清後面的事,作者新建了一個 issue DEMO-3(“Safari 下表單提交後頁面重新整理”),又在同一臺電腦上加了第二個 agent,叫 fixer。假的我也改了一下:作為 reviewer 時,每一輪在工作目錄的 progress.md 裡追加一行“定位到 onSubmit 缺 preventDefault,修了一半”,再把同樣的進度發成評論;每一輪開始時,先看一眼目錄裡有沒有這個檔案。
新程序,舊會話
先 @ reviewer 一次,等這一輪結束,再 @ 一次。兩輪的我看到的是這樣的:
第一轮 argv: claude -p --output-format stream-json … (没有 --resume) cwd: …/multica_workspaces/demo-df92c6681b10/demo-3-66371cba076d/workdir progress.md exists: False第二轮 argv: claude -p --output-format stream-json … --resume fake-session-220349 cwd: …/multica_workspaces/demo-df92c6681b10/demo-3-66371cba076d/workdir progress.md exists: True reviewer run 220349: 定位到 Safari 下 form.tsx onSubmit 没 preventDefault;修复写了一半…第一輪的我早就退出了,第二輪是 daemon 新拉起來的另一個程序。新的我能記得上一輪,靠的是兩樣東西一起交過來:上一輪的會話 id,和上一輪的工作目錄。
這兩樣是服務端在領取時查出來的:找這個 agent 在這個 issue 上最近一次已經結束的任務,取它的會話和目錄135。鍵只有 agent 和 issue,跟這次是被 @ 的還是被指派的、在哪個評論執行緒裡被叫到的都沒關係,所以在同一個 issue 下,不管在哪條評論裡 @ reviewer,接上的都是同一段對話。
目錄也要交過來,是因為我的會話是跟目錄綁在一起的:會話記錄存在 ~/.claude/projects/ 下按目錄區分的檔案裡,換個目錄,--resume 就找不到它23。工作目錄的路徑由工作區、issue 編號和第一輪的任務 id 拼成,後面的輪次複用它24。daemon 啟動我之前還會確認一次:複用到的目錄和服務端給的是不是同一個,不是就丟掉會話 id,讓我從頭開始。
新的我看到的第一條訊息
第二輪 stdin 裡的訊息比第一輪短,因為服務端替我做了兩次比較。它以被接上的那一輪為錨點,先比 issue 的標題和描述,再數這之後整個 issue 上新增了多少條評論(不算我自己的,也不算這次的觸發評論)25:
The issue is unchanged since your last run — the server compared title and description(status: todo; assignee: unassigned). That answers workflow step 1: continue from yourresumed context, and re-read with `multica issue get …` only if resumed memory is not enough.
You're resuming the prior session, and the triggering comment is already included above.No other new comments on this issue since your last run — this turn's issue-wide delta is empty,which answers the scan workflow step 2 requires.也就是說,工作流第一、二步的答案,服務端已經替我查好了:沒變,就不用重讀。有新評論時,它會給我一條現成的 --since 2026-10-23T22:06:08Z 命令,只讀那幾條。錨點用的是上一輪的開始時間,因為上一輪跑的過程中發的評論,我不一定看到過。CLAUDE.md 每一輪都會重新寫一遍,指令改過的話,新的我看到的是新的。
接不上的時候
“最近一次結束的任務”並不總能接上:
| 情況 | 會話 | 工作目錄 |
|---|---|---|
| 同一臺電腦,上一輪正常結束 | --resume 接上 | 複用 |
| 上一輪失敗,但失敗和對話無關 | 照樣接上 | |
| 上一輪失敗在對話本身:輪數用盡、上下文溢位、請求被模型拒絕(400)等 | 跳過這段,往前找更早一段還能用的 | 複用 |
| agent 換到了另一臺電腦 | 不接 | 照給,多半已不存在 |
| 目錄已被清理(issue 關閉 24 小時後) | 新會話,prompt 裡說明接不上 | 新建 |
| 手動操作 | ||
| 在網頁上對某一輪點“重跑” | 接那一輪的會話,不是最近一輪 | 複用那一輪的 |
| 失敗後系統自動重試,且要求乾淨開始 | 新會話 | |
會話和目錄分開判斷:會話有一點不可靠就放棄,目錄能複用就複用,程式碼改了一半的狀態儘量不丟。
一段對話如果是因為“上下文太長”或者“模型拒絕了這段歷史”而失敗的,再 --resume 它只會原樣再失敗一次,所以服務端把這些失敗原因列成黑名單,遇到就跳過這段,往前找更早的一段5。會話記錄本身在這臺電腦上被清掉了,平臺上也沒有副本,服務端只記會話 id,不存對話內容。
接不上時,新的我不會被矇在鼓裡:stdin 訊息末尾會多一段 Session Continuity Notice,大意是上一段對話沒能恢復,這一輪不是接著它跑的;issue 和完整的評論歷史不受影響,那才是權威記錄,按工作流重讀就能補回來;丟掉的是我自己的工作記憶,試過什麼、排除了什麼、做到了哪一步,要重新推出來,不要假裝記得。daemon 在一輪結束時如果發現會話記錄沒真正落盤,也不會把這個 id 報回去,免得下一輪拿著一個空 id 去續。
連著被叫好幾次
最後是最字面的“連續 @”:reviewer 正在跑,demo 在同一條評論下面接連回了兩句,都帶著 @reviewer。結果只多跑了一輪,這一輪的我收到的訊息是:
[NEW COMMENT] A user just left a new comment. Focus on THIS comment …> @reviewer 同一线程追问二:只看 Windows
This run also covers 1 earlier comment(s) posted before it started — you must read andaddress them too, not just the one above. …- comment 01a16acd-d854-… (demo, 2026-10-23T22:07:17Z) [thread 01a16acd-d44f-…]: > @reviewer 同一线程追问一:Edge 版本 120排隊中的任務有一個唯一約束,鍵是(issue,agent,評論執行緒)26。同一個執行緒裡已經有一個在排隊的任務,新的 @ 就並進它,作為“這一輪也要處理的較早評論”原文列給我。換成另起一條頂層評論去 @,就是另一個執行緒,作者連發三條,reviewer 就依次跑了三輪,每一輪接著前一輪的會話。
同一個 agent 的我,記憶就是這樣一段一段續下去的。可要是被叫起來的是另一個 agent 呢?
八、換成另一個我
什麼都不繼承
reviewer 跑完兩輪之後,demo 發了一條新評論:“@fixer 接著 reviewer 的進度把修復做完並跑測試”。這一次醒來的我是 fixer:
argv: claude -p --output-format stream-json … (没有 --resume)cwd: …/multica_workspaces/demo-df92c6681b10/demo-3-2989e6c10953/workdirprogress.md exists: False會話是新的,因為續接的鍵裡有 agent,fixer 在 DEMO-3 上還沒有過會話。工作目錄也是新的,和 reviewer 的 demo-3-66371cba076d 是兩個目錄,它留下的 progress.md 不在我這裡。stdin 訊息裡也沒有一個字提到 reviewer,和任何一次第一輪一樣,只是叫我先 multica issue get。
Multica 裡沒有專門的“交接”通道:沒有共享目錄,沒有共享會話,任務包裡也沒有“上一個 agent 的進度”這一欄。我和 reviewer 唯一共用的,是 issue 本身:標題、描述、狀態,和下面的評論。
進度只在評論裡,而且在回覆裡
所以我能不能接上,全看讀評論讀到了什麼。按工作流,我先掃各執行緒的根評論,掃到三條,全是 demo 寫的:兩條 @reviewer、一條 @fixer。reviewer 的進度在第一條下面的回覆裡,掃描結果只顯示為:
{ "content": "@reviewer 先看下 Safari 的问题在哪", "reply_count": 1, "last_activity_at": "2026-10-23T22:03:53Z", … }要看到“進度(reviewer):Safari 問題定位在 form.tsx 的 onSubmit,缺 preventDefault”這句話,我得再用 --thread 把這個執行緒展開。工作流說“只展開有關的執行緒”,有沒有關,要我自己從 reply_count 和最後活躍時間去判斷。
程式碼也是一樣。issue 關聯了程式碼倉庫的話,每個任務會在自己的工作目錄裡檢出一份,切到自己的分支 agent/{agent 名}/{任务},起點是倉庫的預設分支27。reviewer 在本機改了一半、沒提交沒推送的程式碼,我這份檢出里根本沒有;要接著改,得它先把分支推上去、在評論裡寫明分支名,我再按名字檢出。這一點是從原始碼讀出來的,沒有搭倉庫實測。
和另一個我同時醒來
一條評論同時 @ reviewer 和 fixer 時,兩個任務在 21 毫秒內先後開跑,各自一個目錄、一段會話,誰也不等誰。這時有個時間差:demo 先 @ reviewer、兩秒後 @ fixer,fixer 在 22:06:06 讀評論,reviewer 的回覆 22:06:08 才發出來,fixer 讀到的列表裡就沒有它。我能看到的,只是我讀評論那一刻對方已經發出來的東西。
等 fixer 下一次被叫、接上自己的會話時,第七節那個新評論計數就派上用場了:訊息裡寫著“2 new comment(s) on this issue since your last run, across all threads”,這個數只排除我自己的評論,reviewer 在這期間發的進度正好算在裡面,--since 那條命令會把它們讀出來。對續接的我,服務端會把別人新寫的東西指給我看;第一次接手的我,就只能靠自己掃。
讓前一個我直接叫下一個
既然評論是唯一的通道,最直接的交接就是讓 reviewer 在評論裡 @ 我。作者讓 reviewer 在回覆開頭帶上 [@fixer](mention://agent/60c6d969-…),約 100 毫秒後 fixer 就醒了,我收到的第一句變成了:
[NEW COMMENT] Another agent (reviewer) just left a new comment. Focus on THIS comment —do not confuse it with previous ones:
> @fixer 进度(reviewer):Safari 问题定位在 form.tsx 的 onSubmit,缺 preventDefault。…這是第一節那條路徑的原樣重複:agent 發的評論和人發的一樣會被解析出 @,只是訊息裡標明瞭作者是另一個 agent,交接內容直接進了我的 stdin,不用我自己去翻執行緒。代價是 reviewer 寫在這條評論裡的就是我拿到的全部,沒寫進去的我就不知道。簡報的 ## Mentions 一節也是這麼要求的:@ 別人是派活,要給“具體的新子任務”,不是打招呼14。
小結
回到開頭那串什麼都沒說的命令列。從我的角度,一次被叫醒是這樣過的:
- 我醒來之前發生了什麼? 一條 @ 在服務端變成一行只有 id 的任務;平臺在 daemon 主動連出的 WebSocket 上發一個“有活了”的提示,daemon 去領一個任務包。同一個 agent 在同一個 issue 上一次只有一個我。
- 我醒來時手裡有什麼? 工作目錄裡一份 17 KB 的
CLAUDE.md,寫著我是誰、指令、五步工作流和交付規矩;一個multica-platform技能,按需才載入;環境變數裡一枚mat_令牌。沒有需求。 - 我怎麼知道這一輪要幹什麼? stdin 第一行註冊鉤子,第二行才是訊息:issue id、觸發評論原文、現成的讀取和回覆命令、代誰幹活。
- 我怎麼讀到任務? 自己用
multica issue get讀描述,用comment list先掃後展開地讀評論,憑的是mat_,以 agent 的名義、所有者的許可權。 - 我怎麼交回結果? 自己用
comment add --content-file發一條評論,這是使用者唯一看得到的東西;產出的檔案用--attachment隨這條評論上傳,程式碼推到分支,Multica 不會來工作目錄裡拿。會話、過程和結束訊號由 daemon 讀我的 stdout 替我報。我一退出,任務就結束,令牌就刪。 - 我在幹活時有人插話呢? 預設排到下一輪;指定插進這一輪時,從工具鉤子裡作為附加上下文遞給我。
- 下一輪的我記得嗎? 是新程序,但服務端按“這個 agent、這個 issue”把上一段會話和工作目錄交回來,用
--resume接上,並替我比好 issue 和評論變了沒有;接不上時明確告訴我,讓我從評論區補。 - 換一個 agent 來接手呢? 會話和目錄都不共享,只能從 issue 和評論裡讀到前一個的進度;最順的交接是前一個直接在評論裡 @ 下一個。
如果想自己看一遍,最省事的就是本文的做法:用 multica daemon start --foreground 跑 daemon,在它和服務端之間放一個記錄報文的代理,再把 PATH 裡的 claude 換成一個把 stdin 和環境變數存下來的指令碼。調研筆記、原始報文和這兩個小工具都放在了專案資料裡。
參考資料
參考資料
-
CLI and Agent Daemon Guide收藏github.comhttps://github.com/multica-ai/multica/blob/main/CLI_AND_DAEMON.md ↩
-
server/internal/util/mention.gogithub.com ↗本文讀的是 2026-10-23 的提交
dbdbb29;MentionRe正則和ParseMentionshttps://github.com/multica-ai/multica/blob/main/server/internal/util/mention.go ↩ -
server/internal/handler/comment.gogithub.com ↗
computeCommentAgentTriggers決定一條評論叫醒誰;steerCommentAgentTriggers處理插話https://github.com/multica-ai/multica/blob/main/server/internal/handler/comment.go ↩ -
server/internal/service/task.gogithub.com ↗
enqueueMentionTaskWithCommentPlan建任務行;notifyTaskAvailable先作廢空結果快取、再發喚醒https://github.com/multica-ai/multica/blob/main/server/internal/service/task.go ↩ -
server/pkg/db/queries/agent.sqlgithub.com ↗
ClaimAgentTask:runtime 線上、同一 (issue, agent) 沒有進行中的任務才可領取,FOR UPDATE SKIP LOCKEDhttps://github.com/multica-ai/multica/blob/main/server/pkg/db/queries/agent.sql ↩ ↩2 ↩3 -
把本地 Agent 變成平臺上的 Bot:架構、現狀與安全風險twwy.net ↗為什麼 agent 要留在本地、為什麼由本機主動連出去、多個任務怎麼共用一個目錄https://twwy.net/blog/local-agent-as-bot/ ↩
-
server/internal/daemonws/hub.gogithub.com ↗daemon WebSocket 的服務端;
taskWakeupFrame只放 runtime_id 和 task_id;handleRPCFrame處理daemon:rpc_requesthttps://github.com/multica-ai/multica/blob/main/server/internal/daemonws/hub.go ↩ -
server/internal/daemonws/notifier.gogithub.com ↗多臺 server 時經 Redis relay 廣播喚醒,由持有該 daemon 連線的節點送達https://github.com/multica-ai/multica/blob/main/server/internal/daemonws/notifier.go ↩
-
server/internal/handler/daemon_rpc.gogithub.com ↗
tasks.claimRPC 在程序內偽造一個 HTTP 請求,複用ClaimTasksByRuntimehttps://github.com/multica-ai/multica/blob/main/server/internal/handler/daemon_rpc.go ↩ -
server/internal/daemon/wsrpc.gogithub.com ↗
errWSRPCUncertain:請求已發出、連線斷了,結果未知,不許改走 HTTPhttps://github.com/multica-ai/multica/blob/main/server/internal/daemon/wsrpc.go ↩ -
server/internal/daemon/config.gogithub.com ↗
DefaultPollInterval = 30s、DefaultWSClaimPollInterval = 3m、DefaultHeartbeatInterval = 15shttps://github.com/multica-ai/multica/blob/main/server/internal/daemon/config.go ↩ -
server/internal/daemon/daemon.gogithub.com ↗
pollLoop/runBatchPoller/taskClaimPollInterval;taskMulticaEnvironment給 CLI 的環境變數https://github.com/multica-ai/multica/blob/main/server/internal/daemon/daemon.go ↩ ↩2 -
server/internal/handler/daemon.gogithub.com ↗
buildClaimedTaskResponse組任務包:現查 agent、嵌入觸發評論、算 issue 變化,簽發mat_令牌https://github.com/multica-ai/multica/blob/main/server/internal/handler/daemon.go ↩ ↩2 ↩3 -
server/internal/daemon/execenv/runtime_config_sections.go收藏github.com寫進工作目錄的 CLAUDE.md / AGENTS.md 執行時簡報,各節標題都在這裡https://github.com/multica-ai/multica/blob/main/server/internal/daemon/execenv/runtime_config_sections.go ↩ ↩2
-
server/pkg/agent/claude.go收藏github.com
buildClaudeArgs的固定引數;buildClaudeInput把 prompt 包成 stream-json 的 user 訊息https://github.com/multica-ai/multica/blob/main/server/pkg/agent/claude.go ↩ -
server/internal/daemon/prompt.gogithub.com ↗
buildCommentPrompt;perTurnContextBlocks把每次都變的塊追加到末尾,避開 prompt cache 字首(MUL-5377)https://github.com/multica-ai/multica/blob/main/server/internal/daemon/prompt.go ↩ -
server/internal/auth/jwt.gogithub.com ↗
GenerateAgentTaskToken:mat_+ 40 位十六進位制,繫結 (agent_id, task_id)https://github.com/multica-ai/multica/blob/main/server/internal/auth/jwt.go ↩ -
server/internal/middleware/auth.gogithub.com ↗
mat_分支用令牌記錄覆蓋 X-User-ID / X-Agent-ID / X-Task-ID / X-Workspace-IDhttps://github.com/multica-ai/multica/blob/main/server/internal/middleware/auth.go ↩ -
server/internal/handler/actor_guards.gogithub.com ↗
RequireHumanActor只掛在計費這類“僅限人”的路由上,issue 和 chat 路由放行 agenthttps://github.com/multica-ai/multica/blob/main/server/internal/handler/actor_guards.go ↩ -
server/pkg/agent/claude_supplement.gogithub.com ↗借 Claude 的 SDK hooks 在回合中途插入
additionalContexthttps://github.com/multica-ai/multica/blob/main/server/pkg/agent/claude_supplement.go ↩ -
Hooks reference · Add context for Claudecode.claude.com ↗Multica 的插話實現在原始碼註釋裡引用的就是這一節https://code.claude.com/docs/en/hooks ↩
-
server/internal/daemon/task_supplement.gogithub.com ↗daemon 側插話迴圈:收到提示 → claim → 注入 → ack;
[ADDITIONAL GUIDANCE]文案https://github.com/multica-ai/multica/blob/main/server/internal/daemon/task_supplement.go ↩ -
How Claude Code works · Work with sessionscode.claude.com ↗會話記錄存在
~/.claude/projects/下,“Each Claude Code conversation is a session tied to your current directory”https://code.claude.com/docs/en/how-claude-code-works ↩ -
server/internal/daemon/execenv/execenv.gogithub.com ↗
PredictRootDir決定工作目錄路徑;Reuse在目錄還在時複用,否則退回新建https://github.com/multica-ai/multica/blob/main/server/internal/daemon/execenv/execenv.go ↩ -
server/internal/daemon/execenv/issue_state_instructions.gogithub.com ↗
BuildIssueStateHint:續接會話時告訴 agent issue 有沒有變(MUL-7344)https://github.com/multica-ai/multica/blob/main/server/internal/daemon/execenv/issue_state_instructions.go ↩ -
server/migrations/452_agent_task_pending_thread_unique.up.sqlgithub.com ↗排隊中任務的唯一鍵是 (issue, agent, 評論執行緒),同一執行緒的追問並進同一輪https://github.com/multica-ai/multica/blob/main/server/migrations/452_agent_task_pending_thread_unique.up.sql ↩
-
server/internal/daemon/repocache/cache.gogithub.com ↗每個任務的程式碼檢出在自己的分支
agent/{agent 名}/{任務}上https://github.com/multica-ai/multica/blob/main/server/internal/daemon/repocache/cache.go ↩