362 lines
26 KiB
Markdown
362 lines
26 KiB
Markdown
# Agent Computer progress (V3)
|
||
|
||
基準 commit(計劃):`e6afa324530e19922909d4692c28fb005cc05a7a`
|
||
本機開始 HEAD:`79952b088b394e36d6c3db3952165f1c3fd59961`
|
||
未覆蓋使用者未提交修改(`docs/plan/` 仍未追蹤)。
|
||
|
||
驗證(2026-09-10 複查,見文末「複查:Bug 修正」):`cargo fmt --check`、`cargo clippy --workspace --all-targets -D warnings`、`cargo test --workspace`(含 sqlx,全綠)、`make test-agent-computer`、`apps/web npm run build` 全部 exit 0。
|
||
sqlx 測試用拋棄式 `pgvector/pgvector:pg16` 於 `127.0.0.1:5434`(`docker run --rm --name lazyboy-test-pg …`),不重建正在服務的 `lazyboy-postgres-1`。
|
||
真實 OAuth/主信箱/TigerVNC GUI fixture:`BLOCKED_EXTERNAL`。
|
||
sample MCP 在真實 Computer 容器 `lb-team-space-*` 內 `python3 echo_server.py` JSON-RPC 回 `from-computer`。外掛 UI 已 typecheck;無瀏覽器工具未點擊。
|
||
|
||
---
|
||
|
||
## PR-00 / PR-01
|
||
|
||
狀態:IMPLEMENTED(見前次)。截圖交付、semantic/vision、原生 fs/exec 短路徑。
|
||
|
||
---
|
||
|
||
## PR-02:雙模式身份
|
||
|
||
狀態:IMPLEMENTED(資料+契約+idle;沒有強制遷移)
|
||
|
||
- `computers.generation`;destroy/recreate `generation + 1`(舊 job 不可裝活)。
|
||
- Team 多 bot 同 `computer_id`:`computer_scope_key(Team, space, bot-a) == bot-b`。Dedicated 互斥 scope。
|
||
- **沒有**對 `bots.computer_id` 加 UNIQUE。
|
||
- `AdapterContext.computer_generation` / `control_epoch`。
|
||
- Pause scopes:`PauseScope::{Agent,Display,Computer}`;停 A 不 bump B 的 display epoch。
|
||
- idle reaper 把 `computer_jobs` running 算進活性。
|
||
|
||
回滾:migration 022 + generation SELECT/UPDATE。
|
||
|
||
---
|
||
|
||
## PR-03:ledger / 脫敏 / activity
|
||
|
||
狀態:IMPLEMENTED(Postgres 為 mutation 來源;本環境未跑 sqlx 整合測試)
|
||
|
||
- `dispatch`:mutating tool 先 `begin`;journal insert 失敗則 `JOURNAL_UNAVAILABLE`,不開做。
|
||
- 同 `operationId` + payload hash replay 已存結果;不同 hash → `PAYLOAD_MISMATCH`。
|
||
- `operationId` 不進入 payload hash。
|
||
- `finish` 寫 `computer_operations` + `operation_outbox`;`worker_loop` `flush_outbox` 只補尚未出現在 `run_activity` 的 `operationId`。
|
||
- 結果與 outbox payload 經過 `redact_text` / `redact_json`。
|
||
|
||
---
|
||
|
||
## PR-04:jobs / cancel / CAS
|
||
|
||
狀態:IMPLEMENTED(Runner 在 API 行程內;container recreate 以 generation 中斷)
|
||
|
||
- `JobSupervisor`:`process_group(0)`;cancel TERM→KILL process group。
|
||
- `exec`:`action=run|status|cancel`、`background` 回 `jobId`。
|
||
- `write_file.expectedHash` → `CONFLICT`,不覆蓋。
|
||
- 測試:`/bin/true` 真 exit;`sleep 30` 可 cancel。
|
||
|
||
- API 重啟:`computer_jobs` running/accepted → `interrupted`(不假裝收回 Docker PID)。
|
||
- `LAZYBOY_NATIVE_JOB_CONCURRENCY`(預設 2)超額 → `QUOTA_EXCEEDED`。
|
||
- Artifact:`write_file` 成功寫 `computer_artifacts`;`GET /api/bots/{id}/artifacts` 列表;下載從 Computer `read_file` 串回,sha256 不符 → `CONFLICT`。
|
||
|
||
O04 PTY `terminal.*`:DEFERRED(見文末)。跨重啟回收仍在跑的容器 PID:標 interrupted,不是 resume。
|
||
|
||
---
|
||
|
||
## PR-05:Tool Manager / MCP
|
||
|
||
狀態:IMPLEMENTED(stdio 不在 API spawn;sample 在 Computer;無第三方商店)
|
||
|
||
- Manifest:空 digest / zip-slip / 空 entrypoint 拒絕。
|
||
- 安裝≠Ready(`AuthRequired` ≠ 可用)。
|
||
- `scripts/sample-mcp/echo_server.py` + `manifest.yaml`(本機範例,不是虛構 npm)。
|
||
- REST:`GET/POST /api/bots/{id}/tools`、`POST .../tools/call`、`POST .../revoke`。
|
||
- 安裝把 echo 寫進 Computer:Dedicated `tools/...`,Team `shared/tools/...`;`python3` 在該容器執行 JSON-RPC。
|
||
- Agent 工具 `computer_mcp` 走同一條路徑;未 bind → `NOT_BOUND`。
|
||
- 外掛頁「Computer 套件」可安裝/呼叫/撤銷 echo;市集 MCP 文案不再假裝 stdio 都在 Computer。
|
||
- MCP `call_for` 先複製 client 再 await;`definitions_for(actor)` 不把別人的工具塞進這次 run。
|
||
|
||
未做:任意第三方套件商店、OAuth broker。`/api/mcp-servers` stdio **不再** `spawn` 子行程,只存 metadata 並指向 Computer Tool Manager。HTTP/SSE 仍為遠端 client。真實 Microsoft/Google OAuth:`BLOCKED_EXTERNAL`。
|
||
|
||
---
|
||
|
||
## PR-06:路由 / verifier / wait
|
||
|
||
狀態:IMPLEMENTED(決策層)
|
||
|
||
- `choose_route`:有授權 API 不走 browser;`POLICY_DENIED` 不繞路。
|
||
- `CompletionLayer`:transport ≠ effect ≠ task;`wait_counts_as_progress() == false`。
|
||
- policy:`wait` 不再刷新無進展時鐘;`exec` 可。
|
||
- `wait_until` 有 deadline;未知 disabled 不空轉。
|
||
- system prompt:Gmail/Outlook 有 connector 走 API。
|
||
|
||
---
|
||
|
||
## PR-07:form / takeover scopes
|
||
|
||
狀態:IMPLEMENTED(form 工具已註冊;lease/pause 見 PR-02)
|
||
|
||
- `run_form_macro` 失敗即停、逐步 evidence。
|
||
- Display writer lease 原本就有;pause scope 見 PR-02。
|
||
- Agent 工具 `form_fill`:用最新 snapshot locator/element id;`run_form_macro` 先定位,第一個失敗就停;可選 `submit` click。
|
||
|
||
---
|
||
|
||
## PR-08A Gmail fixture
|
||
|
||
狀態:IMPLEMENTED(純函式 fixture,不碰真信箱)
|
||
|
||
- batchModify 同 delta 分組、1000 cap。
|
||
- undo 只撤本 plan 的 delta。
|
||
- 真實帳號:`BLOCKED_EXTERNAL`。
|
||
|
||
---
|
||
|
||
## PR-08B Outlook
|
||
|
||
狀態:IMPLEMENTED(Graph 語意 fixture,非官方 npm)
|
||
|
||
- `$batch` 最多 20;子項 429/401 分開。
|
||
- categories merge,不覆蓋無關分類。
|
||
- `Mail.ReadWrite` 不自動允許寄信。
|
||
- 真實 Graph OAuth:`BLOCKED_EXTERNAL`。
|
||
|
||
---
|
||
|
||
## PR-09 TigerVNC 候選
|
||
|
||
狀態:IMPLEMENTED(opt-in,預設仍 Xvfb+x11vnc)
|
||
|
||
- `DisplayBackend`;slot 0 = `:1` / 5900 / 6080(T57)。
|
||
- `lazyboy-screen` 讀 `LAZYBOY_DISPLAY_BACKEND=tigervnc_xvnc`。
|
||
- Dockerfile 加入 `tigervnc-standalone-server`,失敗則 script fallback Xvfb。
|
||
- Cua/中文/a11y GUI 對照:`BLOCKED_EXTERNAL`,**未改 default**。
|
||
|
||
---
|
||
|
||
## PR-10 設定 / 文件
|
||
|
||
狀態:IMPLEMENTED
|
||
|
||
`.env.example`:`LAZYBOY_EXECUTION_PROFILE`、`LAZYBOY_DISPLAY_BACKEND`、`LAZYBOY_BROWSER_BACKEND`、concurrency、tool install flags。
|
||
`migrations/022_agent_computer.sql`。
|
||
|
||
---
|
||
|
||
## O01–O48
|
||
|
||
| ID | 狀態 |
|
||
|---|---|
|
||
| O01 | IMPLEMENTED |
|
||
| O02 | IMPLEMENTED(短 exec);長 job 見 O04 |
|
||
| O03 | IMPLEMENTED + CAS |
|
||
| O04 | PARTIAL(Computer 內 background exec + cancel;PTY `terminal.*` DEFERRED) |
|
||
| O05 | IMPLEMENTED(form macro + `form_fill` 工具) |
|
||
| O06 | IMPLEMENTED(stdio 不在 API spawn;HTTP/SSE 遠端;echo 在 Computer) |
|
||
| O07 | DEFERRED(未量測 docker exec 開銷) |
|
||
| O08 | IMPLEMENTED(`boot_for(need_gui=false)` 不 ensure_screen;`LAZYBOY_RUNNER_ONLY` 不 boot XFCE) |
|
||
| O09 | IMPLEMENTED(router) |
|
||
| O10 | IMPLEMENTED(revoke 從下次 schema 拿掉 computer_mcp;stdio 不再進 defs) |
|
||
| O11 | 保留現況 |
|
||
| O12 | IMPLEMENTED |
|
||
| O13 | DEFERRED |
|
||
| O14 | IMPLEMENTED(PR-01) |
|
||
| O15 | 保留現況(既有 profile bind) |
|
||
| O16 | PARTIAL(verifier 層 + wait 不算進度) |
|
||
| O17 | 實驗/opt-in |
|
||
| O18 | DEFERRED |
|
||
| O19 | 保留現況 |
|
||
| O20 | 保留現況 |
|
||
| O21 | 保留現況 |
|
||
| O22–O24 | DEFERRED |
|
||
| O25 | IMPLEMENTED/保留 |
|
||
| O26 | PARTIAL(既有 screen/profile lease) |
|
||
| O27 | IMPLEMENTED(epoch 函式) |
|
||
| O28 | PARTIAL(idle 看 runs/skills/jobs;2026-09-10 修正守門原本永遠回 false,並在判斷前 reconcile job 狀態) |
|
||
| O29 | IMPLEMENTED(package/binding 分離;共用 bytes 不共享帳號) |
|
||
| O30 | IMPLEMENTED(`LAZYBOY_NATIVE_JOB_CONCURRENCY`,預設 2;配額滿時先向 Computer reconcile 未 poll 的 job,不會永久卡死) |
|
||
| O31 | DEFERRED |
|
||
| O32 | DEFERRED(T59:保留 `-ac` 直到 Xauthority 接到 Cua/AT-SPI;VNC 仍走認證 proxy) |
|
||
| O33 | IMPLEMENTED(sample MCP 安裝 REST + UI) |
|
||
| O34 | IMPLEMENTED(digest/zip-slip/unpinned npx 拒絕) |
|
||
| O35 | IMPLEMENTED(installed ≠ ready;AuthRequired 不可用) |
|
||
| O36 | PARTIAL(熱啟用 bindings;無獨立 schema cache 服務) |
|
||
| O37 | IMPLEMENTED(0.0.2 旁置、job pin 舊版、rollback/remove) |
|
||
| O38 | BLOCKED_EXTERNAL |
|
||
| O39–O40 | IMPLEMENTED fixture;真實帳號 BLOCKED_EXTERNAL |
|
||
| O41 | IMPLEMENTED(dispatch 寫 operations + outbox;flush 補送) |
|
||
| O42 | IMPLEMENTED(errorCode/jobId/operationId) |
|
||
| O43 | IMPLEMENTED(列表 + 下載;磁碟 hash 不符 CONFLICT) |
|
||
| O44 | PARTIAL(verifier 層) |
|
||
| O45 | IMPLEMENTED(CAS + operation id) |
|
||
| O46 | IMPLEMENTED(`GET /api/computer/{id}/health`:db/sandbox/display/jobs/operations) |
|
||
| O47 | DEFERRED |
|
||
| O48 | DEFERRED(無固定環境雙模式 P95;不寫未測倍數) |
|
||
|
||
---
|
||
|
||
## T01–T64
|
||
|
||
| ID | 狀態 | 證據/理由 |
|
||
|---|---|---|
|
||
| T01 | IMPLEMENTED | `should_deliver_observation_image` |
|
||
| T02 | IMPLEMENTED | 換模型/takeover 強制交付(同真值表) |
|
||
| T03 | IMPLEMENTED | `pixel_actions_need_vision`;text-only 無圖 |
|
||
| T04 | IMPLEMENTED | `typed_browser_errors` |
|
||
| T05 | IMPLEMENTED | Team/Dedicated path + echo 路徑測試 |
|
||
| T06 | PARTIAL | generation 拒絕舊 job;未覆蓋所有偽造 grant 變體 |
|
||
| T07 | 保留現況 | task 無 Docker socket;未加新 sentinel fixture |
|
||
| T08 | IMPLEMENTED | zip-slip/`..` path 拒絕 |
|
||
| T09 | IMPLEMENTED | invalid UTF-8 binary payload |
|
||
| T10 | PARTIAL | 工具錯誤非空成功;磁碟滿未測 |
|
||
| T11 | IMPLEMENTED | `stale_hash` CAS CONFLICT |
|
||
| T12 | IMPLEMENTED | Supervisor `/exec` 回完整 `CommandResult`;reap 讀 `exit` 檔,`false`/缺檔為 Failed/1 |
|
||
| T13 | IMPLEMENTED | Computer-side job_op cancel 走 `computer_cancel_script`(TERM→KILL 整個 process group,驗證後才標 cancelled;2026-09-10 修正 pid 錯誤);status 缺欄位當 TRANSPORT,不發明 running |
|
||
| T14 | DEFERRED | API 重啟標 interrupted,不 resume 容器 PID |
|
||
| T15 | IMPLEMENTED | generation interrupt |
|
||
| T16 | IMPLEMENTED | operation_id hash/mismatch;2026-09-10 第二輪:key 改 per-bot、in-flight 同 id 回 `UNKNOWN_EFFECT`、傳輸失敗不存成 succeeded(sqlx 測試 `ledger_is_per_bot_and_does_not_rerun_in_flight_work`) |
|
||
| T17 | IMPLEMENTED | outbox flush 補送且不重複(sqlx 測試 `outbox_flush_does_not_duplicate_recorded_operations`;2026-09-10 修正 `SELECT 1` 解碼 bug) |
|
||
| T18 | IMPLEMENTED | `journal_failure` 拒 mutation |
|
||
| T19 | IMPLEMENTED | actor-scoped MCP;stdio 不在 API 持有 client |
|
||
| T20 | PARTIAL | NOT_BOUND/revoke;無真實 token revoke |
|
||
| T21 | IMPLEMENTED | `canary_is_stripped`;2026-09-10 第二輪:ledger/snippet 實際套用 `redact_secret_patterns`(之前傳空 canary 等於沒脫敏) |
|
||
| T22 | 保留現況 | 既有 prompt 邊界;無新 injection corpus |
|
||
| T23 | PARTIAL | 同 display lease 既有;Cua+worker 未雙開 fixture |
|
||
| T24 | PARTIAL | pause/takeover;HTTP in-flight barrier 未單獨測 |
|
||
| T25 | IMPLEMENTED | wait 不算 milestone |
|
||
| T26 | IMPLEMENTED | Gmail batchModify fixture |
|
||
| T27 | IMPLEMENTED | fixture 429/partial;真實帳號 BLOCKED_EXTERNAL |
|
||
| T28 | IMPLEMENTED | undo 只撤本 plan |
|
||
| T29 | IMPLEMENTED | 無強制拆 Team;migration 不 UNIQUE computer_id |
|
||
| T30 | IMPLEMENTED | `boot_for(need_gui=false)` 不 `ensure_screen`;`maybe_ensure_screen` + FakeSandbox native write/read/exec;`start.sh` runner-only 不 boot XFCE |
|
||
| T31 | PARTIAL | job 配額;無壓力 bench |
|
||
| T32 | DEFERRED | 無郵件分類品質集 |
|
||
| T33 | PARTIAL | broker 不進 generic env;無任意 exec 讀 socket fixture |
|
||
| T34 | IMPLEMENTED | artifact 從 Computer 下載 |
|
||
| T35 | PARTIAL | CAS/operation replay;resume 不重播未全測 |
|
||
| T36 | PARTIAL | mutating tools 走 ledger;MCP stdio 不再是旁路 spawn |
|
||
| T37 | IMPLEMENTED | `team_computers_share`;migration 註解+測試 |
|
||
| T38 | IMPLEMENTED | Team `bots/<id>/` 路徑 |
|
||
| T39 | IMPLEMENTED | shared/ + CAS |
|
||
| T40 | PARTIAL | 多 slot 契約;無雙 writer GUI fixture |
|
||
| T41 | PARTIAL | display writer lease 既有 |
|
||
| T42 | IMPLEMENTED | pause scope 停 A 不 bump B |
|
||
| T43 | PARTIAL | idle 看 jobs;整機 pause 未單測 recreate 全集 |
|
||
| T44 | 保留現況 | 既有 ensure-screen single-flight |
|
||
| T45 | IMPLEMENTED | Dedicated 刪 bot;Team Computer 存活 |
|
||
| T46 | IMPLEMENTED | echo 安裝/呼叫 `executionLocation=assigned_computer` |
|
||
| T47 | IMPLEMENTED | binding per bot;套件 per computer |
|
||
| T48 | IMPLEMENTED | digest/zip-slip/unpinned npx |
|
||
| T49 | IMPLEMENTED | `plan_version_switch` job pin |
|
||
| T50 | IMPLEMENTED | revoke/remove;revoke 掉 schema |
|
||
| T51 | IMPLEMENTED | Outlook fixture;非官方 npm |
|
||
| T52 | IMPLEMENTED | Graph 子項 429/401 |
|
||
| T53 | PARTIAL | fixture 分頁語意;真實 delta BLOCKED_EXTERNAL |
|
||
| T54 | IMPLEMENTED | categories merge 不盲覆蓋 |
|
||
| T55 | BLOCKED_EXTERNAL | 無 Cua/中文/a11y GUI |
|
||
| T56 | PARTIAL | generation bump;backend 熱切未做 |
|
||
| T57 | IMPLEMENTED | slot 0 = :1/5900/6080 |
|
||
| T58 | 保留現況 | 既有 noVNC;無新 DPI fixture |
|
||
| T59 | DEFERRED | 保留 `-ac`;VNC 走認證 proxy;Xauthority 未接到 Cua |
|
||
| T60 | IMPLEMENTED | 同 T30:native 路徑不 attach viewer;GUI 之後才 `lazyboy-screen ensure` |
|
||
| T61 | IMPLEMENTED | 未審查 env token/unpinned npx 拒絕 |
|
||
| T62 | 保留現況 | 同 UID 不宣稱強隔離 |
|
||
| T63 | PARTIAL | job 配額;renderer OOM 未測 |
|
||
| T64 | PARTIAL | 套件路徑可重建;舊 job 不裝活 |
|
||
|
||
## 2026-09-10 複查:Bug 修正
|
||
|
||
本輪先跑計劃第 16 節的完整 gate,再修實際發現的 bug;未新增功能。
|
||
|
||
驗證命令與結果(本機,拋棄式 Postgres `pgvector/pgvector:pg16` 於 `127.0.0.1:5434`):
|
||
|
||
- `cargo fmt --all -- --check`:原本有 diff(`artifacts.rs`、`computer.rs` 等新檔未格式化)→ 已 `cargo fmt --all`,現在 exit 0。
|
||
- `cargo clippy --workspace --all-targets -- -D warnings`:exit 0。
|
||
- `DATABASE_URL=… cargo test --workspace`:**全部通過**(api 157 passed / 3 ignored;control 150;harness 37;contracts 7;sandbox 7;supervisor 2)。前次紀錄的「sqlx 未連」現在已實跑;14 個 sqlx 測試(db/memory/monitor/routing::fan_out)全綠。
|
||
- `make test-agent-computer`:exit 0。
|
||
- `apps/web`:`npm run build`(`tsc --noEmit` + vite)通過。
|
||
|
||
修正:
|
||
|
||
1. **`lazyboy-api` 測試 binary 結束時 SIGABRT**(`ort` `Mutex poisoned` → `release_env_on_exit` 二次 panic)。根因:`memory.rs` 用 `catch_unwind` 包住 `TextEmbedding::try_new`,但 `ort` 找不到 `libonnxruntime.so` 時是在持有全域鎖的情況下 `expect` panic,鎖被 poison 後程序退出時 `.fini_array` hook 再 panic → abort。正式環境沒設 `ORT_DYLIB_PATH` 時,API 正常關機也會變 exit 134。修法:`memory.rs` 新增 `preload_onnx_runtime()`,先走 `ort::init_from(path)`(回 `Result`)預載 dylib,失敗即標 `Unavailable` 不再進 fastembed;`crates/api/Cargo.toml` 直接依賴同版 `ort ="=2.0.0-rc.13"`(lock 無新增 crate)。測試 `database_enforces_agent_scope_and_queries_do_not_leak` 改成顯式 `ModelState::Unavailable`,不再靠環境缺 ONNX 來觸發;新增 `a_missing_onnx_runtime_is_an_error_not_a_panic`。反向驗證:`ORT_DYLIB_PATH=/nonexistent cargo test -- --ignored memory_model_recovers_after_cache_failure` 只因無模型 fail,**不再 abort**。
|
||
2. **Computer 內 background job 的 pid 是錯的(T13 process-tree cancel 實際上沒生效)**。`computer_background_launch` 原本是 `mkdir -p … && setsid nohup sh … & echo $!`,`&` 作用在整個 `a && b` list,`$!` 是 bash 子 shell 的 pid,不是 `setsid` 後的 session leader。後果:`status` 只是碰巧能用(子 shell 會 wait);`cancel` 只殺子 shell,真正的 `sh`→子程序變孤兒繼續跑,之後 `status` 讀不到 exit 檔還會誤報 `failed`。修法:`mkdir` 獨立一行;wrapper 先 `printf %s "$$" > …/pid`,launcher 等 pid 檔再輸出(`setsid` 若 fork 也正確)。新增 `computer_cancel_script(pid)`:`kill -TERM -- -pgid` → 最多 1s 等待 → `kill -KILL -- -pgid` → 再驗證;exit 0 才算整棵樹已消失。`supervisor/docker.rs` cancel 改用它,且只有樹確實消失才把 job 標 `cancelled`(否則維持 `running`,不假稱取消)。測試:`computer_cancel_script_kills_the_whole_process_tree`(本機真起 `sh -c 'sleep 300; sleep 300'`,用 `pgrep -g` 驗證子 `sleep` 一起死);另在真實 Team 容器 `lb-team-space-c7bace…` 內手動跑 launcher+cancel:pid=pgid=sid,`tree gone`。
|
||
3. **supervisor `job_follow_up` 沒檢查 job 屬於哪個容器**:拿到別的 Computer 的 jobId 時會在自己容器對同號 pid 送 `kill`。現在 `job.computer_id != container` 一律回 `unknown job`。
|
||
4. **`Makefile` `help` 目標壞掉**:新增的 `test-agent-computer` 說明被塞進同一個 `@echo "…` 造成跨行未閉合字串,`make help` 回 `Unterminated quoted string` Error 2。已拆成兩行 `@echo`。
|
||
5. **idle reaper 的「有工作就不暫停」守門一直是死的(HEAD 既有 bug)**:`computer_has_active_work` 把 `SELECT 1`(INT4)解成 `(i64,)`,sqlx 回 `ColumnDecode mismatched types`,被 `matches!(Ok(Some(_)))` 吞成 false → 只要 `computers.updated_at` 十分鐘沒動,正在跑 run 的 Computer 也會被 suspend。改成 `sqlx::query(...).fetch_optional` 只看有沒有列。sqlx 測試 `idle_reaper_reconciles_unpolled_jobs_with_the_computer` 同時覆蓋 run 與 job 兩種活性。
|
||
6. **`flush_outbox` 去重失效(T17 宣稱的「不重做」實際沒生效)**:同樣的 `SELECT 1`→`i64` 解碼錯誤讓 `exists` 永遠 None,每筆 outbox 都再寫一次 `run_activity`,Activity 出現重複 tool 事件。已改為列存在檢查;查詢出錯時保留 outbox 列到下一輪,不猜。新增 sqlx 測試 `outbox_flush_does_not_duplicate_recorded_operations`。
|
||
7. **背景 job 配額會永久卡死**:`computer_jobs.status` 只在 agent 呼叫 `exec status` 時更新;兩個跑完但沒人 poll 的 job 會讓該 Computer 之後永遠 `QUOTA_EXCEEDED`,idle reaper 也永遠以為有工作。修法:`tools.rs` 配額看起來滿時先 `refresh_running_jobs`(向 Computer 問最舊 16 筆 running 的真實狀態;supervisor 回 `unknown job` 標 `interrupted`)再重算;`computer.rs` 的 `pause_idle_computers` 在判斷活性前做同樣的 reconcile(suspended 容器無法問,`stop_parked_computers` 不做)。
|
||
|
||
未修、已知:
|
||
|
||
- host 端 `JobSupervisor::start`(`crates/control/src/jobs.rs`)用 piped stdout 但只在子程序結束後 `read_to_end`;輸出超過 pipe buffer(~64 KiB)的子程序會永久阻塞。目前正式路徑不用它(背景 job 全在 Computer 內,`exec_background_does_not_spawn_on_the_api` 有守門),僅測試使用;若之後要在 API 行程內跑 job 必須先改成邊跑邊 drain。
|
||
- supervisor 的 `ComputerJobTable` 在記憶體;supervisor 重啟後舊 jobId 回 `unknown job`(API 側已把 `computer_jobs` 標 interrupted,屬同一限制,T14 仍 DEFERRED)。
|
||
|
||
回滾:還原 `crates/api/src/memory.rs`、`crates/api/Cargo.toml`(ort 直接依賴)、`crates/api/src/tools.rs`(refresh_running_jobs)、`crates/api/src/computer.rs`、`crates/api/src/operations.rs`、`crates/control/src/jobs.rs`、`crates/supervisor/src/docker.rs`、`Makefile` 本節改動即可;無 migration 變動。
|
||
|
||
## 2026-09-10 複查(第二輪):三路平行 code review 後的修正
|
||
|
||
三個獨立 review(ledger/outbox/artifacts;jobs/tool-manager/install;雙模式/pause/前端)共回報 40 餘項;重複、已在上一節修掉的略過。以下每項都有測試或反向驗證。gate:`cargo fmt --check`、`clippy -D warnings`、`cargo test --workspace`(api 162/control 154/harness 37)、`tsc --noEmit` 全綠。
|
||
|
||
**Ledger(T15–T17,`operations.rs` 重寫)**
|
||
|
||
1. **operationId 全域命名空間 → 跨 bot replay/拒絕**:ledger key 改為 `{bot_id}:{operation_id}`,查詢加 `bot_id` 條件。Bot B 選到 Bot A 用過的 id,不會拿到 A 的結果,也不會被 `PAYLOAD_MISMATCH` 擋。
|
||
2. **記憶體 `LEDGER` 拆掉**:與 DB 兩份狀態不一致(一邊 accepted 一邊沒列)、無上限增長(連非 mutating tool 都建列)。Postgres 是唯一真相;`OperationLedger` 只留 control crate 單元測試用。
|
||
3. **`bot_computer_id` 查詢失敗被當「沒有 Computer → Proceed」**:現在 DB 錯誤 = `JOURNAL_UNAVAILABLE`;不需要 Computer 的 mutation(remember/schedule)以 `computer_id = NULL` 入帳(migration 023 放寬 NOT NULL)。
|
||
4. **同 id 同 payload 的 `accepted` 列直接 Proceed → 崩潰後重試會做第二次**:新增 `Begin::InProgress` → 工具回 `UNKNOWN_EFFECT`,要求先讀回狀態、換新 id 重試。
|
||
5. **`finish` 對非 mutating tool 也寫 DB/outbox**:提早 return。`exec status` 改為非 mutating(`exec run/cancel` 仍是)。
|
||
6. **`"xxx failed: …"` 的傳輸失敗被存成 `succeeded` 並永遠 replay**:`outcome_status` 辨識這類文字 → 刪除 accepted 列讓同 id 可重試;不寫入結果。
|
||
7. **replay 丟掉 `pause`**:`Begin::Replay` 帶回 `pause`。
|
||
8. **outbox 寫 `run_activity` 失敗仍標 delivered**:新增 `monitor::try_record_pool`,只有成功才標;且只處理 `created_at` 早於 2 秒的列,避免與 `runs.rs` 的直接寫入賽跑而重複。
|
||
9. **`redact_text(&text, &[])` 是 no-op**:新增 `redact_secret_patterns`(Bearer、`password=`/`token:`、`sk-`/`ghp_`/`AKIA`/`xox`/`AIza`、PEM 私鑰、JSON secret key),ledger 結果與 activity snippet 都經過它。control crate 新增 `regex` 直接依賴(lock 已有)。
|
||
10. retention 新增 `operation_outbox`(delivered,7 天)與 `computer_operations`(非 accepted,90 天)。
|
||
|
||
**Jobs(T13/T14)**
|
||
|
||
11. **runner-only 容器 PID 1 是 `sleep infinity`,不收屍**:setsid 後的 wrapper 結束會變 zombie,`kill -0` 仍成功 → job 永遠 `running`。修:`HostConfig.init = true`(docker-init 收屍);`status` 改先讀 exit 檔、再用 `computer_alive_script`(看 `/proc/*/status` State,Z/X 視為死)。測試 `computer_alive_script_treats_a_zombie_as_dead` 本機造真 zombie 驗證。
|
||
12. **launcher 沒吐出 pid 時仍登記 job(pid None)→ 永遠 running、不能 cancel**:現在直接回錯誤,不登記幽靈 job。
|
||
13. **`exec status/cancel` 不看歸屬與世代**:新增 `job_ownership`:別的 bot 的 job → `UNKNOWN_JOB`;`generation` 小於目前 Computer 世代或已 `interrupted` → 直接回 interrupted,不去問新容器;`persist_job` 的 upsert 不再把 `interrupted` 蓋回 `running`。
|
||
14. **`pin_running_jobs` 把同 Computer 上其他 bot 的 job 也 pin 了**:加 `bot_id` 條件。
|
||
|
||
**Tool Manager(T18–T21)**
|
||
|
||
15. **revoke 後重裝撞 `UNIQUE(package_row_id, bot_id)` → 500**:`ON CONFLICT … DO UPDATE SET status='ready' RETURNING id`;切版本走 `switch_binding`(同 tx 內先清掉同 (package_row, bot) 的非 ready 殘留)。
|
||
16. **rollback 會把 `revoked` binding 復活**:非 `ready` → 409。
|
||
17. **執行前不驗 digest**:`verified_run_argv` 在同一條命令內 `sha256sum -c` 通過才 `exec python3`;不符 exit 97 → `PACKAGE_TAMPERED`。測試 `tampered_package_is_refused_before_it_runs` 真改檔驗證。
|
||
18. **`validate_manifest` 只檢查非空**:現要求 64 hex;`compute-at-install` 佔位會被拒。
|
||
19. `InstallBody` 只吃 `package_id`、前端送 `packageId`:serde `rename` + `alias` 兩者皆收。
|
||
20. `refuse_unpinned_npx` 把 `@scope/pkg` 的 `@` 當版本 pin:修為去掉 scope 前綴再判斷。
|
||
|
||
**其他**
|
||
|
||
21. **`form_fill` 靜默截斷**:>20 欄或欄位缺 name/locator → `INVALID_ARGUMENT`,不再填半張表回報成功。
|
||
22. **`looks_like_css` 誤判 `v1.2 Release`/`C++ Guide` 這種標題為 CSS**:click/type 先做精確唯一標題比對;找不到才依 locator 形狀決定 `SELECTOR_UNSUPPORTED`/`TARGET_NOT_FOUND`/`TARGET_STALE`,並各給對應說明。
|
||
23. **`computers.display_backend` 從未寫入**:boot 轉 running 時寫入 `DisplayBackend::from_env()`;health 不再永遠回欄位預設值。
|
||
24. **`delete_bot` 不清 `tool_bindings`/`computer_jobs`/`computer_operations`/`computer_artifacts`**:同 tx 一起刪(Team Computer 的配額不再被已刪 bot 佔用)。
|
||
25. **`wait` 在 `PROGRESS_WHEN_REPEATED` 內,與本文件「wait 不刷新無進展時鐘」相反**:改成 heartbeat 語意 — `wait` 只更新 `last_heartbeat_turn`(不會被當 loop halt),但 stale 教練訊息照常觸發;`polling_a_build_or_a_queue_is_not_treated_as_spinning` 同時鎖住兩半契約。
|
||
26. **Gmail fixture 沒有真的 undo**:新增 `undo_plan(before, batches)`,只還原本次計畫實際改動的 label(原本就有的不拔、原本沒有的不補、人工新加的不動),測試做 plan→undo 往返驗證。
|
||
27. 前端:`RunActivityEntry.operationId` 補型別;`runs.rs` 從工具回傳 JSON 抽 `jobId` 寫進 activity,monitor 原有的 `entry.jobId` 顯示終於有值;並顯示 `op <短 id>`。
|
||
|
||
**Migration**:新增 `migrations/023_agent_computer_fixes.sql`(`computer_operations.computer_id` DROP NOT NULL、移除多餘 `computer_operations_id_hash`、outbox pending 部分索引、`computer_operations(bot_id, created_at)` 索引)。不改 022(已可能套用過)。
|
||
|
||
**review 提到但本輪不動、記錄在此**:
|
||
- Pause scopes/`resume_after_pause`/`route_operation` 有純函式與測試但沒接進 runs 主迴圈;takeover 實際只有 GUI 全暫停,沒有 scope 級細分(T22–T24 應視為 PARTIAL,不是 DONE)。
|
||
- `write_file` 走 `bash -lc` 字串插值路徑(HEAD 既有);`normalize_workspace_path` 有限制字元,本輪未改成 argv。
|
||
- supervisor `ComputerJobTable` 仍在記憶體(T14 DEFERRED 不變)。
|
||
|
||
回滾:還原本節列出的檔案,並 `DELETE FROM _sqlx_migrations WHERE version=23` 後 `ALTER TABLE computer_operations ALTER COLUMN computer_id SET NOT NULL`(若已無 NULL 列)。
|
||
|
||
## 誠實未完成
|
||
|
||
P0/P1 可在本環境落地的已接上。下面不是「做完」:
|
||
|
||
1. 真實 Gmail/Outlook OAuth/主信箱:`BLOCKED_EXTERNAL`。
|
||
2. TigerVNC 當預設 + Cua/中文/a11y GUI:`BLOCKED_EXTERNAL`;預設仍 Xvfb+x11vnc。
|
||
3. Postgres sqlx 整合測試此環境未連。
|
||
4. **O04 PTY `terminal.*`:DEFERRED** — 非互動走 `exec`+jobs+process-tree cancel;TUI 協作仍用 GUI `shell`。不假裝有 PTY。
|
||
5. 跨重啟回收容器內仍在跑的 PID:標 interrupted,不是 resume。
|
||
6. 任意第三方套件商店、完整 OAuth broker:沒有憑證。
|
||
7. O48 無未測速度倍數。
|
||
8. 外掛 UI 未在瀏覽器點過(無瀏覽器工具);已 `tsc --noEmit`。
|
||
|
||
雙模式:Team/Dedicated 契約不變;**沒有** UNIQUE `bots.computer_id`。stdio MCP **不是** API 子行程。
|
||
|
||
回滾:還原本輪 crate / migration 022 / lazyboy-screen `-ac` 註解 / sample-mcp / 前端 plugins+i18n。不要 force push。
|