跳到內容

sb server

sb server 是獨立成一個行程的 Straw Boss 後端,讓塔台、連線持有者與頁面不必取決於哪個 pi session 剛好在執行它們(規格)。它擁有行程紀錄、控制 socket、log、與 intercom broker 的連線,以及 GUI server(頁面、它的 API、對話框 hub、遠端存取、收起與關閉路由)。連線持有者在主 server 中執行(見連線持有者);塔台在 server 中以塔台服務執行,預設開啟(在 config.json 設定 "towerRuns": { "enabled": false } 則改為常駐的 pi session)。

這一節包含:它是什麼、啟動與重啟、shell 的狀態、桌面模式;接著是塔台服務:服務、常駐 pane、主智慧體呼叫塔台時、[本機 run](#本機 run)、老闆對話、塔台記憶與隊友 run。

每個 slot 一個 server:一組(狀態資料夾、Herdr socket)。預設 Herdr 的 slot 是主 slot,檔案放在 <pi agent dir>/straw-boss/;在另一個 Herdr socket 上的 pi 使用 servers/<first 16 hex of sha256 of the socket>/ 底下的次要 slot,它的 server 不持有連線;當 towerRuns.enabled 把塔台放進 server 時,該雜湊的前 8 位十六進位(slot8)用來命名它的塔台身分(sb-tower-<slot8>-w1)。server 是 dist/server/cli.mjs 與 main.mjs,由 npm run build:server(esbuild,由 prepack 執行)從 src/server/ 建置。建置遇到對 @earendil-works/pi-tui、typebox 或 @earendil-works/pi-coding-agent 的值匯入會失敗,這些是 peer dependency,在 server 執行的位置並不在磁碟上;型別匯入會被抹除。

細節

唯一的執行期依賴 tsx 從這個套件解析(它的 ESM 進入點 dist/esm/api/index.mjs,從 tsx 的 exports 讀取:從 CommonJS 進入點執行的 tsImport 在 Node 22.22.1 與 22.22.2 上失敗,在 22.23.4 與 24 上可行),而且只透過它有範圍的 tsImport() 來載入 pi-intercom 的 TypeScript 用戶端(pi-intercom 0.18.1 或更新版本,啟動時檢查:這個版本要求它,較舊的 pi-intercom 會讓 server 無法啟動),所以 server 自己的程式碼從不被轉換;載入失敗時會記錄一行,指名 Node 版本。server 透過 headless kit 以 sb-server-<8 hex> 連到 broker,並在 Herdr 停止回應後 60 秒離開。

檔案放在 slot 的資料夾裡,全部權限 600,資料夾權限 700:server.json(紀錄:pid 與啟動時間、版本、模式 detached 或 serve、桌面開關是否開啟、控制 socket 或 Windows 具名管道與它的 hello 密鑰、server 啟動時使用的 PI_INTERCOM 範圍與傳輸、stopped、restarting {began, since, reason, by}、lastRestart、lastError)、server.lock(持有者的 pid 與啟動時間;已死的持有者在下一次查看時失去它,而超過 120 秒的鎖即使持有者還活著也會被取走)、start-error.json(上次啟動失敗的原因:loader、intercom 或 acl;在啟動嘗試開始時以及 hello 之後清除)、openings.json(頁面的開啟/基地所建立的工作區,依開啟 id)、server.log(5 MB,3 個檔案;只有事件與錯誤,絕不含訊息文字或密鑰),以及 server.sock(超過約 100 位元組的 socket 路徑放不進 sun_path,此時 socket 是使用者自己的私人資料夾中的 /tmp/sb-<uid>/<hash>.sock;server 絕不會 unlink 仍在接受連線的 socket)。

控制 socket 只回應以密鑰開頭的連線(以固定時間比對);其他連線會被關閉而不回應。在 Windows 上,狀態資料夾的 ACL 設為擁有者與 SYSTEM、受保護,並在 server 啟動前以一次 PowerShell 呼叫依 SID 檢查(失敗就是 start-error.json 裡的 acl);該呼叫印出只含 ASCII 的 JSON,所以任何字碼頁的帳號名稱都能讀回,它也會告知 server 自己的啟動時間,所以 server 不會再做其他 PowerShell 啟動。

server 啟動路徑上的 PowerShell 腳本(啟動、ACL、擁有者讀取、啟動時間、行程表)都不呼叫模組的 cmdlet(Get-CimInstance、Get-Acl、ConvertTo-Json、Test-Path、New-Object 之類):全新的 Windows PowerShell 5.1 中的第一個會建立它的模組快取,在冷的 runner 上要 41 到 48 秒,這是啟動等待 hello 的 60 秒中的大部分。這些腳本改用 .NET 呼叫、以 [wmisearcher] 取得啟動時間,以及一個小型的 JSON 寫入器(firewall.ts 的 LAN 設定不在啟動路徑上,仍會呼叫它的網路 cmdlet)。啟動時間直接從 CIM 日期時間字串計算,所以夏令時間回撥的那一小時也讀得正確。

不支援 Constrained Language Mode:ACL 腳本拒絕它,而啟動時間與行程表的讀取需要 FullLanguage。資料夾先被保護,不帶 /T(那會讓已經在裡面的檔案,也就是鎖與 log,留下空的 ACL),其下的一切再以一次 icacls <dir>\* /reset /T /C /Q 重設為從它繼承,所以成本不會隨檔案數量增加;讀回包含每個 slot 的 server.json。server 不會為了回應 hello 而等待自己的啟動時間(在其他地方,它在時間到達時才填進紀錄),鎖也不等它持有者的。

啟動 server 的人會在 spawn 之前把 restarting(原因 starting)寫進紀錄,並一直更新它,直到 server 回應,所以在緩慢的冷啟動期間 status 說的是 restarting,絕不是 gone;Windows 上啟動會等 hello 60 秒(其他地方 20 秒)。server.log 對載入器、ACL 呼叫與開始監聽各有一行 phase,以及帶有時間的 start-requested、start-answered 與 stop-done 行。送了幾 KB 卻沒有完整 hello 行的連線會被關閉。server.log 一次只由一個寫入者輪替(server.log.lock),因為 server、session 與 CLI 都會寫它。

在 macOS 與 Linux 上,獨立的 server 自己領導一個 session,所以殺掉啟動者的行程群組不會讓它結束。在 Windows 上,detached: true 不會離開啟動者的 job object,而且 pane 或 ssh session 可能把它的子行程放在 kill-on-close 的 job 裡,所以 server 是透過 WMI(Win32_Process.Create,在一次 PowerShell 啟動中)以 CREATE_BREAKAWAY_FROM_JOB 建立,隱藏、沒有 console,並帶著啟動者的完整環境作為 Win32_ProcessStartup.EnvironmentVariables(沒有任何東西寫到磁碟)。

細節

同一次 PowerShell 啟動也做 server 的 ACL 檢查(新 pid 的啟動時間、狀態資料夾的重設與讀回),並把結果留在只有 server 的 --acl-token 指名的檔案裡;server 在寫入它的密鑰之前自己檢查每個 SDDL,所以啟動與檢查是一次 PowerShell 啟動,不是兩次(serve 與單純 spawn 的備援仍然各做自己的);在建立第一個檔案之前先讀擁有者的情況下,Windows 上的冷啟動共做兩次(如果這個行程已經讀過這個資料夾的擁有者,就是一次,像重啟 server 的 pi session 那樣)。這次啟動是 log 的 phase breakaway,帶有 createMs 與 aclMs。

當 WMI 無法啟動它時,改用單純的 spawn,而且會說出來:up 警告 breakaway_failed,而 status、boss_server status 與 /boss server 顯示 server 會隨啟動它的 pane 或 ssh session 一起結束。沒有及時回應的 PowerShell,或在 WMI 可能已經建立 server 之後沒有答案的 PowerShell,就是啟動失敗,不會再啟動第二個;一旦 WMI 建立了行程,檢查標記的失敗只會讓等待中的 server 立刻以 acl 結束。在到達 create 之前就死掉的 PowerShell(拒絕某個方法的語言模式、AMSI 或 EDR 阻擋),靠腳本在 create 之前印出的一行區分,並明顯地退回單純的 spawn。

PowerShell 的失敗文字是從它的 CLIXML stderr 的錯誤紀錄中讀取,絕不以 XML 顯示;呼叫是用 -OutputFormat Text 啟動,並關閉進度串流。已知:在 WMI 已建立行程之後才丟出例外的 Win32_Process.Create 仍會退回備援(RPC 中斷),而第二個 server 隨後會因鎖而結束。

狀態資料夾會在每次啟動時,於建立其中第一個檔案、鎖、佔位紀錄或 log 之前被讀取(whoami、icacls /save,不用 PowerShell),權限不對時就保護並讀回(新的資料夾以暫時名稱建立,並在取得它的名稱之前先保護);是連結的資料夾會被拒絕。

不存在的資料夾會在旁邊以不可預測的名稱建立、在那裡保護並讀回,再改名到位,所以 server 自己建立的 straw-boss\ 絕不會在帶著父層寬鬆權限的情況下以它的名稱存在(其他模組,例如看板、收起、連線與交接的程式碼,在它們先到達時仍會以單純的 mkdirSync 建立那個資料夾;那些程式碼比這個更早,這是已知的)。

暫時資料夾帶著父層繼承的權限,以沒有人在找的名稱存在 35 到 130 毫秒;最終名稱從不如此。當掃描程式持有該資料夾時,改名會再試大約兩秒,而崩潰留下的暫時資料夾在一分鐘後移除。執行中、但因為權限不足而拒絕這個行程的 server(從同一使用者的過濾 token 看到的提升權限 server),會顯示為存活但無法連線,絕不會被覆蓋啟動或標為已停止;stop 會說要從提升權限的 shell 停止它。

stop、status 與第二次 up 除了那一次擁有者讀取之外不啟動任何 PowerShell:回應 hello 的 server 就是足夠的證明。Constrained Language Mode 會被如此告知。

控制連線證明 server 知道密鑰(對新的 nonce 的帶鍵答案),而因權限不足的拒絕,只對紀錄指名的行程有效。腳本只走訪狀態資料夾底下的內容一次,不跟隨連結,並一路讀取每個項目的連結類型與擁有者(讀回會重用該清單,並透過 .NET 讀取每個 SDDL,而不是每個檔案一次 Get-Acl:link\inbox 中的 3,000 個檔案曾讓檢查花 7 到 9 秒),並拒絕 junction 或符號連結(OneDrive 之類的雲端佔位檔當作一般檔案走訪);底下的每個資料夾,以及讀回所指名的檔案(五個頂層檔案、link\ 底下的每個檔案、每個次要 slot 的 server.json),都必須屬於使用者。

拒絕訊息會列出前五個問題,以及還有多少個。沒有任何原生呼叫會更改擁有者(成功的 /setowner 會接管另一個帳號建立、並授予使用者取得權利的資料夾;新啟動的暫時資料夾可能被換成這樣的資料夾):既有資料夾以 icacls /save 讀取,當它的權限已是擁有者與 SYSTEM 時就不動;權限不對的資料夾,只有在一次 PowerShell 把它的擁有者讀成 SID 並發現是使用者、BUILTIN\Administrators 或 SYSTEM 之後,才會更改(而且在呼叫之前會再檢查資料夾仍是同一個;在那一次行程啟動中的對調是有記載的殘餘風險)。

擁有者在任何密鑰存在之前,會在 PowerShell 中再次把關,資料夾自己的擁有者與每個項目的擁有者,會在更改其下任何東西之前讀取;同樣三個擁有者在所有地方都被接受。在狀態資料夾中建立第一個檔案之前(既有的資料夾,或剛建立的新資料夾,在建立之後立刻),一次 PowerShell 會讀取它的擁有者(以資料夾的檔案身分記住,所以 pi session 只讀一次):另一個帳號的資料夾會被拒絕,其中不建立也不寫入任何東西,原因送到 stderr 與 up 的 JSON,而不是資料夾裡。

對持有紀錄的資料夾的查看(session 的查看、status、up、stop、restart),在它詢問 pid 是否存活、對紀錄的管道送出 hello,或轉述紀錄中的任何內容之前,都會通過同一道擁有者把關:另一個帳號的資料夾會以同樣的 acl 錯誤被拒絕,而且不送出也不寫入任何東西。這次讀取是備忘錄的,所以 pi session 在它第一次查看時花一次 PowerShell(拒絕與失敗的讀取也會被記住,所以每 2 秒一次的查看不會啟動任何一次),而 shell 指令是新的行程,花一次;沒有紀錄的資料夾不會被讀取。

冷的 up 與已停止 server 的 stop 照舊花一次。這次讀取絕不卡住 pi session 的事件迴圈:它是非同步的子行程(在 VM 上 PowerShell 啟動需要 2 到 5 秒),在它執行期間到來的請求(查看、連線請求、GUI)會等待這一次讀取並取得它的結論,而讀取期間被替換的資料夾會重新讀取,而不是被記住。

沒有 ACL 的地方(macOS 與 Linux),同一道把關是在讀取紀錄之前,對狀態資料夾、slot 的資料夾與紀錄做 stat:另一個 uid 擁有的會被以同樣的 acl 錯誤與同樣的措辭(「belongs to another account」,在 shell 中結束碼 3)拒絕,而且不啟動任何行程。走訪與重設的呼叫不是原子的:在兩者之間植入的項目是已知、有界的殘餘風險。因資料夾屬於另一個帳號而被拒絕的啟動不寫入任何東西:原因在錯誤與 up 的 JSON 裡(不屬於我們的資料夾不會有 start-error.json)。紀錄不再在沒有擁有者讀取的情況下被信任(R13-F003,在 server 承載 session 內容之前結案)。

icacls /T /L 會進入 junction,所以走訪中對 junction 的拒絕,是保護它們目標的東西;在走訪與呼叫之間植入的 junction 是已知、有界的殘餘風險。能寫入狀態資料夾的父層的帳號,永遠能贏得暫時資料夾的對調而讓第一次 up 失敗(沒有東西被接管)。無法保護資料夾的 server 會寫入 start-error.json,所以 up 以 acl 結束碼 3 結束。無法開啟的 server.lock 會以 icacls /reset 修復一次。已消失 server 的已停止紀錄,不會把 pi-intercom 範圍釘住到下一次啟動。

資料夾的確切擁有者(O:)在 PowerShell 中檢查:在 server 建立之前(另一個帳號擁有的資料夾會讓 up 以 acl 結束,結束碼 3)以及再次在任何密鑰寫入之前;其下的資料夾會被變成使用者的,也會被檢查。失敗的 icacls 會以對應其步驟與結束碼的固定訊息說明該怎麼做(對提升權限執行建立的資料夾:以提升權限執行一次 pi,或刪除它),而程式自己的文字只作為可列印 ASCII 的細節。

系統的程式(powershell、whoami、icacls、cmd、clip)以 %SystemRoot%\System32 的絕對路徑呼叫,而套件啟動的其他每個程式(herdr、git、tailscale、ps、sh、開啟器與剪貼簿指令)都以 PATH 搜尋找到,該搜尋會跳過空的、. 與相對的項目,絕不看目前的資料夾,再以找到的路徑 spawn,所以儲存庫裡被植入的 git.exe 絕不會被執行。

擁有者把關在 Windows 上要花 PowerShell 啟動

Section titled “擁有者把關在 Windows 上要花 PowerShell 啟動”

在請求信任紀錄、或寫入既有狀態資料夾之前,一次 PowerShell 會讀取狀態資料夾、slot 的資料夾與紀錄的擁有者(外來的擁有者會以結束碼 3 被拒絕,且不送出也不寫入任何東西;逾時的讀取是等待,結束碼 1,會再讀取一次)。pi session 對每個資料夾身分讀一次(它第一次查看;之後的查看不花任何成本);shell 指令是新的行程,自己讀取。

細節

每次讀取之後都跟著一次 icacls /save,檢查資料夾是封閉的(只有擁有者與 SYSTEM 的 DACL):開放的資料夾會當場被保護(icacls /inheritance:r /grant:r,再一次 /save;它仍顯示的任何其他明確授權,例如提升權限 token 建立的資料夾的 BUILTIN\Administrators ACE,會以 icacls /remove:g *<SID> 依 SID 移除,然後資料夾再讀一次,檢查本身不變),然後被記住。

如果那次保護失敗,會說一次(對 cli.mjs 是一則行程警告;在 pi session 中由查看說一次,因為 pi 會讓行程警告沉默),而資料夾最多每 60 秒再讀一次(紀錄在其中改變身分時立刻讀),不是每次查看都讀。依指令計算,PowerShell 啟動次數與(括號中的)icacls /save 呼叫次數(由假的執行器固定的計數;Windows 時間沒有量測):session 對有紀錄的資料夾的第一次查看 1 [1],之後的查看 0;status 1 [1](外加一次 whoami);存活 server 的 stop 1 [1](外加一次 whoami);第二次 up 1 [1];存活 server 的 restart 2;冷的 up 2 [2:把關的,然後是 secure 的 DACL 讀取;新資料夾在把關之前就被保護並讀回,所以它的把關多一次];開放的資料夾在它的第一次讀取上多一次保護呼叫與一次 /save。

在把關之前,status、stop 與第二次 up 不花任何成本,所以在 Windows 上輪詢 status 的 shell,每次輪詢要付出一次啟動(2 到 5 秒);這是接受的。沒有 ACL 的地方,把關是對擁有者的 stat(先是連結本身的擁有者,再是它目標的;root 算作使用者的),不花任何行程。由 sudo 執行建立、使用者無法使用的資料夾會被如此告知(用 sudo chown -R 它,或以 sudo 移除它)。

在 Herdr 中有畫面的 pi session 會在 session 啟動時以及每 2 秒查看一次:當它的 slot 沒有存活的 server 在執行時,它會從自己的套件,在 server.lock 之下,啟動一個獨立的(detached: true、stdio: "ignore"、windowsHide: true),並持有鎖直到 server 的 hello。它建構的環境會丟掉 session 的身分(PI_INTERCOM_SESSION_ID、PI_INTERCOM_STABLE_ID、PI_SUBAGENT_*、STRAW_BOSS_ROLE),並保留 PI_INTERCOM_SCOPE_ID、PI_INTERCOM_TRANSPORT 與 PI_INTERCOM_TCP,這些由紀錄保存;在另一個範圍或傳輸上的 session 不使用 server,並說一次。失敗的啟動會連同原因說一次,並在一分鐘後再試。子智慧體與沒有畫面的 session 不啟動任何東西。

細節

版本。 session 絕不會從比正在執行的更舊的套件啟動 server;它會說一次。較新套件上的 session 會請存活的 server 升級,這會從較新的套件重啟它(log 中的 restart reason=upgrade)。回到較舊的套件只能透過 /boss server restart --downgrade(或 cli.mjs restart --downgrade),並在確認說明它較舊之後。

sequenceDiagram
    participant R as 請求者
    participant O as 舊 server
    participant N as 新 server
    R->>O: 帶著原因要求重啟(持有 server.lock)
    O->>O: 在 server.json 寫入 restarting 後離開
    R->>R: 每 2 秒更新 since
    N-->>R: hello
    Note over R,N: 超過 10 秒沒更新的紀錄視為 gone

每次重啟都會在連接埠空閒時保留 server 的連接埠,並保留 GUI 的 token(見瀏覽器檢視)。請求者取得 server.lock 並請 server 以某個原因重啟;舊 server 把 restarting {began, since, reason, by} 寫進 server.json 並結束(serve server 的結束碼是 75),然後請求者每 2 秒更新一次 since,直到新 server 的 hello(啟動中的 server 也會更新它),而且只在紀錄仍是舊 server 的時候才寫,所以絕不會覆蓋較新 server 的紀錄。10 秒沒有被更新的 restarting 紀錄算作已消失。

細節

請求者持有鎖直到新 server 回應,但 serve server 例外:它的 supervisor 會啟動下一個 serve,而它需要鎖,所以請求者在那段等待中放開它,只有在沒有 supervisor 啟動時才再取得,自己啟動獨立的 server;如果 serve 在啟動期間持有鎖超過啟動預算(20 秒,Windows 上 60 秒),請求者會在該 server 回應之後回報重啟已完成,而不是 busy。已停止的 server 不論每分鐘一次的限制如何都會被啟動;當另一個請求者的 restarting 還新鮮時,已停止的 serve server 會以同樣的方式被等待,而不是被獨立的 server 取代。

當請求者的 broker 範圍或傳輸與紀錄的不同時,重啟會被拒絕(serve 接管獨立 server 同理),而且 serve 絕不取代較新套件的 server(所以從較舊套件啟動的 serve,在它的 supervisor 每次在 KeepAlive 或 Restart=always 之下再次啟動它時,每次都以 1 結束,直到套件升級或較新的 server 消失為止)。stop 同樣會從另一個範圍或傳輸被拒絕。

只有某個對象請求的重啟(智慧體、/boss server restart、cli.mjs restart)才計入智慧體受限的每分鐘一次限制;升級或 serve 接管不計,而該限制在鎖之下檢查,所以同時兩個請求只得到一次重啟。session 以磁碟上緊鄰它將啟動的 cli.mjs 的套件版本,而不是它載入的程式碼,來判斷升級,並把新 server 不比舊 server 新的升級視為失敗(說一次),而不是每次查看都再問一次;只有在 server 的版本或套件的版本改變時才會再問,絕不隨時間流逝而問,所以過期的 dist/ 不會讓 server 每分鐘重啟一次。

cli.mjs(node dist/server/cli.mjs …,或在 checkout 裡用 npm run server -- …,它透過 tsx 執行 src/server/cli.ts;在沒有 dist/ 的 checkout 裡,session 會以 node src/server/cli.ts 啟動 server,這需要 Node 22.18 或更新版本):

指令 效果
up [--dir FOLDER --label NAME] [--timeout SECONDS] [--exact] 確保有 server 在執行;印出一行 JSON(url(GUI 的,server 回應 200 之後取自 server 自己的答案)、pid、started、version、package、mode、desk、server = found、started、restarted 或 upgraded、log、steps(herdr、version、server、gui;帶 --dir 時還有 integration、base、tower)、warnings)。up 是老闆自己的啟動,所以它會啟動被刻意停掉的 GUI;無法恢復的遠端存取是 remote_off 警告,想要卻無法回來的 Wi-Fi 存取是 lan_off。--timeout 限制這次呼叫的等待,也就是鎖的等待與啟動等待加總,以及桌面步驟的每次 Herdr 呼叫(Windows 上預設 90 秒,其他地方 30 秒),從 up 在執行中的 node 行程裡開始它的步驟那一刻起算:在那之前啟動 node 與載入 CLI,以及在答案寫出之後離開,都在它之外(安靜的機器上約 0.5 秒,資源吃緊的機器上還要多幾秒:在 4 vCPU 上跑 16 個忙碌行程時,--timeout 5 在牆鐘上 11.7 秒才結束,其中 5.3 秒是呼叫自己的),所以呼叫者對 up 自己的上限應該是 timeout 加一點餘裕;等待逾時時,stderr 上的 JSON 會在 step 旁邊以 wait 指名它:lock、start、hello 或 exit。執行中的較新 server 是 warnings 裡的 server_newer,在 --exact 下則是在步驟 version 以結束碼 1 結束。--dir 與 --label 會加入桌面步驟,且需要 STRAW_BOSS_DESK=1(桌面模式):integration,然後在 gui 之前是 base 與 tower,輸出中有 workspace、pane、tower、created、integration。
serve 在前景執行 server,給 supervisor(systemd、launchd)用。它會在同一個連接埠上(同一個 token 之下)接管獨立的 server;另一個存活的 serve 會讓它以 4 結束。
stop 刻意停止 server(server.json 裡的 stopped);pi session 不會再次啟動它(它們的查看會放過已停止的 server),只有老闆的指令(up、serve、/boss server)才會清除它。對 serve server 會被拒絕,結束碼 5:請停止它的 supervisor。
status 印出一行 JSON:state(live、restarting、stopped、gui-stopped 或 gone,shell 的狀態)、紀錄的事實(絕不含密鑰)、GUI 的 url、存活 server 的運行時間與遠端存取(remote)、Wi-Fi 存取(lan:是否想要且已開啟、連接埠、不含 token 的入口 url、提供服務的位址與它們的介面、待處理配對請求的數量,以及一個 note),以及它消失時的上次啟動錯誤。server 停止時也能運作。
restart [--reason TEXT] [--by TEXT] [--downgrade] 在同一個連接埠、同一個 token 之下重啟(--downgrade 到 D27 之前的套件會產生新的 token,並解除每個裝置的配對)。原因是單行 1 到 200 個字元;--by 在 log 中指名提出者(GUI 的 Restart 按鈕使用它)。
logs [--lines N] [--follow] 印出 log 的尾端(預設 200 行)。

結束碼:0 完成;1 某個步驟失敗(stderr 上一行 JSON);2 用法錯誤;3 server 無法執行(start-error.json 說明原因);4 serve 發現有存活的 serve;5 stop 發現是 serve server。

在 cli.mjs stop 之後,session 不會重啟 server:它是被刻意停止的,只有 up、serve 或 /boss server 會再次啟動它。/boss server 顯示狀態,並在 server 停止時啟動它,即使它是被刻意停止的也一樣。/boss server stop 在一次確認之後刻意停止它,該確認會說明每個工作區隨後都失去它存活的塔台,而主智慧體會在 15 秒後被降級。/boss server restart [--downgrade] [reason] 在一次確認之後重啟它。/boss server restart 指令也會清除刻意停止(它是老闆自己的啟動)。

細節

在有畫面的已加入 session 中註冊的 boss_server 工具,給智慧體 status(server 停止時也能運作)、logs(lines,最多 200),以及帶有單行 1 到 200 個字元原因的 restart,該原因會連同提出的 session 寫進 log。透過工具的重啟,在老闆刻意停止 server 期間會被拒絕,整台機器每分鐘只接受一次,而且會啟動已停止的 server。遠端的 turn 在任何層級都無法呼叫 boss_server,包括 dispatch。這項封鎖只在工具上:dispatch 層級的對端仍有 bash,所以它可以執行 cli.mjs stop 或 restart,或殺掉 server 的 pid。在 server 檢查 run token(塔台步驟)之前,這項封鎖在該層級不是界線。

stateDiagram-v2
    state "GUI 已停止" as gs
    state "Server 已停止" as ss
    [*] --> live
    live --> restarting: 重啟
    restarting --> live: hello
    restarting --> gone: 10 秒
    live --> gs: 停 GUI
    live --> ss: stop
    ss --> live: up
    gone --> live: up

顯示 GUI 的程式(straw-boss-desk shell)從 server.json(與 gui.json)得知堆疊的狀態,或從 cli.mjs status 讀取。restarting.since 會由正在啟動的 server 每 2 秒更新一次,所以只有在這個更新持續到來時,重啟才算進行中,不論啟動花多久;沒有固定的總上限。

細節
狀態 如何從檔案判斷 cli.mjs status shell 做什麼
live server.json 沒有 stopped 也沒有 restarting;它的 pid 存活且在 started 啟動;對 URL 的 GET 回應 200 {"state":"live", …} 顯示頁面。
moved live,但 gui.port 或 gui.token 與 shell 載入的 URL 不同(被占用的連接埠、遺失的 store 或 /boss gui rotate 會改變 token;單純重啟不會) live,帶新的 url 在新 URL 回應 200 之後載入它;舊的回應 403。
restarting server.json 有 restarting,其 since 在最近 10 秒內 restarting,帶 reason 保留頁面直到重啟結束;它會重新連到同一個 URL(token 保留),而新的 URL 只有在 token 改變時才以 moved 出現(D27)。
gui stopped live,且 gui.stopped(gui.json 的 stopped)已設定 gui-stopped 說明 GUI 已停止;up 會再次啟動它。
server stopped server.json 有 stopped stopped 在下一次啟動時執行 up(它會清除 stopped)。
gone 以上皆非:PID 已死或在另一個時間啟動,或 restarting.since 超過 10 秒,或 URL 已 10 秒沒有回應而沒有東西更新 since gone,有時帶 lastError 與 start-error.json 的原因 執行 up。

在順利的路徑上,shell 完全不讀任何檔案:cli.mjs up 印出它需要的一切(url、pid、server、steps、warnings),而 cli.mjs status 把上述狀態印成一行 JSON。檔案是給兩次啟動之間觀察用的,而 cli.mjs logs 用於除錯。

遷移,分三個版本,由 shell 一次釘選一個:S1(server 隨附)在不變的契約下寫 gui.json,並把 server.json 作為它有記載的後繼者,附帶 up、status 與 logs,shell 不必做任何改變;S2(晚一個 minor)讓 server.json 成為有記載的契約,shell 透過 up 的輸出與 status 使用它,並保留 gui.json 作為備援;S3(在 shell 於它的釘選中確認 S2 之後)移除備援,而 gui.json 為舊的 shell 留在磁碟上,在這裡標為已棄用,沒有發行說明就絕不移除。不新增 schema 版本:shell 持續釘選 pi-straw-boss 的版本。

自己顯示頁面的 shell(straw-boss-desk)以 STRAW_BOSS_DESK=1 啟動 herdr server,並執行一個指令,建立 shell 的主基地並找到它的塔台(桌面規格,S3 到 S6,如 sb server 規格的 Desk mode: what changes 所列):

終端機視窗
node <pi-straw-boss>/dist/server/cli.mjs up --dir <folder> --label <name> [--timeout <seconds>] [--exact]
細節

GUI 不再等待塔台 pi:sb server 從它自己啟動起就提供頁面,所以 up 在 server、基地與塔台的常駐 pane 就位時立刻返回。shell 可以指望的事:

  • 開關是啟動 herdr server 的環境中的 STRAW_BOSS_DESK=1,所以每個 pane 都繼承它,up 的環境中也要有。當 up 自己的環境缺少它時,up --dir 以結束碼 2 拒絕,這能抓到忘了設的 shell;--dir 與 --label 要一起用(否則結束碼 2)。沒有開關而執行的 server 會帶著開關重啟(server: "restarted");帶著開關執行、卻被沒有開關的 up 找到的 server,則不動它,並給出 desk_on 警告。對沒有設定它的機器沒有任何改變。
  • 狀態資料夾是 $PI_CODING_AGENT_DIR/straw-boss/,沒有其他變數會移動它(某個 pane 漏掉的第二個變數,會把一台機器分裂成兩個 GUI)。shell 可以在那裡讀取的檔案:gui.json 與 server.json(契約、shell 的狀態),以及 desk.json,{workspace, dir, label},也就是主基地(以改名寫入,權限 600)。desk.lock 與其他一切屬於 pi-straw-boss。
  • 步驟,每一步都在失敗時被指名(stderr 上一行 JSON {step, reason, steps, warnings, log},結束碼 1;up 的其他結束碼照舊):
    1. herdr:Herdr 的 socket 有回應。

    2. integration:讀取 herdr integration status,並執行 herdr integration install pi,除非它的 pi: 行說 current(過時的會再次安裝,所以 Herdr 更新會把它帶上);之後該行必須說 current。當 <agent dir>/extensions/ 不存在時(全新的智慧體資料夾),Herdr 的安裝會失敗(結束碼 1,「extension directory not found」),所以 up 先建立該資料夾:也就是 Herdr 自己的 pi: 狀態行所指名的檔案的資料夾,不論環境指向哪裡。沒有這個整合,Herdr 永遠不會指名 pane 的 pi session,pane 就留在網之外,每個 session 也會在它啟動 20 秒後為此警告。輸出會說 current 或 installed(integration_installed 警告)。一般模式從不安裝任何東西。

    3. version、server:與沒有 --dir 時相同(啟動、尋找、為了開關而重啟,或升級)。

    4. base:在 desk.lock 之下(持有它時,第二個 up --dir 會在這裡以「busy」失敗;已死行程的鎖、在與鎖所說的不同時間啟動的行程的鎖,或超過十分鐘的鎖,會一次一個被接手,如 server.lock 那樣;在 Windows 上,行程的啟動時間來自要花幾秒的背景讀取,比簡短的 up 持續的時間更晚,所以在那裡,pid 現在由存活行程持有的鎖無法與它的持有者區分,會一直留到十分鐘過去,而不是讓每個忙碌的 up --dir 都等那次讀取),--dir 必須是既有的資料夾(先建立它,並放在家目錄下可見的資料夾,例如 ~/Straw Boss:隱藏的資料夾無法挑選,所以頁面在該基地中的資料夾步驟會從家目錄開始)。

      只要 Herdr 列出 desk.json 所指名的工作區,它就是基地,不論這次的 --dir 與 --label 說什麼(Herdr 還原會保留工作區 id),只要它仍符合紀錄:它的標籤是記錄的那個(up 建立它時用的標籤),且不只是它某個 pane 的資料夾名稱(Herdr 會給沒有標籤的工作區那個名稱並跟隨 cd,所以這樣的標籤什麼也證明不了),或它有一個 pane 在記錄的資料夾中啟動(以真實路徑比較;在 Windows 上不分大小寫,這在真實機器上沒有驗證)。被手動改名、而且所有 pane 都已離開該資料夾的基地不再符合:會建立新的基地,並給出 desk_base_replaced。

      塔台分頁什麼也證明不了,因為每個工作區的塔台分頁都帶著同樣的標籤;所以塔台已結束、而其他分頁在別處工作的基地仍是基地,會得到新的塔台,而有自己塔台的使用者工作區不會被當成它。Herdr 從它還原的最高 id 開始為新工作區編號,所以已結束的基地的 id 可能以使用者的工作區回來;那個工作區不動,會建立新的基地,並由 desk_base_replaced 警告說明。保留的基地在記錄的資料夾中開啟它的塔台;--dir 只在建立基地時才檢查。

      否則 up 會在 --dir 中以 --label 建立一個工作區,把它的第一個分頁標為塔台分頁,並把它的 root pane 標為 sb_role=tower,所以基地沒有多餘的 shell 分頁,並記錄它。它的 shell 得不到任何 Straw Boss 環境。無法標記的基地會再次被關閉。不是紀錄的 desk.json 會在 desk_json_unreadable 警告中說明,並重新建立基地。

    5. tower:對基地的工作區執行 ensure-tower(塔台服務):server 把那個 pane 當作常駐 pane,註冊塔台的身分,並寫入 tower token;不啟動任何 pi,也不等待被還原的那一個(還原在常駐 pane 中接續的 pi 會被結束)。當 up 在這次執行中建立了基地,它會把 root pane 與時間告訴 ensure-tower;只有當該 pane 是工作區唯一的 pane、而且這項聲明不到 30 秒時,server 才相信它。

      同一項聲明接著會結束仍為該工作區 id 註冊著的塔台服務(Herdr 在 session 未存檔的情況下被殺掉,會把新基地編成與遺失的那個相同的號碼),不論該服務在此期間在 pane 裡做了什麼(它可能以任何塔台都有的圖示證明了新基地、在那裡輸入了提示、並顯示了它的替身),所以帶著基地的標籤與資料夾的新塔台立刻取得它的 token,而不是等下一次 4 分鐘的更新;遺失的服務的查看在那一刻停止。

      該 pane 算作全新的 shell,所以在 Windows 上(Herdr 無法分辨閒置的 shell 與忙碌的 shell),tower> 提示只有在它同時被標為塔台、沒有智慧體,而且此後 server 沒有在該終端機裡輸入過任何東西時(claimGate)才會被輸入:不論由哪個服務代表它,提示對一個 pane 只輸入一次。任何其他 pane 在那裡絕不會被輸入。被使用者關閉的塔台已丟掉它的紀錄,所以下一次 up 會在同一個基地開一個新的;被使用者結束的基地會被重新建立。它需要塔台服務(towerRuns.enabled,預設開啟):關閉時,這一步會以 server 自己的話失敗。

    6. gui:URL 回應 200(以 gui.json 為契約)。

  • 輸出是 up 的 JSON,加上 workspace(基地)、pane(塔台的常駐 pane)、tower(它的 intercom id)、created(這次執行是否建立了基地或塔台)與 integration。第二次執行印出同一個基地與 created: false,而且不開任何分頁;在 herdr server stop 與新 server 之後,up 依它的工作區 id 找到基地與還原的塔台,不開任何東西。
  • 頁面在桌面堆疊中隱藏的東西:GUI 的遠端存取(設定頁的 Phone & devices)與收起(每個基地的 Stash、HUD 的 Stashed),只是呈現上的隱藏(路由仍在);/boss gui 印出 URL,不開瀏覽器;當 shell 的 preload 暴露 strawBossDesk.pickFolder 時,資料夾選擇器會提供 shell 的原生對話框(在頁面與 session 中)。
  • 停止是 shell 的事:cli.mjs stop,然後 herdr server stop。它的下一次啟動會再次執行 up。up 啟動的 server 是獨立的,並記錄在 server.json(pid、started),shell 用 cli.mjs stop 停止的就是它。
  • Straw Link 只有一個狀態資料夾,所以 link/ 與 gui.json 並排;連線持有者是主 server。主塔台在 shell 的資料夾中工作,所以它不持有任何團隊的訊息(遞送會送到資料夾在團隊專案內的塔台);訊息會在收件匣裡等待,直到專案中有基地開啟。

一個開關,~/.pi/agent/straw-boss/config.json 中的 towerRuns:{ "towerRuns": { "enabled": false } }(max,同時最多的 pi run 數,預設 4,是給[本機 run](#本機 run)用的)。它預設是開啟的:沒有 config.json,或 towerRuns 沒有 enabled: false,都表示開啟。讀不出來或解析不了的 config.json 也表示開啟,但 server 會說一次:在 log 與 status 的 lastError 中有一行 code=config 的 error,因為在此期間檔案自己的 enabled: false 並未被遵守。在 enabled: false 時,塔台維持為常駐,本節中的一切都不會發生。

細節

server 在啟動時與每次 ensure-tower 時讀取它,所以改變它不需要重啟。關閉它對 server 在一次更新(4 分鐘)之內生效:它釋放它的塔台身分,清除它的 tower token,並清掉每個 service.json、佇列與帳本;在那之前,發現存活身分 token 的 session 仍能找到它,而它送出的東西會被排進佇列、永遠不被讀取。以關閉的開關啟動的 server 也會清掉遺留物。服務代替塔台;喚醒 pi 處理它收到的事件的東西,是下文的[本機 run](#本機 run)(規格,The tower service)。

flowchart TD
    c["升格、/boss tower、<br/>main 的 call_tower"] --> e["透過 control socket<br/>ensure-tower"]
    e --> t["重用已標記的分頁,<br/>或建立塔台分頁"]
    t --> i["向 pi-intercom<br/>登記塔台身分"]
    i --> w["寫入 tower token<br/>和 service.json"]

工作區的塔台不再是一個 pi:開啟它是透過 control socket 對伺服器要求 ensure-tower。伺服器重用已標記的塔台分頁或建立一個,向 pi-intercom 登記塔台身分,並寫入該工作區的 tower token 和 service.json。子工作區得到的是它父工作區的塔台。

細節

openTower(升任、會派工的成員、兩位成員規則、/boss tower、主智慧體的 call_tower)會透過控制 socket 對 server 詢問 ensure-tower {workspace},而不是啟動 pi;存活的塔台 token 就是塔台,不會被詢問。server 會重用已標記的塔台分頁或空白的分頁(無線電塔圖示、一個 shell pane、沒有智慧體),否則建立沒有 Straw Boss 環境的分頁,把它移到最前面,把它的 pane 標為 sb_role=tower(常駐 pane,停在它的 shell 提示符號),向 pi-intercom 註冊塔台身分(id sb-tower-<workspace id in lowercase>,在次要 slot 上是 sb-tower-<slot8>-<id>;名稱 tower · <label>;cwd <slot>/tower/<workspace id>,沒有 pi 在其中執行的資料夾;herdrPaneId = 常駐 pane),並寫入工作區 token tower 與 tower/<workspace id>/service.json。

子工作區由它父層的塔台回應。/boss tower 是老闆自己的指令,所以它也會啟動被刻意停止的 server;對已停止 server 的自動開啟會以「the sb server was stopped on purpose; /boss server starts it again」失敗。

有人值守的裝置為隊友喚醒一個

Section titled “有人值守的裝置為隊友喚醒一個”

當隊友的訊息在等待一個沒有塔台的專案時,連線持有者會請 server 喚醒這台電腦已有的某個工作區的塔台:在專案內有 pane 的工作區,或寄件者指名的那一個。寄件者的信任必須是 reply 或更高(沒有 link.trust 項目的成員是 reply,所以團隊的每個成員都符合;離開名冊的寄件者會先被剔除)。

細節

server 絕不為它建立工作區或基地,不移動任何焦點,也不在其他 pane 輸入;它唯一可能建立的是該既有工作區中的塔台分頁,當工作區沒有塔台分頁時(老闆 2026-11-07 的裁定)。沒有這樣的工作區時,它什麼也不建立,並說明。寄件者寫的名稱不是這台電腦的工作區(或是主塔台或子工作區)時,不花任何喚醒:持有者自己回答它,towerRuns 被關閉時也是這樣。

每個專案與被詢問的工作區(依它的 id)各執行一次喚醒,一個寄件者最多同時有 3 個,而下一次嘗試在上一次結束之後等一分鐘,再兩分鐘、四分鐘,依此類推,最長 30 分鐘(失敗的是 10 分鐘);沒有找到工作區的喚醒,在專案內有 pane 的工作區出現時會立刻再試。連線 log 在狀態改變時(不是每次嘗試)有 tower-wake、tower-woke、tower-wake-none 與 tower-wake-failed(沒有訊息文字)。當喚醒在排隊或執行中,以及第一次成功的喚醒之後兩分鐘內,Link 按鈕與 pi 不會對該訊息顯示通知。

它們只在喚醒失敗時(原因以平白的話說明)、沒有工作區存在或塔台 run 被關閉時(說明該怎麼做),或成功的喚醒在兩分鐘內沒有讓訊息被接收時才顯示一則。此時訊息就像對 server 執行的任何塔台一樣,得到它的隊友 run。

無人值守的裝置為控制者開啟一個

Section titled “無人值守的裝置為控制者開啟一個”

當 dispatch 層級(政策檔的 trust)的寄件者的訊息在等待一個沒有塔台的專案時,連線持有者會請 server 開啟一個:Herdr 中在專案內有 pane 的工作區,否則在專案的真實路徑建立新工作區,然後對它執行 ensure-tower。只有無人值守的裝置這麼做,絕不為低於 dispatch 的寄件者,絕不在專案之外,也絕不用名稱由寄件者選擇的工作區。每個專案同時只有一次開啟,下一次在上一次結束後等 60 秒(連線 log 中的 tower-open、tower-opened、tower-open-failed)。此時訊息就像對 server 執行的任何塔台一樣,得到它的隊友 run。dispatch 層級的隊友 run 以 boss_open_main(run-tool 請求)開啟它的主智慧體,run token 的控制 socket open-main 拒絕不會影響它。

每 4 分鐘更新 tower token 與 pane 的 sb_role 及排序鍵(它們的 TTL 是 10)。主塔台監看:在一條事件連線上,主工作區的變動會作為摘要行記在每個塔台工作區裡。開啟器(boss_open_main)與 close-main 請求在 server 中為該身分執行:該身分註冊 straw-boss/control 頻道,所以主智慧體檢查寄件者是否為工作區 tower token 持有者的做法成立,而開啟器的帳本(寄件者、寄件者與請求的雜湊、結果、分頁、主智慧體)保存在 tower/<workspace id>/opened.json 24 小時,所以已經嘗試過的請求在重啟之後不會再嘗試。控制指令:ensure-tower、close-tower、open-main、close-main。

較舊版本的常駐塔台保有它的工作區:服務在它存活時等待,而失敗的讀取(server 自己的 broker 連線尚未建立)算作「可能存活」。存活的服務在每次發布之前檢查,並對 token 的外來持有者讓步。第一次詢問失敗的交接會再問一次。

細節

roles/ 紀錄只有在它的 pane 在紀錄的分頁中、而且該分頁帶有塔台圖示,或 pane 帶有存活塔台的 sb_role=tower 標記並執行紀錄自己的 session 檔(該分頁會得到圖示)時,才會成為服務;自 0.75.0 起,單憑紀錄與檔案不會成為服務,因為 session 兩者都寫得到。第一次之後的每次事件重新連線(Herdr 重啟了)都會開啟一分鐘的復原視窗,不論它與監看的接手哪一個先到。佇列每個工作區最多保留 1000 個事件(最舊的先丟),而且沒有比 24 小時更舊的。

在 Herdr 中關閉塔台的分頁(或 pane,或工作區),或 close-tower,會結束服務:身分離開,token 隨著 sb_tower_quit=<members it left> 消失(就像結束中的塔台所寫的那樣),而 service.json 被刪除;佇列保留 24 小時,給在那段時間內重新開啟的塔台。Herdr 沒有回應的 pane 或分頁清單是未知,不是已消失的分頁:服務留著,也不降級任何人。停止或重啟的 server 只釋放它的身分:token 與檔案留著,而守衛在它那個 slot 的 server.json 有開始不到 30 秒的新 restarting 時,會把自己 slot 的塔台身分算作存活。

新的 Herdr server 沒有 token。server 依記錄的 pane 與分頁再次找到每個塔台分頁,否則(接手或 server 啟動之後的一分鐘內)依標有該圖示、在工作區中只有一個 pane 的分頁,再次寫入標記,並在 pane 改變時再次註冊身分。一分鐘內沒有回來的分頁會結束它的服務。id 什麼也證明不了。

細節

Herdr 以短計數器為工作區、分頁與 pane 編號,這些計數器在不同的 Herdr server 之間會重複,所以遺失已存 session 的 Herdr,可能給老闆自己的新工作區塔台原有的那個 id(server 會把塔台指令輸入到老闆的 shell、標記它,並在那裡啟動 run)。在 Herdr 回來之後,或當 server 從 service.json 啟動時,服務只有在記錄的 id 之下的 pane 位於帶有塔台圖示的分頁中,才算就位(typegate.ts 中的 seatGate;圖示是還原所保留的 server 建立的分頁的東西,而 token 與 pane 的環境都沒了)。

在那之前,它不輸入任何東西、不寫任何標記、不喚醒任何 run,而它的 Herdr 連線或監看再次觸發,會使在它之前讀到的證明作廢。證明只針對一個終端機。 Herdr 給新 server 的每個 pane 一個新的 terminal_id,而連線在新 server 回應之後最多兩秒才再次被確認,所以只被它們作廢的旗標,會在其間被相信為重用 id 之下的陌生人的 shell。

因此,每個仰賴證明的動作(輸入、標記、run 的開始,以及提示離開之後 pi 本身的開始),都會對剛讀取的 pane 清單詢問 proofGate:清單中在與被證明的不同的終端機裡的 pane,表示是新的 Herdr,這會使每個證明作廢並立刻開啟那一分鐘(log 中的 tower-unseated),而查看會再次證明就位;失敗的清單什麼也證明不了,因此被拒絕的喚醒會在服務的下一次更新時再做一次。

為 server 剛建立的 pane 做的證明,取第一次顯示它的清單的終端機;Herdr 不列出終端機 id 的地方,證明就只靠圖示。在變動之前開始的 run 以 herdr-restart 結束,沒有啟動 pi,而它的 pane 不會被標為全新。任何其他分頁中的 pane 都是陌生人的:重啟之後的一分鐘內,會在其他 id 之下尋找圖示分頁,找到就跟著;否則服務像結束的塔台那樣結束(tower-closed,佇列保留 24 小時),而陌生人的工作區與 pane 不被動到,連清除一個標記都不做。

在這個 Herdr 上沒有就位的服務所對應的分頁消失時,除非記錄的 id 之下的工作區仍帶有服務所建立的標籤,否則同樣讓它結束:否則工作區不會得到 sb_tower_quit(RVSEAT6-F006)。Herdr 尚未列出的 pane 或分頁,不論哪一方面都什麼也證明不了,並且會被等待。被老闆改名而離開圖示的分頁,在重啟之後無法被證明。

存活的常駐塔台(持有 tower token 的較舊 pi)保有它的工作區:服務等待,只有在該 session 離開之後才註冊(並在它關閉了它的分頁時結束)。由較舊的 session 啟動、或由 Herdr 還原帶回來的這個版本的常駐塔台,在它啟動時為它自己的 pane 詢問 ensure-tower,在 server 寫入 token 之後結束,並把 pane 留作常駐 pane;無法接手它的 server 會讓它維持常駐,並給出通知。

細節

塔台的每次 session_start 都會做同樣的詢問,不論原因為何,所以老闆重新載入(/reload)、接續、fork 或以 /new 取代的、這個版本的存活常駐塔台,會因此被退役:它詢問,server 接手 pane,而塔台結束。服務開啟時,這個版本沒有任何東西會開啟常駐塔台:openTower(升任、call_tower、對塔台的廣播、/boss tower)詢問 server,由它建立分頁,在有事件喚醒 run 之前不啟動任何 pi。

服務等待期間,頁面顯示的是常駐塔台,而不是服務:它自己的通訊畫面與終端機、它的輸入框,以及它的 Quit tower(會詢問它的橋接)都像服務出現之前那樣運作。從未重新載入的較舊版本的常駐塔台,會保有它的工作區直到它離開(服務如上所述等待它);要退役一個,請在 Herdr 中關閉它的分頁(或以 /quit 結束它):服務在它的 token 消失之後註冊身分,而下一次升任或事件會得到 server 的塔台。

角色為 tower、pane 在這個 Herdr 中,而且位於塔台分頁或帶有存活塔台標記的 roles/ 紀錄,會成為 service.json 並被刪除(主智慧體的紀錄保留)。

給該身分的訊息會寫入 tower/<workspace id>/queue/<seq>-<id>.json,刷到磁碟,文字照收到的樣子;log 不含文字。回條 queued 只在 fsync 之後才送給寄件者,而已被接收的訊息(相同的寄件者、去掉重試的 ~r<n> 的基礎訊息 id,以及文字雜湊;seen.jsonl,48 小時,每個工作區 5,000 筆)不會再被排入佇列,但仍會得到它的回條。對塔台身分的失敗傳送,會由 Straw Boss 自己的傳送器(src/reach.ts)以 <request id>~r<n> 重試最多 30 秒。

細節

在 seen.jsonl 存在之前寫的佇列照原樣讀取:重試後綴被去掉,超過一天的事件被丟掉,每個工作區最多保留 1,000 個。給 server 所執行塔台的連線訊息會在 link/inbox/ 中等待,而且不提供給任何 session;老闆會看到一則通知(a Straw Link message waits … ally runs come with the next step),直到 B6。

兩次 run 之間,塔台的 pane(常駐 pane)不是空白的 shell。server 把塔台提示輸入它閒置的 shell,而提示在那裡執行期間,它會在該 pane 上回報一個替身智慧體 tower(規格,The standing pane)。於是塔台在兩次 run 之間,保有它在 Herdr 的 Agents 清單與網檢視中的那一列,位於它的工作區最前面。

node "<cli.mjs>" prompt --workspace <id> --state-dir "<dir>"(src/towerprompt/),第一個字是每個 shell 都接受的,所以 Windows 不需要 bash,PATH 上的 node 就是老闆執行 pi 所用的那一個。它在有第一個狀態可顯示時清除 pane 的畫面,所以 server 輸入的指令不會留在上面(只有可見畫面;捲動緩衝區保留上次 run 的輸出),並畫出標題(分頁的無線電塔圖示與 tower · <label>,粗體)、狀態行(每個狀態有一個圖示與一種顏色,然後是 standing by、3 queued、waking up、running、closing 或 held;狀態改變時,印在老闆正在輸入的內容之上)、作為提示的五列按鍵(⏎ 喚醒塔台、/new、/wait、/quit 與 ⌃D、任何 pi session 中的 /boss tower <message>),以及提示符號 󰐻 ❯ 。

細節

它在其間說的幾行(喚醒塔台、下一次對話是新的 session、server 沒有回應、塔台沒有接收、不認得的指令、離開 pane)帶有各自種類的圖示與顏色。Nerd Font 圖示是塔台分頁自己的,所以假設字型有它。NO_COLOR(已設定且不是空的)或 TERM=dumb 會去掉顏色;pane 所執行的 shell 中的 STRAW_BOSS_ASCII=1 會去掉每個 ASCII 之外的圖示(# tower - <label>、o standing by、Enter、tower> );TERM=dumb 也會去掉清除畫面。

沒有終端機(管線、log)時,輸出是沒有跳脫序列的單純 ASCII,除非 STRAW_BOSS_ASCII=0 要求圖示;外觀在 src/towerprompt/look.ts。文字是英文,而當 shell 的 LC_ALL、LC_MESSAGES 或 LANG 說 zh_TW、zh_HK、zh_MO 或 zh-Hant 時是繁體中文。它每 2 秒透過控制 socket,以狀態資料夾紀錄的密鑰輪詢 server,如同 CLI(在判斷資料夾的擁有者之後才讀取紀錄),不啟動任何東西,也不持有 run token;已停止或載入中的 server 會被等待,並說一次。

  • 等待卡片(src/towerprompt/waitblock.ts、pick.ts)。提示在狀態行下面印出這個 pane 的塔台所開著的卡片區塊(head pane 則是 head 自己的),只在 server 的摘要改變時印一次,摘要由 prompt-poll 帶來,每個欄位截到 120 字:W2 choice · <do> · from <raiser> · parked,最多五行,再接 3 more: /wait(中文介面寫成 還有 3 張:/wait),或一行說沒有開著的卡片;不響鈴。卡片文字在寫出前會再清一次,因為提示是直接寫進終端機的。

    /wait 或 /boss wait 取走鍵盤(提示變成 wait> ):它先把這個 pane 標記為這台電腦的鍵盤(標記不了的 pane 拒絕這個模式,什麼都不讀),列出每座塔台的卡片、這座塔台的排第一,先用數字選卡片,再用數字選動作;按下去的動作、略過的理由與 y/N、回覆都和 pi 裡一樣,被拒絕時會連同重新讀取的清單一起顯示。

    只有打出來的按鍵會按卡片:貼上(括號貼上,或一起到達的多行)結束這個模式,什麼都不按也不送出;一個開始的執行會放棄它(行上的內容絕不會當成老闆的話送出,提示會說卡片現在在執行的窗格裡用 /boss wait,而已經送出的按鍵會先得到回答);在這個模式裡打的數字絕不會變成事件。提示的指令包含 /wait,而在清單上打空行會離開這個模式。

  • Enter 透過 GUI 輸入框與 /boss tower 的端點,把該行作為 boss 事件(present)送出,印出 waking the tower…,而提示留著,在 server 顯示有喚醒待處理的整段期間,每四分之一秒(而不是每 2 秒)詢問一次,不論啟動花多久(Windows 上的一次聲明花了 10 到 46 秒),直到 server 在 pane 裡啟動 run 的 pi 並叫它離開(在送出 3 秒後沒有任何排隊而待命的塔台,就沒有待處理的喚醒,而詢問會再次變慢):老闆的對話。

    老闆在那一秒輸入的東西會被保留在提示的那一行,並在離開時作為第二個事件送出;立刻結束的提示會讓那些按鍵落進 shell 的那一行,run 的啟動在那裡把它們蓋掉,而逾時。單獨的 /new 會讓下一次成為新的 session,而提示留著;單獨的 /quit 把 pane 留給 shell,如同 Ctrl+D 那樣(提示處沒有存活的對話,所以 Leave 沒有東西可以結束)。

    任何其他以 / 開頭的第一行(出於習慣輸入的 /reload)不會被送出:提示以一行回答,列出它接受的東西,而以 / 開頭的訊息要在前面加一個空格寫(以 // 開頭的行照輸入的樣子送出)。在貼上之中,不論行以什麼開頭,這些行都是一則訊息。在 40 毫秒內到達的行是一次貼上、一則訊息;在終端機的 bracketed-paste 標記之間的貼上會保持開啟直到它們結束,不論它的片段相隔多遠到達,而最後一行還沒有行結束的貼上(Windows PowerShell 先送文字,稍後才送 Enter)會等那個 Enter,並連同它整個送出,絕不拆成兩則訊息。

    最後一行只有在與貼上的行到達在同一組輸入讀取中時,才被當作貼上的一部分保留。一組是 bracketed paste 的讀取,從它的開始標記到結束標記,加上任何在前一次讀取之後 40 毫秒內跟上、而且帶有不只一個按鍵的讀取,以及跟在留下未完成一行的讀取之後的單一按鍵(pty 可能把大型貼上拆成多次讀取,也可能只把它的最後一個字元留給一次讀取)。

    一個按鍵是一個字元(一個 code point,é 或 中 和 x 一樣;被拆開的字元的位元組會先被組合,所以半個字元絕不是按鍵)或一個完整的跳脫序列(方向鍵、Alt+b)。輸入的 /quit 與它之後片刻輸入的按鍵,即使只差幾毫秒,也是兩組,而 /quit 會被遵守;一次同時含有 /quit⏎ 與多於一個後續按鍵的讀取,無法與貼上區分,會被保留。最後一行是單獨成一次讀取的單一字元、而且在行結束之後的貼上,是兩則訊息。

    結束標記始終沒有到來的 bracketed paste,在它最後的資料之後 2 秒被關閉。提示自己不打開 bracketed paste(shell 在每個提示處設定該模式,在能關掉它之前被殺掉的提示,會讓老闆的 shell 停在它沒要求的模式,而且 Windows console 可能不認得它):終端機送出標記時,這些標記會被遵守。空的 Enter 會以一行說明老闆在鍵盤前而且什麼都沒寫的訊息來喚醒塔台:這就是老闆現在和塔台說話的方式(見[run 中的老闆](#本機 run))。server 拒絕的行會被顯示(the tower did not take it: …),而提示留著。

  • Ctrl+D 或 Ctrl+C 把 pane 留給老闆的 shell,不論提示裡有什麼(尚未送出的貼上,或正在途中的送出;被保留的東西會被丟掉,絕不在之後送出),而關閉的輸入也以同樣的方式結束提示,所以提示絕不會帶著死掉的輸入繼續執行:server 被告知,替身消失,而在那裡不會再輸入任何東西,不論老闆待多久,直到塔台 run 接手 pane 並結束,或 Herdr 重啟(沒有計時器把 pane 還回去:輸入到老闆正在使用的 shell 裡的提示,會接在他們寫到一半的那一行後面,或把他們的下一個指令當作訊息)。

    老闆透過喚醒塔台取回提示(任何 pi session 中的 /boss tower <message>,或任何事件);那個 run 結束時,提示會回來。老闆可以在那裡手動啟動 pi:那是他們自己的 run。老闆在那裡手動啟動的塔台提示,會在它輪詢時就結束這個停放;Retry 不會結束它。

  • 正在啟動的 run 會讓提示的下一次輪詢說明這件事;它印出 waking the tower…(一次:老闆片刻之前才寫過時不會再印),先把老闆在那一行上輸入的東西(以及 40 毫秒貼上視窗中的行)作為他們的 boss 事件送出,所以他們正在寫的東西不會遺失(如果 server 沒有接收,這些字會印在拒絕訊息之下),然後以 0 結束。提示結束之後、run 的 pi 啟動之前,老闆輸入的按鍵仍會落進 shell(提示無法持有它已經交給 pi 的終端機);這個視窗是 pi 的啟動,不是 Enter 與 run 之間的那一秒。

  • 當 server 沒有該工作區的塔台時(塔台被關閉了),提示會說明並結束。

每 3 秒查看一次每個常駐 pane。它只對前景是它的 shell 且在閒置提示處的 pane(Herdr 的 pane process-info)輸入指令,而且該 pane 沒有智慧體、沒有被 Ctrl+D 停放、最近 15 秒內沒有收到指令(提示送出老闆的字並結束之後的 15 秒內也不:run 在一秒內就會啟動),而且沒有 run 即將接手 pane(有事件在等待,而它的喚醒在 5 秒內到期:那時輸入的提示必須啟動、輪詢並離開,才輪得到 run 的 pi,在 VM 的 PowerShell pane 中要 29 秒):老闆留在那裡的建置或編輯器是他們的。

細節

「閒置提示」是由 shell 的 pid 與 Herdr 在 macOS 和 Linux 上給的前景行程群組來證明的(該群組就是 shell 自己的)。Windows 不論 shell 底下有沒有建置在執行,都只列出 shell(而且它的 foreground_process_group_id 永遠是 shell 的 pid,所以 server 在那裡不相信它:pane 裡的 ping -t 仍看起來閒置),所以沒有被相信的群組的 pane,只有在 server 知道它是全新的時候才被輸入:它建立的分頁、run 釋放的 pane、常駐塔台交接的 pane(交接帶著 pi 的 PID:一旦該行程消失,pane 就算作空白的 shell,在交接後的一分鐘內以及之後一分鐘,所以 /reload 之後也會輸入提示,而且在 pi 仍在 Herdr 列為 shell 的 cmd.exe /c 包裝之後執行時絕不輸入),或 Herdr 重啟之後的任何 pane,而且只有在每個前景行程都是 shell 時;空的清單是未知,絕不是閒置。

因此,在 Windows 上死掉的提示,在下一次 run 或 Herdr 重啟之前不會再被輸入;server 記錄一次 prompt-lost(在那裡輸入卻從未啟動的提示,改記錄一次 prompt-not-starting,而且不再輸入)。從未啟動的提示(shell 的 PATH 上沒有 node、無法處理引號的 shell)會在 15 秒後再輸入,然後 30、60,依此類推,最長每 10 分鐘一次,server 在第三次嘗試之後記錄一次 prompt-not-starting;一次輪詢會結束退避。

Herdr 拒絕的輸入(pane run 失敗)不算一次嘗試:server 記錄 error code=prompt-typed,附上 Herdr 的結束碼與完整 stderr,在 pane 仍被相信是全新時 15 秒後再試,連續三次拒絕之後記錄一次 prompt-not-typed,並且不再輸入任何東西,在任何平台上,直到下一次 run、常駐塔台的交接或 Herdr 重啟。只有 Herdr 自己給的結束碼才算拒絕:被 5 秒逾時切斷的 pane run 可能已經輸入了指令,所以算作已輸入(一行帶信號的 error code=prompt-typed),而且不再輸入。

因為 pane 執行老闆的某個程式而被保持的服務,在該程式消失之後會自己再次啟動:在 Herdr 給前景群組的地方,是當該群組是 shell 的時候;在 Windows 上,是當程式的 PID 消失而清單連續兩次查看都只顯示 shell 的時候(程式是在 pane 的 shell 底下的行程表中找到的,因為那裡的前景清單只顯示 shell)。有任何其他原因的保持,會等老闆的 Retry 或對塔台說的一句話。

Retry 在沒有事件等待、也沒有 run 時,會像 run 結束那樣把 pane 還回去,所以提示會再次被輸入;它在提示輪詢期間什麼也不改變(執行中的提示絕不被輸入,不論 Herdr 有沒有保留它的標籤),而且它絕不結束老闆的 Ctrl+D 停放(pane 仍是他們的)。

在 Windows 上,前景清單無法分辨閒置的 shell 與執行中的程式,Retry 會先讀行程表,只有在那次讀取成功、而且在 pane 的 shell 之下沒有找到老闆的程式(塔台提示除外)時才輸入:失敗的讀取會說明並且不輸入任何東西,找到的程式會讓服務再次被它保持並指名它,而 Retry 會再問一次。歸還最多把 pane 標為全新一分鐘,而 pane 被停放時完全不標。啟動失敗的通知,在按下 Retry 時、老闆寫給被保持的塔台時,以及之後有 run 聲明時消失。

它在提示於最近 6 秒內輪詢過時回報替身,在訊息改變時再回報,並每 4 分鐘一次。在 run 於 pane 裡啟動 pi 之前,它會釋放替身(Herdr 拒絕在有智慧體的 pane 中 agent start:agent_pane_busy),告訴提示離開,最多等 3 秒讓它消失,而且只有在 pane 的前景正是那個提示時(依 PID)才送 Ctrl+C。

然後,在 macOS 與 Linux 上,它最多等 2 秒讓提示的行程本身消失(提示說它要離開不等於它結束:Herdr 在結束後幾毫秒就接受 agent start,而 zsh 還沒有讀取,指令的 Enter 就遺失了),再多等 300 毫秒讓它穩定下來;在最近 15 秒內輸入、還沒有輪詢過的提示會以同樣的方式被等待,而沒有及時結束的提示會在 log 中被指名(run-yield),run 繼續。

Herdr 失敗的啟動記為 herdr <code>: <stderr> | <message>;run 結束,而下一次查看會再次輸入提示,如同它在 Herdr 重啟(每個 pane 都是全新的 shell)或提示崩潰之後所做的。

server 是否可以把指令輸入常駐 pane,只在一個地方決定,src/server/typegate.ts:查看(standing.ts)、run 的結束、Retry 與行程表證明(runs.ts)只蒐集關於 pane 的事實,並依閘門的答案行動,答案是輸入,或是否決並附上說不的規則。改變 server 何時輸入,就是改那裡,而它的表格測試(tests/typegate.test.ts)固定每一條規則。

細節

這些事實依閘門的順序詢問,而提示的輪詢在每次 await 之後(替身的釋放、Herdr 的行程資訊)再次詢問,所以在 Herdr 回答期間到來的輪詢絕不會被蓋過;在 run 於 5 秒內到期時,查看會回答 run-soon,在釋放之後不再重讀輪詢(對那時到來的輪詢不記錄 prompt-lost)。行程表證明在它的讀取之後再次詢問 Retry 的規則:期間到來的事件會把 pane 留給 run。

沒有就位的 pane(seatGate,Herdr 重啟)會在任何其他規則之前得到答案 unseated,而就位像輪詢一樣在每次 await 之後再次詢問;輸入之前,pane 會連同它的行程資訊再次被列出,而就位是依那份清單判斷(proofGate)。查看也保存每個 pane 上指令最後一次輸入的時間與所在的終端機:為同一個 pane 取代遺失服務的服務會像第一個那樣等待,桌面的聲明會詢問它(claimGate),而遺失的服務不會從它進行中的查看輸入任何東西。

有兩個標記是事實,在它們發生的地方設定,不是由閘門設定:Herdr 剛建立 pane 時與 Herdr 重啟之後的 fresh。

為什麼是行程,以及為什麼標籤是 tower

Section titled “為什麼是行程,以及為什麼標籤是 tower”

Herdr(0.9.5)會在一秒內,丟掉它不認得標籤的已回報智慧體,只要它發現 pane 的前景在閒置的 shell;當 shell 之外的行程持有前景時,標籤會留著(看過 52 分鐘沒有更新),並在該行程結束後 0.3 秒消失。已知的智慧體種類會留在空白的 shell 上,但那會把另一個智慧體的名稱放在那一列。所以提示是持有前景的行程,而標籤是塔台自己的。

細節

如果 Herdr 沒有保留替身(連續三次回報,下一次查看都沒有找到),server 會記錄一次 standin-unsupported,並停止為該提示回報:塔台在兩次 run 之間沒有那一列,和以前一樣,其他一切不變。回報沒有接續指令也沒有 session,所以 Herdr 還原從不會重新啟動它,而 pi 結束之後的回報立刻生效(Herdr 不會自己把它帶回來)。

老闆決定(D18、D19)協調經由塔台進行,而主智慧體只在三個時點呼叫它。主智慧體的提示(prompts/main.md)指名了它們,而 call_tower 以 why 接收原因:

why 時機 回覆
milestone 一項任務落地、交付物就緒,或塔台要求的事完成了。 reply: false,除非主智慧體需要答案。
shared 在主智慧體動它的分頁之外的任何東西之前:連接埠、資料庫、共用分支、部署目標、另一個應用程式的 checkout、執行中的服務。它說明是什麼以及多久,然後等待答案。 預期有。
blocked 主智慧體在等它的分頁之外的某個人,或等一個它的授權沒有涵蓋的決定。成員也以同樣的方式回報協調上的摩擦。 預期有。
細節

例行進度在工作看板上,不喚醒任何東西。程式碼把原因寫進標頭(reply= 之後的 why=<kind>)。在 pi 的工具中,why 對主智慧體是必填的,塔台可以省略,而沒有它時標頭是 why=unspecified,事件像 blocked 那樣喚醒。集合之外的字會在送出任何東西之前被拒絕。塔台的 call_tower 把 why 當作選填(它是 blocked 呼叫);常駐塔台提示(prompts/tower.md,towerRuns 關閉時)刻意不帶 why= 的指引,以維持在它的字數預算內。不阻塞的事件放在工作看板上,不喚醒任何東西。

標頭說 why=milestone 與 reply=none、而且沒有本機 run 存活的 intercom 事件,會從第一個這樣的事件起算,最多等 30 秒(hold)等其他事件,所以一起完成的主智慧體會落在同一個 run 裡。其他每個事件(shared、blocked、未指定、預期回覆、老闆、通知、計時器)像以前一樣在 1 秒時喚醒,並帶著任何被保留的 milestone 一起。存活的 run 在它的下一個閒置時刻取得每個事件,不保留。server 只讀事件文字最開頭的標頭,所以在更後面或引用的名稱裡拼出 why=milestone 的訊息不會被保留。

細節

塔台的 run 提示(prompts/tower-run.md)說明每個原因該怎麼做:milestone 更新它的義務,告訴等它的人,並關閉它開啟的主智慧體;shared 呼叫(主智慧體寫入或改變它的分頁之外的東西,不是讀取)會對照看板與它的義務檢查,並回答放行或等待,指名擁有者;十分鐘沒有得到回答的主智慧體會告訴老闆並不再動它;blocked 呼叫會轉給它的擁有者,唯獨只有老闆能做的決定例外,塔台在它的交接筆記中指名它為等待老闆,並告訴主智慧體,而不是把它送回去。

塔台開啟的主智慧體用 call_tower(why: milestone,不回覆)回報它的完成報告。它絕不向主智慧體要求進度,因為 board_read 有。

三種執行共用下面的機制:

flowchart TD
    ev["事件<br/>主智慧體的 call_tower、<br/>老闆的話、<br/>隊友的訊息"] --> q["佇列<br/>每個工作區一個"]
    q --> who{"誰送來的?"}
    who -->|"本機的 session"| er["事件 run<br/>全新 pi,以 tower_done 結束"]
    who -->|"老闆"| bc["老闆對話<br/>接續上一個 session"]
    who -->|"隊友的裝置"| ar["隊友 run<br/>在常駐 pane 旁邊"]
    er --> sp["常駐 pane<br/>待命"]
    bc --> sp
    ar --> sp
細節

塔台服務開啟時(預設),送給塔台身分的事件會喚醒常駐 pane 裡的一個 pi(規格,The wake protocol、Tower tools in a run)。每個工作區同時只有一個本機 run,整台機器最多 towerRuns.max 個(預設 4);一陣事件只會啟動一個執行(喚醒前等 1 秒;已完成的里程碑最多等 30 秒,見主智慧體呼叫塔台時)。boss 事件(GUI 的輸入框、/boss tower <prompt>)優先處理。

啟動。 每次執行前,server 都會對 pane 所在的資料夾跑設定檢查(assertSettingsSafe),被拒絕就什麼都不啟動:事件留在佇列裡,塔台卡片顯示一則錯誤通知,下一次嘗試跟著下一個事件來,最多每分鐘一次。server 會在 pane 裡執行 pi --name "tower run · <label> · <run8>" --sb-run <run id> --tools <the run's tools>,不帶任何 Straw Boss 環境變數,並記錄這個 run的 PID 與啟動時間。老闆對話會在 --tools 之前加上 --session <file>;隊友 run 不帶 --tools 啟動(見下方本機 run沒有 shell)。

run 的 pi 在加入任何東西之前,先靠認領(claim)證明自己就是伺服器啟動的那個 run:它透過 control socket 送出 run id、pane、PID 和啟動時間。只有這些都和紀錄相符、而且該 run 還沒被認領過時,伺服器才會回覆 run token、run 的種類和 brief;被拒絕的 pi 會帶著原因退出。

細節

帶 --sb-run 的 pi 在加入任何東西之前,會先透過控制 socket 向 server 送出 claim {run id, pane, pid, start time}。只有在 run id 是 server 自己啟動且尚未結束的、pane、PID 與啟動時間都和記錄相符(server 自己在行程表裡找 pi 的行程:當 Herdr 只列出啟動 pi 時經過的包裝程式,例如 Windows 上的 cmd.exe /c pi.cmd,記錄的 PID 就是包裝程式底下第一個帶有 run id 且本身不是包裝程式的行程;停止或逾時時會先結束 pi 再結束包裝程式,各自在當下的行程表裡確認;啟動失敗的執行,只結束 pane 裡命令列帶有 run id 的行程)、server 看到的每一次 Herdr 接管都已處理完,而且這個 run尚未被認領時,server 才會回傳 run token(只存在記憶體)、執行的種類和 brief;被拒絕的 pi 會附上原因退出。

常駐 pane 裡沒帶旗標的 pi 會問 pane-role(常駐塔台帶有 STRAW_BOSS_ROLE=tower,且 session 裡有它的 tower 項目,永遠不會被這樣問;已經成為常駐塔台的行程也不會,這由行程自己記住:server 回答了 none,或當時沒有 server 可以回答。它的 /reload、/new、/resume 或 /fork 是它自己的切換,新分支還沒有項目。

尚未決定或單純的 pi 不是常駐塔台,不論它的切換為何;光有環境變數並不夠,因為環境變數會留在已遷移的常駐 pane 分頁裡,而老闆在那裡輸入的 pi 是手動執行:/reload 時若發現 pane 已被 server 標記,否則會變成一個永遠不交接的手動執行;它自己負責交接):Herdr 重啟後 2 分鐘內(Herdr 的 pi 整合在接回舊執行)會被告知退出,或成為自己的執行(hand;在第一次輪詢前 pi 就不見了、或沒有人釋放的認領,因為 pi 在遲到的回答還在路上時退出了,這些都記為 quit,不是當機,所以不會計入當機迴圈)。

只有環境變數的 pi,這個詢問有時限(2.5 秒),但超過時限才來的回答不是可以猜著行動的依據:session 保持未決定(不加入任何東西、不交接、不退出),詢問仍在進行中,回答來時才據以行動(hand 以該認領建立手動執行,none 或較舊的 server 讓它成為會交接的常駐塔台,quit 則退出),期間結束的 session 會釋放遲到的認領,而失敗或始終沒有回答的詢問則留下一個單純的 session 和一則通知。

未決定或單純的 pi 若被 /reload、/new、/resume 或 /fork,仍只有那一個詢問(對已發出手動認領的 pane 再問一次,會被告知該 pane 已被占用),單純的仍維持單純;從未成為正常 session 的 pi 被 /quit 時,常駐 pane 保持開啟,並最多等 2 秒讓進行中的詢問結束,好釋放它帶著的手動認領;行程啟動時間在時限開始計時之前就讀取一次(在 Windows 上這個讀取要好幾秒)。

手動執行在 /reload、/new、/resume 與 /fork 之間保留它的認領(同一個行程、同一個 pane:再問一次只會被告知 pane 已被占用,執行 30 秒後以當機結束)。老闆在執行中說的話,不論 server 有沒有聽到,都會保護該輪不被中止。認領會經過 session 在 server 的 look,所以記錄在任何機密送出之前,先通過擁有者閘門。server 始終連不上的執行,會在結束前中止它的這一輪,除非老闆在裡面(手動執行,或他們在執行中說了話):那種情況下它會等到閒置。

執行的這一輪忙碌時 server 下達的 quit,由執行保留到這一輪結束:pi 保留延後的 quit 而無法收回,所以這期間重啟的 server(執行隨後用它的 run id 再次認領)否則會把一個它從未停止的執行以當機結束。再次認領會丟掉保留的 quit。

它不加入任何網、不發佈 token、不跑守衛。它的角色段落是 prompts/tower-run.md,加上 server 建好的 brief(誰、為什麼、它監督的主智慧體、未了的義務、記憶、摘要、上一份交接筆記、工具、旗標;最多 8,000 字元)。它每秒向 server 輪詢一次,每個閒置時刻取一個等待中的事件,當成帶 triggerTurn 的 straw-tower 訊息,然後確認為 taken。

細節

它的工具是塔台自己的(boss_status、call_tower、tower_list、board_read、tower_send、boss_open_main、boss_close_main、boss_server),加上 tower_reply、tower_memory(塔台記憶)與 tower_done,每一個都是在 run token 之下透過 socket 對 server 發出的請求:工作由 server 以塔台身分完成,所以一切都以 id=sb-tower-<ws> 送出,回覆也回到 server。pi-intercom 的 intercom 工具被擋下,並指向 tower_reply。執行以 tower_done {note, wakeAfter?} 結束(備註 1 到 1,200 字元,交給下一個本機 run;wakeAfter 5 到 240 分鐘,要求一次後續執行,每個工作區同時只有一個待辦);還有事件是給這個 run的時候,它會被拒絕。

intercom 事件以程式寫的發送者行([intercom from <name> (<id>)])抵達模型,所以聲稱是老闆指示的文字可以和真正的老闆事件區分開。名稱與 id 只保留字母、數字、空格與 ._@-(各 80 字元),所以那一行始終只有一行,而真正的 id 永遠是 ASCII 括號裡的最後一組;已知的限制:名稱可能含有長得像括號的字母(例如 ᐸ…ᐳ),而用組合記號書寫的文字(例如天城文)會失去它們的記號。

執行不派遣子智慧體(subagent 與 subagent_resume 被擋下)、除了老闆在它自己的窗格裡打的 /boss wait(見塔台記憶的等待事項)之外不接受 /boss 指令,也不看 server 的任何東西(不啟動也不升級任何 server)。server 重啟不會結束執行:它的下一次輪詢發現 token 不明,會用它的 run id 再次認領;新 server 還沒載入執行或登錄身分時,會對認領回答 wait,而收不到回答或只收到拒絕的執行,最多等 30 秒才結束。/boss server stop(不是重啟)會告訴每個存活的執行退出、結束仍留著的 PID,並把執行記為 quit。

常駐 pane 裡被 Herdr 在沒有旗標的情況下接回的 pi(即使它的 pane 失去了 token,只要它的分頁有塔台標籤),會向 server 詢問它的角色,並最多等 30 秒讓 server 上線,所以 server 的兩分鐘退出規則能夠涵蓋到它。

執行可能被交給不是老闆寫的文字(主智慧體或對等端的訊息、遠端看板項目、舊對話),所以 sb server 啟動它的 pi 時用 --tools,只列出塔台需要的東西:塔台工具(boss_status、call_tower、boss_server、tower_list、board_read、tower_send、boss_open_main、boss_close_main、tower_memory、tower_reply、tower_done、link_request)和唯讀的檔案存取(read、grep、find、ls:合併火車的路由會讀專案的 .straw-boss/apps.json,回答工作進度到哪裡也可能需要一個檔案)。pi 不註冊其他工具,所以 bash、write、edit、subagent,以及老闆其他擴充的每一個工具(網路搜尋、Linear)都不在那裡,呼叫 setActiveTools 的擴充也無法把它們帶回來。

細節

MCP 工具(mcp__*)是「不註冊」的例外:pi 在 --tools 之下仍保留它們的註冊,但它們維持停用(清單裡沒有 tool_search),而下方的 tool_call 後盾會拒絕它們。若有擴充註冊一個名叫 read、grep、find 或 ls 的工具,就會取代內建工具而被放行;路徑檢查只看到那個工具的 path、pattern 與 glob。

執行模式在 tool_call 再次拒絕同樣的名稱作為後盾,並拒絕 read、grep、find 與 ls 讀取以下的路徑:sb server 的狀態資料夾(server.json、gui.json、gui-secrets.json、tower/<workspace>/memory.json、LAN、配對、遠端與連線檔案)、agent 資料夾、家目錄的機密資料夾(.pi、.ssh、.aws、.config、.gnupg),以及金鑰檔(.env*、*.pem、*.key、id_*、*.p12);不論是照字面寫的或經過符號連結,也不論是否按 pi 解析路徑的方式解析之後(鏡射 resolveArg 裡 pi 的 resolveToCwd:file:// URL 經 fileURLToPath 與百分比跳脫、開頭的 @、~、奇怪的空格,以及在 Windows 上的 /c/...、/mnt/c/...、/cygdrive/c/...、~\;Windows 網路路徑(\\host\share)一律拒絕);只有路徑引數會被當成位置來讀(四個工具的 path、find 的 pattern、grep 的 glob),所以以 / 開頭的 grep 規則運算式就只是規則運算式;根目錄包含上述資料夾之一的搜尋(從家目錄跑 grep -r)也會被拒絕,所以執行只能在專案底下搜尋。

這就關上了執行自己寫 memory.json、讀 hello secret(因而偽造 tower-event 或 tower-memory)、以及讀 GUI 與連線機密的路子。仍然存在的是:執行還是可以請有 shell 的主智慧體動手(boss_open_main、tower_reply),這受主智慧體自己的 prompt 與 hook 約束,而主智慧體保有它們的 shell。老闆自己在執行的 pane 裡打的 !cmd 是 pi 給使用者的 shell,不是工具呼叫,運作如前。

路徑拒絕(不是工具清單)也約束兩種保有 shell 的執行。隊友 run在每個信任等級都受它約束,包括 dispatch,所以對等端的執行無法用檔案工具讀取狀態資料夾、連線金鑰或 auth.json,另外還受它發送者等級的閘門約束(link/hooks.ts)。手動執行(老闆自己在常駐 pane 裡的 pi,帶著老闆選的工具)在老闆自己的話沒有開啟的每一輪,同樣受本機 run的工具清單約束(LOCAL_RUN_TOOLS,在 tool_call 強制:bash、write、edit 和清單外的其他工具都會被拒絕,並附訊息指向老闆自己的那一輪或 boss_open_main),也受路徑拒絕約束:對等端、主智慧體或看板的事件會在老闆沉默時送到它那裡。

只有老闆的話(來自鍵盤的 input)在 session 閒置時開啟的那一輪,直到智慧體穩定下來,才算是老闆的;他們在某個事件那一輪的中途輸入的 steer,並不會讓那一輪變成他們的。老闆自己的輪次保有每一個工具。未涵蓋的:常駐塔台(舊模式),以及隊友 run 的 shell。bash 仍然可以在命令裡指名受保護的路徑(cat ~/.pi/agent/straw-boss/server.json、變數、萬用字元、cd),而命令列無法被可靠地檢查,所以不嘗試對它做任何過濾:有人在受管的電腦上以 dispatch 等級連來的對等端,就擁有這台電腦使用者的權限,包括 sb server 的老闆憑證。

這是留給老闆的選擇:在 dispatch 給隊友 run 本機 run的工具清單(它做實際工作就需要 open-main),或保留 shell,並照等級所說去信任對等端。

token 的種類。 server 把每個帶 run token 的請求限定在該執行的種類之內。本機 run可以讀 status、使用連線持有者的 list、board-read、send、confirms 以及已核准請求的操作(一律以它自己的塔台身分),並對自己的工作區使用 open-main 與 close-main。隊友 run 的 token 一個都沒有。gui、gui-remote、gui-pair、close-tower、restart 以及控制 socket 的其餘部分,對任何 run token 都一律拒絕:hello secret 是 session 或 shell 的憑證,run token 只是執行的憑證。

老闆一旦在執行中說話,它就不會自己結束。他們說話的方式有:在塔台 prompt 寫字(它喚醒的執行從認領起就屬於他們)、在執行的 pi 裡打字(它的擴充在第一個字時通知 server,之後最多每 15 秒一次:只算來自鍵盤的輸入,絕不是事件或 server 的 steer),或在常駐 pane 裡手動啟動 pi。老闆在執行中的時候,tower_done 回答說他們在這裡,不記備註,執行繼續,所以模型會繼續回答他們。

細節

執行在老闆退出它時結束(/quit 以完成結束,備註為 (the boss left the run),在老闆對話裡是 (boss quit);它尚未取走的事件等下一個執行;退出一個老闆不在其中的執行,仍然是當機),或在他們沉默 10 分鐘後(沒有輸入、沒有輪次),這時執行會得到下面的收尾。

上限: 下面的上限與閒置限制,在老闆於執行中的期間不適用,連一輪進行到一半也一樣,從他們的第一個字算起最長 4 小時(從執行的第一個字起算,不因沉默之後的回來而重新開始);超過之後上限立即適用,之後老闆再有任何跡象都不會改變什麼。老闆沉默之後,執行獨享閒置收尾(6 分鐘),上限不會把它截短。server 重啟會忘記誰在執行中,直到他們的下一個字。

30 秒沒有輪詢的執行視為遺失,以當機結束,除非電腦睡著了:當 server 自己的監看發現,它的某個 tick 結束到下一個 tick 開始之間的間隔超過這個時間(慢的 tick 不算睡著),就沒有執行能在這段時間輪詢,所以每個執行的沉默都從那個 tick 起算。

細節

沒有 tower_done 就閒置下來的執行,60 秒後得到一次 steer,5 分鐘後得到 quit;18 分鐘時被要求收尾,20 分鐘時被 quit(quit 後 10 秒仍未退出的 pi,會透過 Herdr 收到 Escape 與 /quit,再 5 秒後 SIGTERM 送到 herdr pane process-info 回報的 PID)。

在 Windows 上,以 TerminateProcess 結束的 pi 會讓它 pane 的 console 停在一個模式,之後在那裡的下一個 herdr agent start 會輸入出殘缺的命令並逾時,所以等待時間改為 30 秒與 20 秒,停止中的 server 等它的執行離開 8 秒,而且從不對仍留著的 pi 發訊號(執行就沒有結果,如同重啟時留下它的樣子:那個 pi 會從下一個 server 重新認領,或在 server 已離開 lostMs 之後的下一個閒置時刻自己結束:一個正在進行一輪的 pi 先中止這一輪,因為 pi 把關閉保留到它的下一個閒置時刻,而一次模型呼叫可能持續好幾分鐘,當那個 pi 不見時,下一個 server 把執行記為 quit,不是 crashed),被發過訊號的 pi 所在的 pane,在下一個執行之前換成一個新的常駐 pane,而且只關閉常駐 pane,並且只在它裡面是 shell 或該執行自己的 pi 時才關(跑著老闆程式的 pane,以及同一分頁的其他 pane,都會留著;分頁隨後失去塔台標籤;服務保留它的佇列與通知),而 Herdr 以錯誤碼 timeout 回報的啟動,會在新的 pane 裡再試一次,且不計入當機迴圈(其他任何失敗都結束這個 run)。

當機會把它的事件退回佇列,並標記 possibly handled by a run that crashed: check before acting;被兩個都當機或逾時的執行取走的事件會被擱置(卡片顯示重新送達與捨棄);10 分鐘內三個當機或逾時的執行會暫停服務(卡片顯示重試)。隨 Herdr 一起死掉的 pi 不算當機。 Herdr 停止時會帶走它各個 pane 的行程。

server 對 Herdr socket 的監看會說出一個曾經回答過的 server 何時停止(herdrLost;記錄行 herdr-lost 帶有它的時間,在一次乾淨的停止仍以 crashed 結束執行時,用來和執行的 run-end 比對),而進行中的接管在它的 look 完成時,讓每個存活的執行以 herdr-restart 結束;期間被看見消失的 pi(或在停止後 30 秒內,這是執行可以沉默的時間)等著那個,而且兩者都不計入暫停。Herdr 還在時被看見消失的 pi,只有在下一個 tick 也看見它消失時才是當機,因為 socket 可能在 pane 的行程之後片刻才關閉。

隊友 run 也一樣,它的訊息隨後留在收件匣,等下一個執行。常駐 pane 裡跑著老闆的程式(不是 shell、不是塔台 prompt)時啟動失敗,是 pane 自己造成的:它們的東西一概不關(而且不是 Herdr 的 timeout 碼的啟動不會得到新 pane),這個 run像任何當機一樣計入迴圈,那一則啟動通知會指出程式的名稱(它的可執行檔檔名,不含路徑或引數;在 Windows 上,Herdr 的前景清單只顯示 shell,所以在 Herdr 回答 agent_pane_busy 時,會到行程表裡 pane 的 shell 底下去找這個程式),而暫停的服務在 pane 的前景回到它的 shell 之後會自己再啟動,如同按重試或老闆說一句話時一樣。

boss_server 與重啟限制。 在本機 run裡,boss_server 經過 server:它對隊友 token 拒絕 restart、status 與 logs,而每分鐘一次重啟的限制由 server 自己守著,所以兩個智慧體同時要求也不可能都通過。

塔台單位以身分的形式留在地圖上:執行工作時是 working,在對話框上是 waiting,沒有存活的執行時是 standing by(待命)。服務自己的狀態在地圖、卷宗與通訊畫面標頭裡有自己的字(喚醒中、收工中、已暫停、隊友 run · 裝置),後面接著上一個執行如何結束(當機、逾時、退出)以及最近失敗了多少個執行;地圖卡片帶有等待中訊息數量的計數晶片。等待中的事件以單位的排隊列呈現(種類、發送者、第一行),標示為等待下一個執行,沒有編輯(server 的佇列不是 pi 的)。

細節

通知連同它們的重試、重新送達與捨棄按鈕,顯示在地圖卡片上,也再次出現在塔台的通訊畫面裡;按鈕在 server 回答之前是關閉的,並顯示「處理中…」,拒絕則顯示在該列底下。執行的事件是 straw-tower 類型的 custom_message 項目,通訊畫面依 server 送出它時帶的 kind(details.kind:boss、intercom、notice、timer、link;steer 是 server 通知)來讀每一則:老闆自己的訊息是拿掉 server 標記行的 YOU 泡泡(其後的當機旗標顯示為泡泡裡的一則註記),intercom 訊息、server 通知與喚醒呼叫是帶標籤、沒有標記行的進來的註記,而隊友的訊息(隊友 run 的輸入、主智慧體的 link_reply、本機 session 的答覆)則依它的性質加上標籤(request from、reply to your ask from、answer from、message from the device),文字放在裡面,而經過驗證的標頭收在「details」後面。

只有 details.kind 能讓一則訊息成為老闆的,絕不是它的文字,而且只在 sb server 所執行的塔台的執行裡才算:在任何其他 session(主智慧體、隊友 run 的通訊畫面)裡,種類為 boss 的 straw-tower 項目顯示為帶標籤的通知,文字照寫的樣子,絕不顯示為 YOU。intercom 訊息從 server 自己的發送者行顯示發送者的名稱與 id(清理成字母、數字、空格與 ._-@,最多 80 字元),並說明它來自這台電腦;經 Straw Link 來的隊友訊息會寫 teammate,所以兩個同名的發送者不會看起來一樣。尚未說過任何話而待命的塔台,會解釋待命是什麼,以及第一則訊息會啟動一個執行。

隊友 run顯示在塔台狀態的旁邊:隊友 run 工作時,狀態寫成 ally run · <device label>(或一個數字),而不是「standing by」,而每個執行一個晶片(ally run · <device label>,有好幾個時前面先寫數量)放在地圖卡片上、工作區檢視裡,以及通訊畫面標頭底下。

只有在執行的 session 檔案已知時,晶片才會以唯讀開啟該執行的通訊畫面:網把每個這樣的執行當成自己的單位(key ally-<run id>,沒有 pane,所以輸入、按鍵與焦點都回答 409 not_live),不讓路徑出現在頁面上,並在執行離開網十分鐘後把這個 key 從允許清單拿掉,如同任何 session;標頭寫「唯讀」,而不是開放的通道,輸入框與附加按鈕是關閉的,而輸入框說明隊友的訊息驅動這個 run。

執行連同它的檔案一起回報它的 pi session id,而 server 只有在檔案以那個 session 命名時(<time>_<session id>.jsonl)才保留它;執行兩者都回報,所以這只能擋住失手,擋不住說謊的執行,這是依路徑讀檔共有的殘餘風險。工作區檢視帶有和卡片相同的佇列晶片與通知(重試、重新送達、捨棄)。等待隊友 run 的訊息(served)說明隊友 run 還沒有取走它,並指向塔台的卡片;轉送來的確認會指出它來自哪一位隊友的塔台。

link_request 的對話框維持在擴充寫它時用的語言(英文):它的標題是 server 用詞、智慧體的理由、以及詢問者與裝置名稱組成的一個字串,而把固定用語從中拿掉的頁面,會在下一次改寫用詞時無聲地壞掉,所以頁面照寫的樣子顯示它,旁邊配上自己的在地化外框。輸入框寫著「喚醒塔台」,而送出會發出一個 boss 事件(不會往 shell 輸入任何東西);在沒有存活的執行時,它的按鍵、鏡像與焦點回答 409 standing,而終端機分頁用一句話說明,而不是顯示按鍵板。

單位的通訊畫面在有老闆 session 時就是老闆 session(它工作時是存活對話的檔案),否則是存活執行的 session,再否則是上一個執行的;事件 run是旁邊的晶片,唯讀(見老闆對話)。

/boss tower <prompt>。 和單獨的 /boss tower 一樣先確保塔台存在,把 prompt 以帶 present 的 boss 事件送出(它啟動或延續老闆對話,如同 GUI 的輸入框),並回答「Handed to the tower」,不改變焦點。/boss tower status、parked、retry、deliver <id>、drop <id>、leave 與 new 是該節的指令。單獨的 /boss tower 仍然是開啟並聚焦那個分頁。

本機 run有 link_request:server 開出請求,老闆的核准對話框(和主智慧體得到的同一個:動作、每個引數、理由、提出請求的 session)由這個 run的 pi 顯示,而動作只在得到 yes 時才由持有者執行;server 拿到核准的 call id,並且只對明確的 yes(執行的對話框以 allow 關閉)才接受,所以執行無法自己核准自己。tower_send 接受 device、reply: true 與 for,後者是提出請求的工作區裡一個存活的主智慧體:server 記錄這個詢問(tower/<workspace id>/awaiting.json,24 小時),裝置的答覆直接送給那個主智慧體(見隊友 run)。來自 pi-intercom broker 的信箱 flush 被標記為已送達,卻沒有回執;擬議的修正寫在pi-intercom 提案。

老闆對話是當沒有存活的本機 run時,帶 present 的 boss 事件(塔台 prompt 的 Enter、GUI 的輸入框、/boss tower <prompt>)所啟動的本機 run(規格,The boss conversation;D23)。它接續老闆上一個 session;其他每一個本機 run,即事件 run,都啟動一個新的 session,並以 tower_done 結束,一如以往。每個工作區只有一個對話,不論老闆從哪裡寫的:GUI 的輸入框和 prompt 是同一個 server 函式(runs.boss(…, {present: true})),所以兩邊來的訊息都延續同一個對話。

tower/<workspace id>/boss-session.json({file, run, startedAt, lastAt, turns, fresh?},模式 600)指名它。只有對話能移動它,靠它自己的 run-file 回報,而且只限 pi 的 sessions 資料夾底下的 .jsonl(真實路徑,所以不會跟著跳出它的連結)。pi 只在第一則助理訊息時才寫出新 session 的檔案,那是在執行回報它之後,所以該資料夾底下一個尚未在磁碟上的 .jsonl 會被擱置(不報錯),並以同樣的檢查,保留到執行的下一次輪詢、老闆的下一個字,或執行結束;始終沒有出現的檔案不留下指標,而一個絕不可能合格的路徑在回報時會被拒絕並記錄。

細節

等待、期間進行的輪次,以及期間要求的 New(塔台 prompt、GUI 或 pi 自己的 /new)都記在執行記錄裡,所以 server 重啟不會失去它們,而檔案被標記為 fresh 保留。事件 run永遠不會移動它,老闆在事件 run的 pi 裡打字也不會。在事件 run存活時抵達的老闆訊息會送到那個執行(它不等老闆),塔台的通訊畫面隨後顯示該執行直到它結束;執行記錄保留老闆第一次在其中說話的時間(bossIn),而下一個對話的 brief 會指出該執行及它的交接筆記。

當下列每一項都成立時,server 以 pi --name "tower · boss · <label>" --sb-run <id> --session <file> 啟動對話,否則就不帶 --session,並由 brief 說明原因:該檔案是 sessions 底下的一般檔案,在該資料夾以下的路徑任何地方都沒有符號連結,而且屬於老闆自己(POSIX uid),至多 4 MB,老闆在其中的上一輪至多是 7 天前,沒有任何存活 pane 的 agent_session 指名它,而且老闆沒有要求新的。turns 計算對話取走的老闆事件,以及老闆在它的 pi 裡的打字(最多每 15 秒一次)。認領、run token 與執行模式都是事件 run的;隊友 run 從不接續它。

run token 不能啟動、離開或重啟對話,也不能以老闆的身分寫入:tower-event、tower-leave、tower-new 與其他老闆指令,對每個 run token 都一律拒絕(rungate.ts),所以接續的 session 的智慧體流量不會得到任何地位;執行的 prompt 只說以老闆行開頭的事件才是老闆的,不論多舊。

老闆從認領起就在執行中,所以[執行中的老闆](#本機 run)底下的一切都成立:抵達的事件在它的閒置時刻送給它,tower_done 以留下回答,而 20 分鐘上限與 60 秒閒置 steer 都不適用。brief 的第一段寫著 Boss conversation: the boss is at this pane. Resumed session from <date>, <n> boss turns 或 New session: <why>,而對接續的對話,還有 Since you last talked with the boss:,後面接此後寫下的最多 3 則事件 run備註的第一行(合計 1,200 字元,整段 1,400)。

細節

存活的執行在記憶或上一則本機備註變動時,會得到較新的 brief(輪詢回答裡的 brief);它的下一輪會讀到它。老闆在執行閒置時沉默 10 分鐘,或 4 小時上限,會觸發收尾:一次 steer(「The boss has left: call tower_done with your handoff note.」),5 分鐘後 quit,備註為 (ended idle) 或 (ended at the 4 h cap),記為 quit(對話從不是逾時,所以上限永遠不計入當機迴圈)。tower_done,以及老闆在對話的 pi 裡的 /quit(備註 (boss quit),記為 done,長時間沉默之後也一樣),立即結束它。server 重啟會讓對話繼續存活,從它的開始算起。

/boss tower leave(GUI 的離開對話、在塔台輸入框打的 /quit、socket 上的 tower-leave)告訴 pi 退出,並像 /quit 一樣結束它;/boss tower new(GUI 的開新對話、輸入框或塔台 prompt 的 /new、tower-new)標記指標,讓下一個對話是新的 session,並以同樣的方式結束存活的對話。在對話自己的 pi 裡打 /new 有相同的效果。沒有存活的對話(事件 run不算)或它還沒認領時,離開會被拒絕。這些詞必須恰好是 /quit 與 /new:多了別的字,它們就是訊息。

細節

/boss tower。 status(服務狀態、帶期限的對話、通知、擱置的事件、最近的執行)、parked、retry、deliver <id>、drop <id>、leave、new;其他任何東西都是給塔台的訊息。每一個都是 GUI 的按鈕也能觸及的一個控制指令(tower-retry、tower-deliver、tower-drop、tower-leave、tower-new,以及 status),帶著 server 的拒絕;寫入在沒有畫面的 pi 裡會被拒絕,如同 /boss memory 的寫入。這些詞有英文與繁體中文。恰好是這些詞之一的 prompt 就是指令。

另一個塔台、主塔台,以及回答等待事項

Section titled “另一個塔台、主塔台,以及回答等待事項”

/boss tower -w <workspace id | label | head> <command | message> 對另一個工作區的塔台動作:工作區用 id 或標籤找出,方式和 /boss memory -w 找它一樣(兩個工作區共用的標籤會被拒絕並列出它們的 id:請指名 id;主塔台自己的 Herdr 工作區,不論用 id 或標籤,都是 head),那裡每一次寫入都會在 pi 裡詢問並指名塔台,而訊息以帶 present 的 tower-event 送出,如同 GUI 的輸入框送出的方式,不開啟也不聚焦塔台(它需要塔台 run開著)。

細節

head 是主塔台:它的指令是 head-retry、head-deliver、head-drop、head-leave 與 head-new,由 /boss head retry | parked | deliver <id> | drop <id> 不經確認送出;這是標準寫法,而 -w head 送出同樣的指令,但會先詢問並說「the head tower」,如同在另一個塔台的每一次寫入。-w head <message> 就是 /boss head <message>,而單獨只是一個主塔台指令詞的訊息(close)在那裡會被拒絕:多寫幾個字,或用 /boss head。

--answers W<n>(在 -w 之前或之後;用在 /boss tower 與 /boss head)帶著 answers 送出訊息:塔台取走訊息後,server 把該等待事項標記為已回答,如同頁面的 Answer 做的(answered 是 id,或在記憶已不含它時為 false),而那一行其餘的部分此後一律是訊息;帶 answers 的恰好 /quit 或 /new 會被拒絕(A command is not an answer to a wait),如同頁面的輸入框拒絕它。沒有畫面的 pi 拒絕 -w 寫入;parked 與 status 是讀取。

/boss send <name|key> [--after] <text> 把 sessions/<key>/input {text, as: "send" | "followUp", guarded: true} 送到塔台、主智慧體、成員或子智慧體,如同頁面的輸入框做的(--after 是輸入框的接在這之後;單獨的 --after 而沒有文字是用法錯誤,不是訊息)。含空格的名稱要加引號(/boss send "fix login" hi);兩個 session 共用的名稱會列出它們。

細節

這個送出來自這台電腦,但已配對的裝置可能是打這個指令的人(它的輸入框,或按鍵板一個字母一個字母地打),所以 guarded: true 讓 server 讓它通過已配對裝置自己的寫入會遇到的每一道守衛,不論它的位址為何:僅限本機的文字(localOnlyText,它也讀 /boss send 的文字,在任何深度,如同讀 /boss open 的文字那樣)、未決的 link_request,以及 /boss gui lan 畫面的標記。一般的守衛也照舊(session 正在等的對話框、長度限制),而結束或取代 session 的指令,會在 pi 裡指名該 session 並詢問,之後才帶 confirmed 再送一次。這個 session 本身會被拒絕。

/boss stop <name|key> 如同頁面的停止一樣詢問(它需要 pi 的畫面),然後把 {keys: ["esc"]} 送到 sessions/<key>/keys;單獨的 Esc 不需要 guarded。兩者都是老闆親自打的輸入:塔台 run或隊友 run 不接受 /boss 指令,也沒有任何工具能觸及它們。

/boss answer 列出其他 session 正在等的對話框;/boss answer <name|key> [<choice number | text> | --cancel] 回答其中一個,如同頁面做的。這份清單就是頁面自己的讀法:本機 events 串流的一則一般訊息(readStream,它讀完會再關閉),其中等待中的單位帶有 pending.answer(對話框 id、它的摘要、select 或問題)。

細節

串流在 hello 之後先送出它快取的讀數,而沒有頁面在聽時,那個讀數最多是 10 秒前的;所以接在 hello 之後的網會繼續讀 0.8 秒,並以最新的為準(在 hello 之前的網是剛剛才讀的,立即成立),而剛剛開啟或關閉的對話框,就照它現在的樣子列出。清單與確認會在每個選項底下顯示它的備註與各選項的說明,如同頁面,而串流回報的 problem 顯示時不帶它的 JSON 引號。

讀串流對 operate 主機(host.page())算是一個頁面,所以在列出之後約 60 秒內,控制端會保持它到盒子的 operate session 開著,雖然盒子的對話框在這裡被排除;只有 server 旗標能省掉這點。無人值守盒子的單位(base.box)以及這個 session 本身會被排除。

答案的建立方式和 /boss link answer 建立一個一樣(bosslink.ts 的 build:select 用數字,問題用數字或文字,什麼都沒打時用 pi 自己的 prompt,多個問題則一次一個),連同問題完整顯示在一個只有這台電腦能回答的確認裡(hereOnlyOpts() 並標記 pane,heremark.ts),然後 post 到 sessions/<key>/answer {dialog, digest, answerId, choice | answers | cancel},即頁面 post 的路由,從這台電腦發出,如同它的本機頁面。所以 server 保留每一項檢查(checkAnswer、依顯示樣子的問題摘要、每個 session 一個開啟的對話框、已配對裝置的 hereOnly 選項),而 stale、changed 或 unknown 的結果由它的答覆來措辭。

機密從不在這裡回答:當一個等待事項的 pending.tool 是 secret_drop 或它的 why 是 SECRET(ask.ts、dialogs.ts 對機密對話框給的讀法,所以頁面與這個指令共用同一個字串)時,它就是機密;它被列為存在,並在問任何其他事情或 post 任何東西之前被拒絕,即使它的 pending 帶著 answer(server 從不給),而且這個指令的任何請求都不帶 value。

它是老闆親自打的輸入:塔台 run或隊友 run 不接受 /boss 指令,沒有任何工具能觸及它,而 control.ts 的 BOSS_LOCAL 讓已配對裝置的輸入框無法打它(在 /boss send 或 /boss open 的文字裡也一樣,如同 link decide)。

塔台的通訊畫面是老闆 session(跨執行穩定;它工作時是存活對話的檔案):一條分隔線開啟每個對話執行(Boss conversation · resumed · 10:04 或 · new session ·,一旦結束,還帶著該執行如何結束與它的備註),而事件 run是標頭底下的晶片(event run · 10:12,在它們的 session 檔案已知時是網的一個唯讀單位,結果與備註在提示文字裡)。

細節

狀態字寫著 boss conversation · ends after 10 min idle,執行等老闆時是 … · waiting for you · …(塔台那時顯示閒置,不是工作中),或 … · closing;輸入框上方的方塊說明它如何開始(接續並附輪數,或新的並附原因)、它現在在做什麼、閒置期限與硬上限,旁邊有離開對話與開新對話。輸入框的 /quit 與 /new 在塔台裡不要求確認。

老闆事件的標記行(BOSS_MARK)逐位元組不變:接續的 session 保有舊的行,而頁面靠那段文字讀它們;寫入者(GUI、prompt、pi)不告訴模型,所以沒有 via。

事件 run從頭開始,所以必須比執行活得更久的東西,另外放在 tower/<workspace id>/memory.json(規格,Tower memory):老闆的長期指示(最多 12 條)與決定(最多 24 條),各至多 300 字元,塔台的等待事項(開啟中最多 12 個;對智慧體是 200 字元,對老闆是一張短欄位的卡片),以及老闆尚未回答的執行所做的提案(最多 12 個)。每個本機 run的 brief 都在一個 Memory 段落裡帶著它們(至多 4,900 字元,排在未了的義務之後;長期指示一律完整放進,在 2,400 字元的預算之內;待處理的提案放最後,標示為未確認)。

細節

當整份 brief 超過 8,000 字元時,記憶最先被砍,所以工具、tower_done 那行、當機旗標與 20 分鐘那行始終都在。隊友 run 的 brief 從不帶記憶,而 server.log 只保留一次寫入的操作與 id。

stateDiagram-v2
    [*] --> proposal: run 用 tower_memory 提議
    proposal --> live: 老闆確認
    proposal --> [*]: 被拒絕,或 7 天後過期
    [*] --> live: 老闆用 /boss memory 新增
    live --> [*]: 退役

老闆寫,用工作區內任何已加入的 pi session 裡的 /boss memory:/boss memory 列出(塔台的提案排最前面,各帶它引用的老闆訊息),/boss memory confirm P3 與 reject P3 回答提案,add standing|decision <text>、edit S2 <text>、delete D4 與 resolve W1 [outcome] 改其餘的(-w <workspace> 指名另一個工作區;這些詞依你的語系是英文或繁體中文)。pi 是第一個介面:塔台通訊畫面的記憶面板與 GUI 的塔台卡片,以同樣的 rev 檢查呼叫同一個 server 函式,自己不另加任何東西。

細節

本機 run透過 tower_memory 只能提議一條長期指示或一項決定,或撤銷其一:提案與存活的項目分開放著等 7 天,只有在老闆確認時才生效(因清單已滿或項目已變動而無法套用的確認會被拒絕,提案留著)。這個工具沒有確認,控制 socket 的 tower-memory 指令對每個 run token 都一律拒絕,而執行除了 /boss wait 之外不接受 /boss 指令(它只按老闆自己清單上的卡片,不造出指示,見下),所以沒有任何執行的工具、隊友、連線信封或訊息能造出一條指示。

這就是全部的保證:它對抗的是這些路徑,不是同一個使用者、握有 shell 的行程。任何有 shell 的智慧體(主智慧體和執行一樣)都能讀 server.json 裡的機密,並向控制 socket 送 tower-memory 指令、直接寫 memory.json、打 pi -p "/boss memory …",或往 pane 裡送文字。/boss memory 在它的 pi session 沒有畫面(pi -p、RPC)時拒絕每一次寫入,這只關上 pi -p 這條路;其餘就是整個 server 所依賴的同一使用者信任(限制 T3R-F005,它也涵蓋主智慧體)。執行可以用 read 讀整份記憶與提案。

在滿額時,老闆自己的新增會被拒絕並附訊息;不會有任何東西被丟掉。隊友 run 依它的 token 種類被拒絕使用這個工具。

等待事項是塔台自己的備註,絕不是指示。對 for: "boss" 的等待事項是一張卡片,欄位由塔台填寫(type、happened、do、url 或 command、after、options、secret.name,以及 from,即引起它的訊息的 session id,會對照執行收到的內容檢查),以 sb 自己的語系寫成;sb 檢查格式、長度、URL 與命令,以及沒有任何東西長得像機密,會連同每一個要修正的欄位把卡片退回,而且從不判斷語言(規格)。

在它存在不到一天時,它以警示的形式到達老闆:dock 的對話框與禪的卡片完整畫出它(發生了什麼、要做什麼、在哪裡、之後會怎樣、誰提出的、多久以前),而塔台卡片、它的通訊畫面與主塔台頁面把它收成一行(種類標籤、要做什麼、誰提出的;點一下展開面板,收起來的一列自己沒有按鈕);頁面隱藏時,系統通知會說一次。

卡片就是警示確認卡片那個提問面板(gui/askview.ts,與 gui/ask.ts 共用),在標準、極客、鴉閣與禪都一樣:它的頂部放種類標籤、誰提出的與多久以前,以及自動模式的「已暫放,等你回來」標記;要做什麼是它的問題;它的動作是單選群組的列,而且每一次按下都是先選,再送出(點一列只是標記它,沒有任何預選,所以不會有誤觸按下去;手機上每一列、送出與連結都是 44 px)。

列依它的種類而定。動作卡片有我做完了;選擇卡片每個選項一列,底下寫著選那個會導向什麼;機密卡片有開始輸入,它告訴提出者呼叫 secret_drop(值在它的終端機裡輸入,從不在頁面上;已配對的裝置顯示「請到電腦上輸入」而沒有開始按鈕),然後是我輸入好了。每張卡片都以回覆塔台與略過兩列作結,而打開 是送出旁邊一個安靜的連結。在卡片出現之前寫的等待事項,以塔台的舊用語畫出,並附請塔台重寫成卡片這一列。

URL 是點擊才開啟的文字(不會自己開啟或擷取任何東西),而命令是帶複製指令的文字:sb 從不執行它。按下去會到記憶路由的 press 操作,它依 session id 告訴提出者(一則 wait_card 訊息),然後告訴塔台,塔台的事件會說提出者有沒有收到。略過同樣是兩下(先選它,再送出):選它會顯示誰會被告知與一個選填的理由框,送出則告訴塔台與提出者老闆不會做這件事;等待事項在已解決的狀態停留一天,而不是消失。

回覆塔台選了之後在列的底下顯示一個答覆框:Enter 或送出告訴塔台,這些字原封不動送到塔台的 session input,如同它的通訊畫面的輸入框送出的那樣;拒絕顯示在保留的草稿底下,塔台取走時對話框關閉,禪的卡片隨敲門離開,塔台卡片那一列或它通訊畫面裡的框清空並取消選取該列(所以第二個 Enter 什麼都不送),而禪裡的 Esc 把該列放回未選並保留草稿,但在答案還在路上時不行。對話框與禪的卡片標題寫一次角色與地點(在 zh-TW 是「塔台 · Harbor API」),而不是單位的原始英文名稱;通訊畫面連結與稍後在對話框的頁尾。

主塔台的卡片用它自己的詞說主塔台(zh-TW 是主塔台,鴉閣裡是鴉閣;卡片訊息的 _head 對應版本),工作區的卡片說塔台(鴉閣裡是管家)。

訊息指名它回答的等待事項(文字旁邊帶 answers: "W3"),而一旦塔台取走,server 就把那個等待事項標記為已回答(記憶路由的 answer):它留在塔台的記憶裡等執行去解決(執行 prompt,包含主塔台的,說明遇到寫著 answered by the boss 的一列該怎麼做;它不占記憶保留的 12 個開啟中等待事項的名額,並像開啟中的一樣 7 天後過期),但它不是警示,所以塔台處理它期間,沒有任何檢視會再次提起它,而卡片與通訊畫面那一行說明按了什麼、誰被告知了,或「Answered, waiting for the tower」(/boss memory 說「you answered it; the tower has not resolved it yet」)。

只有等待事項自己的框會標記它:在塔台通訊畫面的輸入框裡打的答案是一則一般訊息,會讓等待事項維持開啟,而帶 answers 送出的指令(/quit、/new)會以 not_an_answer 被拒絕。塔台新寫的等待事項會再問一次。等待事項也會作為塔台卡片與它通訊畫面裡的一行留著,直到它被解決、捨棄或過期,所以沒有人能回答的等待事項絕不會卡在那裡;一天前的等待事項不發警示,所以打開的頁面不會對它嗡嗡作響。

brief 以 (open 2d 3h) 顯示一天前的開啟中等待事項,執行會檢查它並解決、捨棄或重新陳述;for: "agent" 的等待事項指名一個存活的 session,而來自它的訊息會把等待事項標記為已回答(由執行判斷它是不是答案)。開啟中的等待事項 7 天後過期(在下一份 brief 裡顯示一次為已過期);已解決或已過期的再留 24 小時,在面板裡畫上刪除線。常駐 pane 的「waits for you」那行隨塔台 prompt 而來。示範的 ?waits=cards 顯示每種種類與狀態各一張卡片,而 ?waits=cards&head=waiting 在主塔台頁面加上主塔台自己的卡片。

在終端機裡按卡片(todo #144,規格)。頁面能按的每一個動作也都能用打字的方式按:任何已加入的 pi 裡的 /boss wait [-w workspace | label | head] 列出每座塔台與 head 開著的卡片(這座塔台的排第一;-w 只列一座),先在選單對話框選卡片,再在另一個對話框選這張卡片的動作(動作由種類與狀態決定:動作卡有完成與略過,選擇卡每個選項一個,機密卡有開始輸入與我已輸入,卡片出現前寫的等待事項有重寫,永遠還有回覆與返回),並送出頁面那個 press op 本身,所以頁面按不了的東西這裡也按不了。

略過先在輸入對話框問理由,再開確認對話框;回覆是一個輸入對話框,以老闆身分連同 answers 送給塔台。打數字與 y/N 只發生在常駐提示,因為它沒有對話框(見上面的常駐 pane)。server 用 boss-waits 列出它們(一個對每個 run token 都拒絕的控制指令,和 tower-memory 一樣),每張卡片被讀出時都再清一次,並帶 parked 與 server 的 auto 旗標。

選擇器的對話框只在終端機(terminalOnlyOpts),以老闆自己的指令執行,整個選擇器都在此處標記之內:這個 pane 必須能標記為這台電腦的鍵盤,標記不了的 pane 拒絕選擇器,所以手機的按鍵板驅動不了它。機密的第一步只在那裡提供,自動模式開著時不提供(那一行會說要先關掉)。每次按下都會記錄它的入口(via=pi、prompt、run-pane 或 gui),絕不記文字;控制 socket 的 tower-memory 只從 pi、常駐提示與執行的窗格接受 via,GUI 的路由永遠是 gui。

記憶檔案是老闆的文字,所以塔台服務關閉或它的分頁關閉時,它會保留;要清空它,用 /boss memory delete 或在面板裡刪除它的項目。

flowchart TD
    m["給這個 server 所跑塔台的<br/>Straw Link 訊息"] --> i["在 link/inbox 等待"]
    i --> s["每台裝置一個 run,<br/>最多 5 則訊息"]
    s --> p["在常駐 pane 旁切出<br/>一個新的 pi"]
    p --> t["以寄件者的<br/>信任層級工作"]

送給 server 所執行塔台的 Straw Link 訊息,由隊友 run處理:在塔台常駐 pane 右側分出的 pane 裡的一個新 pi(同一個分頁,沒有新分頁、沒有新工作區),以發送者的信任等級執行,沒有本機 context(規格,Ally runs)。這修訂了 Straw Link 的規則,即受管的裝置不會為遠端訊息開啟任何 session:它會開啟 session,但從不開工作區。

細節

受管的裝置可以把塔台的分頁加到它已有的工作區(絕不是工作區或基地,不聚焦,不往別的 pane 輸入任何東西);沒有這台電腦工作區的專案所收到的訊息,仍在 link/inbox/ 等 7 天並發出一則通知,而 server 不為它開啟任何東西(Straw Link 的喚醒段落有這些規則)。

隊友 pane 是智慧體清單的一列,像子智慧體的一樣:server 在分出 pane 時於其上寫入 sb_role=ally 與四個顯示 token,並隨塔台 4 分鐘一次的發佈再寫一次(一個執行最久 20 分鐘,token 是 10 分鐘)。sb_order 是塔台的 key 再加一段(000.0 → 000.0.000,第二個隊友 run 是 000.0.001),所以這一列正好在塔台底下;sb_mark 是 [ally] 與裝置的標籤(截成 24 字元);sb_indent 在後面還有被標記的隊友 pane 時是 ├─,最後一個是 └─;sb_name 是 ally run。

細節

執行結束時,server 在關閉 pane 之前先清掉它們,而其他隊友 run 的 pane 往上移。沒有它們,這一列會掉到清單的最後,混在網不認識的智慧體之間,而它的角色 token 在 10 分鐘後失效。

只有在握有陌生者在重用 id 之下不可能有的事實時,才會標記、在其中啟動並關閉 pane(F10)。這個事實是分出 pane 時產生的終端機,記在執行記錄裡,所以 server 重啟後復原的執行也能證明它的 pane;記錄裡沒有指名任何終端機的復原執行,絕不只憑 id 綁定,分出 pane 的列表從未指名它的 pane 的存活執行也一樣(這個列表最多讀三次):在 Herdr 會列出終端機 id 的地方,這樣的執行在 Herdr 重啟後,不會憑 id 標記或關閉任何東西。

細節

等待忙碌 pane 的啟動會在每次重試前詢問它,並在執行已結束或 pane 在另一個終端機裡時停止;在不列出終端機 id 的 Herdr 上,一個位於未知終端機的 pane 照舊處理(證明只靠字形)。請求卡住的 pi 退出的按鍵(esc、/quit、enter)只送進已證明屬於該執行的 pane(隊友的終端機,本機 run的塔台座位);沒有證明就跳過,而以啟動時間檢查 pid 的訊號會結束該執行。

Herdr 重啟之後每個終端機都是新的,所以 server 絕不再碰舊的隊友 pane:不關閉、不標記或取消標記、不啟動或重試、不送按鍵。執行以 herdr-restart 結束(它的佇列與認領規則如前),而 pane 留給老闆去關;舊 id 之下的陌生者,一個 shell、老闆自己的 pi 或 claude,也同樣留著不動。一道閘門(typegate.ts 的 allyPaneGate)允許隊友 pane 的動作,而且只在 pane 仍列在它被分出時所在的終端機時。

Herdr 在舊的隊友 pane 裡接回的 pi 向 server 詢問它的角色時,會隨詢問送上它自己的 session 檔案,並說明它的 session 是否帶有隊友標記:隊友 run 在啟動時往自己的 session 寫入一個 straw-boss-ally 項目,而 pi 在每次接回時都保留它。

當那個檔案是某個隊友 run 記錄的 session 檔案(重啟之前與之後都看得到),或它回報標記時,回答是 quit;標記才是持久的,因為執行記錄會被修剪(每個工作區保留最新 50 筆),而在它的執行很久之後第二次被接回的 session,否則會以老闆的等級、帶著隊友的文字在它的 context 裡,不受約束地執行。那個 pane 裡任何其他 pi,包括老闆自己的 pi 或 claude,以及兩者都不送的 pi(較舊的用戶端),回答是 none,並作為老闆的一般 pi 執行。

server 採信用戶端對標記的說法,因為這給出的比什麼都不給還少:quit 只是讓那個 pi 自己關閉,沒有任何認領、角色或 pane 動作隨之而來。兩種情況下 server 都不碰 pane,也不送按鍵:接回的隊友 session 自己離開,所以隊友的訊息絕不會以老闆的等級執行。自己開啟隊友 run session 的老闆 pi(/resume、/fork)帶有標記,並以同樣的方式被 quit。

持有者把收件匣項目留在原處;server 依發送者列出等待中的訊息,並為該裝置啟動一個執行,帶最多 5 則,最舊的在前(執行期間來的訊息等下一個執行)。裝置保有它的執行,直到執行結束把它的訊息交還持有者(在那一刻列出的訊息不會交給第二個執行),而 server 記住十分鐘內交還了哪些訊息:持有者隨後片刻仍列出的訊息,絕不會再被收進來。

細節

執行在分出 pane 之前再讀一次它的訊息,而一則都沒剩的就什麼都不啟動(VM 複查 A6)。每個工作區在它的本機 run旁邊有一個隊友 run,並且計入 towerRuns.max。離開名冊的發送者,它的訊息被丟棄,什麼都不啟動;在執行期間離開的,執行以 quit 結束(server 在每個監看 tick 與每次工具呼叫時都檢查),並在此期間拒絕它的工具呼叫。

執行以發送者的等級認領,來自 link.trust 或無人值守的政策;執行的 hook 再讀一次政策,取較低者。整個 session 是釘在那個發送者身上的一個遠端輪次:和塔台的遠端輪次相同的允許清單、拒絕清單、閘門與 forward 確認(src/link/hooks.ts)。受閘門管的呼叫經 server 詢問老闆(執行的 pi 顯示對話框;在 forward 時,確認送到發送者的裝置,它的答案在輪詢時回來),而 server 只在閘門為那個 call id 記錄了對應該摘要引數的 yes 時,才執行隊友 run 受閘門管的工具呼叫,且只一次。在 dispatch,閘門是關的。

細節

server 把執行限制在它被認領的等級與裝置現在的等級兩者較低的那個(降低信任會對執行中的執行生效;提高則不會把它抬高)。沒有指名發送者的認領,或無法釘住該輪的 session,會讓執行關閉。tower_done 在每個等級都是自由的。server 從它啟動起就記下老闆的 yes,不論 GUI 有沒有在聽,所以 GUI 停止時,終端機的回答仍然有效。

每個等級都自由的:boss_status、tower_list 與 board_read(兩者都限定在發送者的團隊:它專案的工作區與該團隊的看板)、送給發送者裝置的 tower_send(device 是必需的;任何其他裝置或目標都會被拒絕,不論有沒有 Allow)、tower_done,以及執行自己在專案受追蹤檔案之內的 read、grep、find 與 ls,如同遠端輪次。塔台自己工具中的其他任何東西(tower_reply、boss_open_main、boss_close_main),server 只在閘門記錄了 yes 時才執行(見下),除了 dispatch,在那裡它們不經確認就執行。

細節

call_tower、boss_server、link_request,以及對這台電腦塔台身分的 tower_reply,不論等級都被拒絕;close-tower 與其他控制指令對每個 run token 都被拒絕,需要 session 身分的連線持有者操作也一樣(taken、gui、request-open、request-run、以塔台身分送出;B3R-F003)。boss_open_main 帶 role: "train" 時,不論等級都被拒絕;帶 role: "integrator" 時,只接受為這個 run自己的裝置開啟的主智慧體所送出的車廂單(見下)。執行送給本機 session 的一切,都帶有由 server 蓋上的 origin=remote:<sender device id>。

本機 session 回給隊友 run 的 tower_reply(帶 reply: true)或它所開啟的主智慧體的內容,儲存給該裝置(ally-routes.json、ally-answers.json,24 小時),不會排給本機 run,並等待該裝置的下一個隊友 run;隊友 run 所開啟的主智慧體有自己的帳冊(ally-opened.json)。本機 brief 從不列出它們。隊友 run 開啟的主智慧體會持續把它的報告送給該裝置,而不只是第一個答案:它的路由在 40 條路由的上限之外,每個它轉送的報告都會更新它,而沉默 7 天的主智慧體會被放掉。

細節

開啟的路由每個裝置最多 8 條、總共 20 條(broker 不再列出的主智慧體的列先去,然後是最舊的),broker 不再列出的主智慧體的列,在它沉默 10 分鐘之後、下一個主智慧體開啟時被丟棄,而關閉主智慧體會丟掉它的列。裝置自己對它的主智慧體的 tower_reply(帶 reply: true)不會增加第二條路由,也不會取代已開啟的那條。即使伺服器不提供 GUI,被閘門管的呼叫的終端機 yes 也會到達 server:它在 hub.json 裡為橋接指名它的 session。

隊友 run 交給 boss_open_main 的請求,其發送者是裝置,不是這台電腦的 session,所以執行照原樣傳遞標頭的 id=(link:<device id>;link: 之後的短 id 也行,而重複的 link:link: 讀成一個前綴)。server 只在它指名執行被認領時對應的裝置時才接受它(id 來自認領,不是來自引數),並拒絕任何其他 link: 發送者、單純的標籤,或沒有 link: 的短 id 當成裝置:那些會像本機 run一樣,在存活的 session 之間查找。

細節

執行可以做的其他任何事都沒有改變:在 dispatch 以下,開啟仍需要老闆的確認,而離開團隊的裝置什麼都開不了。主智慧體的交接說明這個請求是對等端的,它用 call_tower 向塔台回報、無法聯繫發送者,而它的報告會原樣離開這台電腦。一直為開啟的主智慧體而設的路由(ally-routes.json)隨後做轉送:每個報告都儲存為給該裝置的答案,從不排給本機 run,並立即喚醒該裝置的一個執行,由它用 tower_send 送出。

報告只由它為之開啟的那個裝置的執行讀取,別的不行,而且在那個裝置不再是成員時被丟棄,不開啟任何執行。當開啟主智慧體的執行恰好帶著一個請求(asks=<id>)時,路由保留它,報告的標頭再次顯示它,而執行用 inReplyTo 回答它,所以報告到達另一台電腦上提出請求的主智慧體;執行自己已經回答的請求不被保留,請求在執行回答一次之後就用盡(之後的報告不帶),而角色 prompt 告訴執行,把「進行中」的備註與任何進度報告不帶 inReplyTo 地送出,並且只用主智慧體的最終報告來回答請求。

隊友 run 對為另一個裝置開啟的主智慧體所做的 tower_reply,不論來自哪個工作區都被拒絕(主智慧體自己的 sb_opened_by pane token 與路由表是兩個見證,任何一個指名另一個裝置就拒絕這次寫入,所以本機行程設的 token 絕不會蓋過路由表),而且在詢問老闆的 Allow 之前就拒絕,所以不會為它花掉任何 Allow:那個主智慧體的報告留給它為之開啟的裝置。

拒絕不重複名稱;不存在或有歧義的目標在那裡不會被拒絕,而是像存在的一樣繼續到閘門,所以 dispatch 以下的執行被告知的,對不存在的 session 與存在的 session 是一樣的(閘門的確認,不花 Allow),只有為另一個裝置所持的主智慧體會被區分出來。開啟的列保留主智慧體的 pane,而只有當 broker 與 Herdr 都不再顯示該主智慧體時,才當成已死而丟棄。當 session 有好幾列時,先讀開啟的路由。執行已回答的請求集合在 24 小時後被遺忘。執行裡有好幾個請求時,一個都不保留。

為裝置開啟的主智慧體在這台電腦上工作,碰到有 integration 的 app,最後會用 call_tower 的 why=shared 送出車廂單,上面的路由把它交給該裝置的下一個隊友 run。那個執行通往專案整合員的門只有一道,和本機 run的一樣:boss_open_main 帶 role: "integrator"、車廂單,並以那個主智慧體作為 sender。

細節

server 只接受這個 run收下的、由為它自己的裝置開啟的主智慧體送出的 call_tower(why=shared)(路由表這樣記載,主智慧體的 sb_opened_by token 若指名裝置,也必須一致),其餘一律在閘門之前拒絕,所以不會消耗 Allow:執行自己編的車廂單、只被寫過訊息的 session 的車廂單、開啟整合員的請求。

在 dispatch 以下,開啟仍需要老闆的確認,而確認顯示的是呼叫的引數:所以請求必須是車廂單送來時的原樣(行尾與行末空白不計),別的文字在閘門之前就被拒絕;老闆允許的就是被轉發的那段文字。工作區的 checkout 必須在這個 run的團隊於這台電腦綁定的專案之內。整合員由塔台自己的開啟器開啟,是塔台自己的主智慧體:它沒有隊友路由,所以它的報告和它的火車都交給本機 run,也不算進裝置的那一個主智慧體或工作區的兩個。

轉發除了 fwd 與 fwd-from 之外還帶 origin=remote:<device id>,它的 fwd 是答案 id 的摘要,因為答案沒有事件 id。這節車廂的協調者是裝置的主智慧體,所以 sb server 依 session id 把車廂通知送給它,它的報告照舊送達裝置。一個裝置仍然只有一個主智慧體:要求隊友 run 再開第二個來當整合員,照舊被拒絕(這次修正要解決的 2026-11-10 案例)。

Brief 與對話串。 執行的角色段落是 prompts/ally-run.md,加上 server 建的 brief(誰、哪個等級、發送者能看到工作區的什麼、這個裝置的上一份交接筆記、執行帶著的請求、裝置對話串的最後六列,以及它的工具;至多 4,000 字元)。對話串儲存(link/threads/<device id>.jsonl,模式 600)保留兩個方向,每個裝置最後 20 則訊息,沒有比 7 天更舊的,並隨裝置的名冊項目一起刪除。它靜態保存著對等端的文字。

裝置的答案從不進入塔台 run,所以為本機 session 把工作送給裝置的塔台,必須發出一個請求,否則這裡沒有人拿得到答案。帶 device 與 reply: true 而沒有 for 的 tower_send,取請求者本身:在執行取走的事件之中,向該執行送出期望答案的 call_tower(reply=expected,來自程式寫的標頭)的那一個 session,當它是存活的主智慧體時(自 0.58.2 起是這台電腦上任何工作區的,不只這個塔台的;見下);結果會這樣說(「The answer will come to

directly (the main that called you for this)」)。

細節

兩個請求者、本身是成員或已消失的請求者、不期望答案的呼叫、老闆事件,以及 reply: false,都不綁定任何東西;帶 reply: true 時,結果會說沒有綁定任何 session(帶 reply: false 則什麼都不加),而答案到達隊友 run,所以塔台不承諾它做不到的轉送。for 仍然優先;它指名工作區的一個主智慧體,或這台電腦上另一個工作區、向這個 run送出期望答案的 call_tower 的主智慧體。

來自另一個工作區的請求者或 for 只由那個呼叫綁定:server 從同樣的事件讀出請求者,對照 broker 與 Herdr 的 pane token(sb_role=main)檢查它,並對這個 run裡沒有呼叫過這個塔台、在這個工作區之外的 session 拒絕 for(這個工作區的主智慧體不需要呼叫)。請求者只依 session id 查找,絕不依名稱。請求記在塔台的工作區底下、該 session id 之下,而持有者依 id 把答案提供給該 session,所以投遞中沒有任何東西依賴工作區相符。請求只能用一次,24 小時,綁定被詢問的裝置,並在裝置被撤銷時隨它一起丟棄,如前。

caps 是每個持有者在每次串流開始、它的 build 改變時,以及每小時送出的全團隊訊息:它接受的功能(tower.awaitsReply、tower-reply);超過 2 小時的視為沒有。帶 for 的 tower_send(見上)對 caps 裡沒有 tower.awaitsReply 的裝置送出一般的 4 欄位 tower,對有的則送出以請求 id 為第五欄位的 tower;工具的回答說明是哪一種。帶請求 id 的訊息在它的標頭裡顯示 asks=<id>,而隊友 run 用 tower_send({ device, text, inReplyTo }) 回答它;不是它自己請求之一的 inReplyTo 會被拒絕。

細節

答案以 tower-reply 種類送給 caps 列出它的裝置,否則送一般的 tower(在 relay 以 unknown-kind 拒絕這個種類時也是;每個裝置每小時記錄一次)。提出請求的電腦只對來自那個裝置、開啟且未使用的請求,才把 tower-reply 當成回覆,關閉該請求,並透過 straw-boss/link 把它提供給提出請求的主智慧體,後者持有它直到閒置,再把它以帶 triggerTurn 的 straw-link 訊息放進它的 session,在經過驗證的標頭底下加上 kind=link_reply 與 for=<tower>。那個主智慧體隨後以回覆裝置的等級執行一個遠端輪次。

已經消失、或在 5 分鐘內沒有取走該提供的主智慧體,會得到隊友 run,其 brief 說明是哪個主智慧體提出請求;任何其他回覆也一樣。對較舊的 build 不會丟掉任何東西:請求以一般 tower 回來並得到隊友 run。

boss_server 與重啟限制。 隊友 run 的 boss_server 依它的種類被拒絕;對任何執行,server 自己守著每分鐘一次重啟的限制,所以兩個智慧體同時要求也不可能都通過。

控制者的工作隨它的 dispatch 結束

Section titled “控制者的工作隨它的 dispatch 結束”

在 dispatch 認領的隊友 run,在它的裝置離開團隊或降到 dispatch 以下時(連續兩個 tick,而且只在持有者能讀團隊名冊時),由監看者 quit,原因寫在結束通知裡。隊友 run 在 dispatch 開啟的主智慧體(路由保留執行的等級;沒有等級的列算作 dispatch)由 revoke.ts 每 15 秒對照名冊與政策檢查,裝置已不在或低於 dispatch 的主智慧體,會像 boss_close_main 那樣被要求 quit,忙碌時再次被要求,並在它 quit 時連同它的路由一起放掉;團隊的管理員會被告知一次它仍在執行、一次它已結束(規格)。

細節

當機與限制沿用本機 run的(閒置下來、20 分鐘、10 分鐘內三次失敗暫停服務),依裝置分別計算。

線路格式。 caps、5 欄位的 tower 與 tower-reply 在 src/link/kinds.ts 與 tests/link/vectors.json 裡(Straw Link 規格的 Message kinds 表有這些列);straw-relay 帶有同樣兩列與一份向量的副本,並先重新部署。