跳到內容

程序衛生與 slot

本節的第 1 版(docs/specs/2026-11-04-procs.md 計畫的第 2 層):worker 啟動的任何東西都隨 worker 一起結束,而且 worktree 不會在還有行程執行時被移除。大量指令的 slot、Linux cgroup 路徑與 CPU 卡住偵測屬於第 2 版,目前還沒建好;本節會隨它們一起擴充。

原因。 pi 會停止一次 bash 呼叫的行程群組(killProcessTree,對它啟動的 shell 的群組送 SIGKILL)。timeout、setsid、detached: true 的 spawn 與測試執行器會把它們的子行程放進自己的群組,所以結束或被殺掉的 worker 會把它們留下,改由 launchd 或 init 接管。11/03 有四個這樣的 node 測試行程,在父層已經移除的 worktree 裡跑了六小時,每個占 55 到 60% 的 CPU。

每個載入 src/hygiene.ts 擴充套件的 pi(主智慧體、worker、任何 session)都會在自己的 process.env 裡設定 STRAW_BOSS_OWNER=<owner id>。pi 的 bash 會把這個環境傳給它的 shell,所以每個後代行程都帶著它。id 對 pi-herdr-agents 的 worker 是 <subagent id>-<pid>,對其他任何 pi 是 pi-<pid>。讀取者只找那一個鍵(Linux 上是 /proc/<pid>/environ,macOS 上是 ps 的 environment 選項),並在記憶體中比對:任何行程的指令列或環境都絕不會被記錄、回傳或顯示。server 自己的 spawn 環境會丟掉這個鍵,而常駐的塔台 run(server 擁有的 pane)與 Windows 沒有標記。

設有 PI_SUBAGENT_ID 的 pi(pi-herdr-agents 的 worker)會在 session_start 時,透過控制 socket 對 sb server 送出 procs-hello {pid, started, subagentId, sessionFile, pane, terminal, cwd},每 30 秒打一次 beat,結束時送出 procs-bye(重新載入或切換 session 會保留行程,所以不算 bye)。server 在自己的行程表中找到該 pid 並檢查啟動時間;它從 cwd 找出 worktree 根目錄(往上最近的 .git 是帶有 gitdir: 的檔案;在主 checkout 裡的 worker 沒有 worktree,也沒有 cwd 規則)。紀錄存放在 <slot dir>/procs/owners.json(權限 600)。

細節

如果 server 拒絕 hello,worker 會說一次,而它啟動的東西不會被清理;沒有 server 在執行時,會在下一次 beat 再試一次 hello。server 會拒絕 pid 是 Herdr server、pane 的 shell、shell、server 或 broker,或底下有 Herdr 或 pane 的 shell 的 hello。session 的資料夾、session 檔與通知函式在它啟動時讀取,絕不在 await 之後讀,所以進行中的重新載入不會在過期的情境上出錯。

server 保存什麼。 每 10 秒(第一次 tick 花了超過一秒時改為 30 秒;這個成本會以 procs-tick-cost 記錄一次),server 依 ppid 對每個存活擁有者的後代做快照,格式為(pid、啟動時間、pgid)。之後才換親的行程仍被視為該擁有者的。身分在每個地方都是 (pid, start time),而且每個動作在執行之前都會再檢查一次。

擁有者何時結束。 在 procs-bye 時、它的 pid 已經消失或被重用時,或 Herdr 回報它的 pane 發生 pane.exited 或 pane.closed 且終端機 id 相符時(Herdr 會重用 pane id,所以終端機不同的 pane 的事件會被忽略)。在 server 停止期間結束的擁有者,會在 server 啟動時依 owners.json 清理。

flowchart TD
    e["擁有者結束"] --> k["殺除集合:還活著的後代、<br/>帶有它標記的行程、<br/>在它 worktree 裡的孤兒"]
    k --> t["對每個 group 再對每個<br/>pid 送 SIGTERM"]
    t --> s["10 秒後,對剩下的<br/>送 SIGKILL"]

已結束擁有者的擊殺集合是:快照中仍存活且身分相同的後代,加上帶有它標記的行程,加上工作資料夾在它的 worktree 或在最後一次被看成該根目錄的已移除 checkout 裡的孤兒(孤兒的父 pid 是 1,Linux 上則是 systemd --user),再加上這些行程在它們底下啟動的東西。要扣除受保護的集合:Herdr server、每個存活 pane 的 shell 與前景行程、server.json 中指名的 sb server、intercom broker、其他每個存活的已註冊擁有者及其樹,以及帶有其他存活擁有者標記的行程。

細節

在沒有已註冊擁有者的已連結 worktree 工作區中關閉的 pane(不是 pi 的智慧體)只套用 cwd 規則,而且不會套用到另有存活 session 在其中工作的 worktree。主智慧體的結束不清理任何東西:主智慧體會刻意啟動長期存活的 daemon。

擊殺順序。 對每個目標群組送 SIGTERM,再對每個 pid 送;10 秒後,對仍存活且身分相同的送 SIGKILL;最後一次查看會被記錄。log 每次清理有一行 procs-reaped {owner, reason, targets, termed, killed, left, protected},有東西仍然存活時還有 procs-left(此時它的紀錄會保留給 reap 使用)。在測試執行中,每個信號都經過 src/procs/killguard.ts,它會拒絕測試沒有啟動的 pid,或 cwd 在 SB_TEST_ROOT 之外的 pid,並讓行程失敗。

cli.mjs procs。

指令 效果
procs 列出 server 認識的擁有者:id、pid、啟動時間、存活或已結束(附原因)、worktree 根目錄、快照大小、pane。
procs reap --owner <id> 對已結束而留有東西存活的擁有者,再執行一次擊殺順序。對仍存活的擁有者會被拒絕。
procs reap --checkout <path> 殺掉在某個已連結 worktree(或 worker 已知在其中工作的已移除 checkout)裡工作的東西:孤兒與已結束擁有者的遺留。有存活擁有者在那裡工作時會被拒絕:請先結束該 worker 或關閉它的 pane。有行程仍然存活時結束碼為 1;stdout 上的 JSON 會說是哪些。

只要有任何行程的工作資料夾在目標 checkout 裡,擴充套件就會拒絕 worktree_remove,以及含有 git worktree remove 或 herdr worktree remove 的 bash 指令(也包括在 cd、git -C、&&、;、管線之後,或在 bash -c "…" 裡面)。目標是從路徑、分支名稱或 Herdr 工作區 id,透過 git worktree list --porcelain 與 Herdr 的工作區清單解析出來的。拒絕訊息會逐一列出每個持有者的 pid、程式名稱(comm;在 Linux 上是其執行檔的檔名,因為在那裡 comm 是主執行緒的名稱,由程式自己設定,而 node 24 把它設成 MainThread;絕不是指令列)、CPU 時間、存活時間與擁有者狀態(存活、已結束、未知或沒有標記),並給出 cli.mjs procs reap --checkout '<path>';清理之後,同一個呼叫就會通過。

細節

目標也可以用 git 唯一的結尾路徑元件解析,而沒有 --workspace 的 herdr worktree remove 取用 HERDR_WORKSPACE_ID 裡的工作區。無法執行的檢查(git 或 Herdr 沒有回應)不算通過。已完成的 worker 保留的閒置 pane shell(Herdr 說它是該 pane 的前景行程),在目標自己的工作區裡時不會擋住移除;其他任何資料夾在那裡的東西都會。/worktree remove 斜線指令不經過 tool_call:這是有記載的限制,而 reaper 外掛仍會拒絕它自己的移除。

reaper 外掛的提示。 herdr-reaper仍然只會拒絕移除有行程在其中工作的 checkout,絕不殺行程。它的訊息現在以 node <pi-straw-boss>/dist/server/cli.mjs procs reap --checkout '<path>' 結尾,因為外掛無從知道套件安裝在哪裡。更新執行中的外掛與套件發行是不同的步驟:再執行一次 herdr plugin install wayne930242/pi-straw-boss/herdr-reaper,或連結到 checkout。

停滯監看。 卡死的 pi 會通報給它所在工作區的塔台;server 絕不對它送信號,也不送任何按鍵。11/12 有一個主智慧體的 pi 把一個核心占滿了 40 小時,不收按鍵,也已經從 pi-intercom 的清單掉出去,直到有人請塔台手動救援才被發現。server 每分鐘取用那一次 tick 的行程表,檢查 Herdr 表示有 pi 在跑的每個 pane 裡的那個 pi(Herdr 在該 pane 列出的行程;session 自己回報的 pid,只有 pi-intercom 此刻在那個 pane 找到這個 session,或完全找不到它的位置時才算數;一次取樣最多查問八個 pane,輪流查問,因為 pane process-info 會啟動 Herdr 的 CLI,所以 server 啟動後要幾分鐘才會把所有 pane 納入;找不到 pi 的 pane 隔 2、4、8、16 次取樣再問,之後每 32 次問一次,並以 stall-unresolved 記錄一次)。三個訊號,各以相隔一分鐘的取樣計數:cpu,行程連續 cpuMinutes(5)次取樣平均占用一個核心的 cpuPercent(75)以上,其中沒有任何一次平靜(低於 cpuPercent 的一半),同時 session 檔沒有寫入:機器負載高時,滿載的 pi 偶爾會有一分鐘掉到門檻以下,所以這樣的一次取樣不會讓計數重新開始,平靜的取樣或一次寫入才會,短暫的尖峰和還在寫入的 session 也就不會通報(歷史很長的正常主智慧體在寫入 session 檔的同時,實測一分鐘內占用一個核心的 73% 和 83%,所以分辨工作和卡死靠的是寫入,不是調低門檻);missing,行程還活著,但 pi-intercom 連續 missingMinutes(3)次取樣沒有列出它,對象是 server 先前看過列在清單裡的 session,或 pane 帶有角色和 session 檔的 session(broker 會丟掉事件迴圈停住的用戶端;server 讀不到的清單不算數);silent,pane 顯示工作中,session 檔連續 silentMinutes(30)次取樣沒有寫入,而且其中沒有進行中的工具呼叫,所以長時間的指令、建置或子智慧體絕不算停滯。晚了超過三分鐘的取樣(電腦睡眠、server 被停掉)會讓所有計數重新開始。已知的 session 和未結的事件存在 <slot dir>/procs/stallwatch.json,所以 server 重啟後沒有回來的 session,過了前三次取樣就會通報,重啟前通報過的事件不會再通報一次。一個事件是塔台佇列裡的一則通知事件(子工作區找父工作區的塔台,server 沒有代管塔台的地方找主塔台;兩者都沒有時每分鐘重試,並以 stall-untold 記錄一次;towerRuns.enabled: false 時塔台是常駐的 session,server 不通報任何塔台,並以 stall-no-tower 記錄一次):它列出 pane、session 的 id 和名稱(兩者都加引號;id 只保留字母、數字和 ._:-)、pid 和啟動時間、CPU 分鐘數和訊號,並附上處理步驟:把 pane 讀兩次,畫面在動就不要管;畫面不動才送 Esc 再讀一次;只有在有交接檔或老闆同意時,才停掉行程並帶著交接內容開一個新的 session。只有在又一個訊號成立、訊號消失滿三次取樣(恢復了:不要停掉它;silent 的 session 有了回應、現在有進行中的工具呼叫,也算恢復),以及過了 remindMinutes(60)之後的提醒才會再通報,提醒的等待每次加倍,最長 8 小時;訊號消失期間不發提醒。pi 結束或它的 pane 關閉時,事件跟著結束,不發訊息。設定是 config.json 的 stallWatch,每次取樣讀取;分鐘數設 0 會關掉那一個訊號或提醒,enabled: false 關掉整個監看。log 裡有 stall-told、stall-untold、stall-no-tower、stall-unresolved、stall-ended 和 stall-error。看不到的:行程平靜但工具呼叫卡住(進行中的呼叫算是在工作);行程平靜、而且在監看第一次看到它的 pane 之前就已經卡死超過十分鐘的 session,因為角色 token 由 session 自己更新,到那時已經過期,也沒有別的東西能說明這個行程註冊過(session 檔裡的紀錄會跟著 resume 或 fork 帶到別的行程),這種情況出現在 stallwatch.json 還沒有這個 pi 的紀錄時 server 的第一次取樣(帶來這個監看的版本、監看剛打開、檔案遺失);以及 Windows 上的一切,這裡沒有它的行程表。會誤報的:在另一個 pi-intercom scope 裡的 session 永遠不在 server 的清單裡,pane 帶有角色時就會被當成 missing 通報。

限制。 Windows 沒有衛生機制(擴充套件不作用,並說一次)。worker 刻意啟動、要比它活得更久的行程(開發 server、broker),除非受到保護,否則會被清理;長期存活的行程要透過 MonitorCreate,反正 pi 會在 session 結束時停止它。在第 1 版之前載入 Straw Boss 的 pi 同樣沒有註冊,所以它的 worker 留下的東西,只會在 pane 關閉時被 cwd 規則找到。

在 macOS 上重放時顯示出三個限制。(1) 只有程式不是 Apple 平台二進位檔的行程,才讀得到標記:node 與 Homebrew 工具看得到,/bin/bash、/bin/sleep 與 /usr/bin/perl 則看不到,因為系統隱藏了它們的環境,此時移除守衛的拒絕訊息會對它們說「no owner marker」。這些由快照與 cwd 規則負責。pi 本身也看不到標記(它在啟動之後才設定該變數;只有它啟動的東西會繼承)。

(2) cwd 規則不會伸進另有其他 pane 也在工作的 worktree(老闆的 shell、主智慧體),因為那個 session 可能擁有在那裡執行的東西;以雙重 fork 逃過每一次快照(在幾毫秒內被 launchd 接管)、又無法靠標記讀取的行程,在這種情況下會留著,而 cli.mjs procs reap --checkout <path> 是明確停止它的方法。(3) server 在它的 tick(每 10 秒)才得知已連結 worktree 的工作區:沒有擁有者的 pane,在其工作區建立後 10 秒內關閉,不會被視為 worktree 的 pane。

清理在讀完行程表、孤兒的工作資料夾、標記與受保護集合之後(負載 25 到 35 時約 3 秒,負載更高時更久)立刻送出 SIGTERM,10 秒後送 SIGKILL。在重放中,每個逃脫的行程都在 worker 結束後 12 到 14 秒消失,有一次在機器負載 60 以上時是 22 秒。Herdr 關閉的 pane 會透過 pane 事件立刻結束一般行程(Herdr 在線路上拼成 pane_closed 與 pane_exited),而真正的 pi 則透過它自己關閉時送出的 procs-bye。