Files

198 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运行手册
## 安装
```bash
git clone https://git.kappiee.com/kappiee/plane-agentd.git && cd plane-agentd
python3 -m pip install --user pyyaml # 唯一依赖;有 jsonschema 时用它校验,否则用内置子集校验器
cp config/agentd.example.yaml config/agentd.local.yaml # git-ignored;或用 AGENTD_CONFIG 指向别处
cli/agentd check-config # 校验 + 列出各终端 token 环境变量是否已注入 + cron 所需 --secret-env 参数
python3 -m unittest # 离线单测
```
调度器迁到不装 agent CLI 的 LXC:见 `docs/deploy-lxc.md`。
配置里只写 Vault 键名(`token_key`)。agentd 读同名环境变量,token 只由 `agent-access.py exec --secret-env KEY=KEY` 注入进程内存;不要 export 到 shell、不要写文件。agentd 起的所有子进程(本机 runner/执行器、win10 中继脚本)拿到的环境都去掉了全部 `token_key` 与 `webhook_secret_key` 变量。
## 运行
**调度器 2026-10-21(PAD-11)起跑在 LXC `agentd-01`**(root,仓库 `/opt/plane-agentd`),部署细节见 `docs/deploy-lxc.md`。任何时候只能有一个调度器排程。
单次 tick(在 LXC 上):
```bash
cd /root/.local/share/server-access-client && python3 -I agent-access.py \
--config https://nas.tail9531b.ts.net:8443/identity/agent.json exec \
--secret-env PLANE_OREGON_API_TOKEN=PLANE_OREGON_API_TOKEN \
--secret-env PLANE_WINDOW10_API_TOKEN=PLANE_WINDOW10_API_TOKEN \
--secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY \
--timeout 900 -- python3 /opt/plane-agentd/cli/agentd tick --once
```
`AGENTD_SSH_KEY` 是 direct-ssh 终端的专用密钥(PAD-12,design §9)。手工运行时不加这个参数不会出错,只是直连终端整次 tick 都走 Warpgate 兜底(慢),`agentd.log` 里会有一行 `direct-ssh unavailable in this process`。但加了参数而 Vault 里没有这个键,agent-access.py 会以 `access-hub: credential_unavailable` 直接失败,agentd 根本不运行(它没有可选密钥);cron 脚本遇到这种情况会去掉这个参数再跑一次,手工运行则要自己去掉。
crontab(LXC root,每 2 分钟):
```
*/2 * * * * /opt/plane-agentd/cron/agentd-tick-lxc.sh
```
`cron/agentd-tick-lxc.sh` = 拿 `/var/lib/plane-agentd/cron.lock`(`flock -n -E 75`,撞锁退出 75)后执行 `timeout 900 python3 -I agent-access.py --config <identity> exec --secret-env PLANE_OREGON_API_TOKEN=… --secret-env PLANE_WINDOW10_API_TOKEN=… --secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY --timeout 850 -- python3 /opt/plane-agentd/cli/agentd --config /opt/plane-agentd/config/agentd.yaml tick --once`,输出追加到 `/var/log/plane-agentd/cron.log`(stderr 排在 stdout 之后)。如果这次 exec 只报了 `access-hub: credential_unavailable`/`credential_invalid`(退出 1,agentd 没运行),脚本在 cron.log 记一行 `AGENTD_SSH_KEY not available from Vault: tick repeated without it …`,去掉 `AGENTD_SSH_KEY` 再跑一次:直连终端走兜底,派单不中断。空闲 tick 不打印任何东西(cron 日志为空是正常的),活着与否看 `heartbeat.json` 的 `at`;`skip: another tick holds …` 表示与手工 tick 撞锁,无害。
实际配置 `config/agentd.yaml`(已提交,不含密钥):`state_dir: /var/lib/plane-agentd`、`log_dir: /var/log/plane-agentd`,所以下表的 `state/…` 在 LXC 上是 `/var/lib/plane-agentd/…`,`agentd.log` 在 `/var/log/plane-agentd/`。改配置/代码 = 在 oregon 提交并推送,然后 LXC 上 `cd /opt/plane-agentd && git pull`(下一个 tick 生效)。
oregon 上的 `cli/agentd` 只当 CLI 用(new/show/comment/state/assign,`smoke … --watch`);**不要在 oregon 上跑 `tick` 或不带 `--watch` 的 `smoke`**(那是第二个调度器)。
暂停一个终端(不再派新运行、也不探测,在途运行照常收尾):把它的 `concurrency` 设为 0。
常驻替代:`... exec ... -- cli/agentd tick --loop --interval 120`(vault 会话超时要够长,cron 更稳)。
执行器的二进制一律用终端 `bins` 写绝对路径(如 `claude: /home/ubuntu/.local/bin/claude`):cron 和 SSH 非登录 shell 的 PATH 都只有系统目录。
### 执行器二进制的解析(`bins` 或 PATH)
命令模板的首词(`codex`、`claude`…)若在该终端的 `bins` 里,替换为那里的绝对路径;否则**原样交给终端的 shell 按 PATH 查找**。没有任何其他探测或回退:
- oregon(2026-10-21 起 `warpgate-ssh`,os linux):agentd 经中继 `agent-access.py run --target oregon -- bash -c …` 在 oregon 上 `setsid nohup bash run.sh`,runner 的环境是 sshd 给 ubuntu 用户的非登录环境(PATH = 系统默认,没有 `~/.local/bin`),所以 `claude` 必须写进 `bins`。`transport: local`(runner 继承 agentd 进程环境,cron 下 PATH 只有 `/usr/bin:/bin`)仍可用于调度器与执行器同机的部署和测试。**示例配置的 oregon 列了 `codex` 执行器但没有 `bins.codex`,即假定 `codex` 在 PATH 上——目前 oregon 没装 codex**(`which codex` 为空),带 `exec:codex` 的 oregon 工作项会以 exit 127(`command not found`,类型=crash,非临时性)转 Waiting。要用就装 codex 并加 `bins: {codex: <绝对路径>}`,否则从 oregon 的 `executors` 去掉 codex。
- win10(warpgate-ssh):runner 是计划任务里的 `powershell.exe -File run.ps1`,以创建任务的用户(Administrator,交互登录)运行,PATH 取该用户的环境。示例配置用 `bins.codex` 写死 `codex.exe` 绝对路径;去掉它就等于假定 Administrator 的 PATH 里有 `codex`。
- `check-config` 不检查二进制是否存在;首次接入一个终端用 `agentd smoke <PROJECT> <terminal>` 验证。
人工操作(同样在 `exec --secret-env` 下):
```
cli/agentd new GWENT "T-010 标题" --file brief.md --assign window10 --labels model:astra,needs-game --module M1 --state Todo
cli/agentd show GWENT GW-12 3 # 状态/指派/标签 + 最近 3 条评论
cli/agentd comment GWENT 12 reply.md # 或 - 读 stdin
cli/agentd state GWENT 12 Todo # Plane 状态名或配置键(todo/review…)
cli/agentd assign GWENT 12 window10
cli/agentd smoke GWENT oregon --watch # 建一个要求回复 "[review] ok" 的工作项,等排程中的调度器跑完,PASS 则移到 Done
cli/agentd smoke GWENT win10 --labels model:sol --watch # --labels 给冒烟项加标签(路由到模型/档位)
cli/agentd ping win10 [--transport direct-ssh|warpgate-ssh] [-n 3] # 在终端上跑一条空命令并计时,每次调用一行
# (direct-ssh 需要 --secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY,不需要 Plane token)
# 不带 --watch = 本机自己 tick(只在调度器所在机器上、且它的 cron 停着时用)
```
CLI 以 `--as <terminal>` / `agentd.cli_terminal` / 第一个有 token 的终端的身份操作。
## Webhook(PAD-7:Plane 事件即时触发 tick)
cron 每 2 分钟一次的 tick 保留作兜底;另有常驻接收端,Plane 一有工作项/评论变化就立刻触发一次 tick。
- **服务**:LXC `plane-agentd-webhook.service`(单元文件在仓库 `systemd/`,`Restart=always`)= `agent-access.py … exec --secret-env PLANE_WEBHOOK_SECRET=PLANE_WEBHOOK_SECRET --timeout 315360000 -- python3 /opt/plane-agentd/cli/agentd webhook-serve --bind 10.77.10.249 --port 8790`。只听内网地址;密钥只在进程环境里,agentd 起的子进程(tick 脚本)环境里去掉了它。
- **端点**:`POST http://10.77.10.249:8790/plane`;`GET /healthz` → `{"ok": true, "pending", "queued", "running", "retrying_since", "ticks", "last": {rc, seconds, deliveries, at}}`。
- **Plane 侧**:工作区级 webhook(kappiee 工作区设置 → Webhooks,或 `tools/plane-webhook.py`),订阅"工作项"(`issue`)和"评论"(`issue_comment`)。Plane 会把所有项目的事件都发来,接收端按 `data.project` 只认配置里的项目。
- **签名**:头 `X-Plane-Signature` = 用 webhook 密钥对请求体做 HMAC-SHA256 的十六进制摘要(Plane 签的是 `json.dumps(payload)`,发出的正是这串字节;接收端先比原始请求体,再比重新序列化的结果,均为常量时间比较)。缺失/不对 → 401,什么都不触发。其他头:`X-Plane-Event`(issue / issue_comment)、`X-Plane-Delivery`(每次投递的 UUID)。请求体:`{"event","action"(created/updated/deleted),"webhook_id","workspace_id","workspace_slug","data":{…,"project":<uuid>},"activity":{field,new_value,old_value,actor…}}`;删除事件的 `data` 只有 `id`(没有项目 → 202 忽略,由 cron 兜底)。
- **触发逻辑**:配置项目的投递 → 200,3 s(`debounce_seconds`)内的投递合并成一次 tick;tick 运行期间又来的投递 → 它结束后再跑一次(正在跑的 tick 可能在变化之前就读过 Plane)。tick 命令就是 cron 脚本(`systemd-run --scope … cron/agentd-tick-lxc.sh`,在独立 scope 里跑,重启接收端不会杀掉正在跑的 tick)。脚本持 `flock -n -E 75`:锁被 cron/手工 tick 占着 → 退出码 75 → 每 15 s 重试,最长 2 min,之后交给下一个 cron tick。tick 本身的输出照旧进 `cron.log`/`agentd.log`。
- **日志** `/var/log/plane-agentd/webhook.log`:每次投递一行(`delivery=<X-Plane-Delivery> event=issue/updated project=GWENT item=<uuid> decision=tick|202 ignored|401 bad signature|401 missing signature`),每次 tick 两行(`tick start (n deliveries: …)`、`tick done rc=0 in 12.3s` / `tick busy …, retry in 15s` / `giving up`)。Plane 侧的投递记录:`tools/plane-webhook.py logs <webhook id>`。
- **查看**:`systemctl status plane-agentd-webhook`、`journalctl -u plane-agentd-webhook`、`curl -s http://10.77.10.249:8790/healthz`、`tail -f /var/log/plane-agentd/webhook.log`。
- **轮换密钥**(在 oregon 上,kappiee 管理员会话,密钥不出进程、直接写进 Vault):
```bash
cd ~/.local/share/server-access-client && python3 -I agent-access.py --config https://nas.tail9531b.ts.net:8443/identity/agent.json exec \
--secret-env PLANE_KAPPIEE_PASSWORD=PLANE_KAPPIEE_PASSWORD --timeout 90 -- \
python3 -I /home/ubuntu/Documents/wjj/project/plane-agentd/tools/plane-webhook.py regenerate <webhook id> # id: … plane-webhook.py list
```
然后 LXC 上 `systemctl restart plane-agentd-webhook`(接收端只在启动时取一次密钥;轮换到重启之间的投递会 401,cron 兜底)。核对:`agent-access.py … exec --secret-env S=PLANE_WEBHOOK_SECRET -- python3 -c 'import os;print(len(os.environ["S"]))'`。
- **停用**:只停接收端 `systemctl disable --now plane-agentd-webhook`(Plane 投递会失败并重试,多次失败后 Plane 自动停用该 webhook 并给管理员发邮件);连 Plane 侧一起停:`plane-webhook.py patch <id> is_active=false`。cron 不受影响。
- **通知**:配置里的 `notify: {kind: none}` 是钩子,核心每次改状态都调用 `Agentd.notify()`,目前什么都不做(没有 ntfy 服务)。
## 文件
| 路径 | 内容 |
|---|---|
| `state/agentd-state.json` | 每项目每工作项:轮次、线程 ID 与所在终端/执行器、线程轮数、各轮标签、已见评论、重试计数/下次重试、在途运行(handle、deadline) |
| `<log_dir>/agentd.log`(默认 `state/`) | 每次 tick 的动作与错误 |
| `state/heartbeat.json` | 最近一次 tick 的时间、在途运行、启动未确认的运行(`launching`)、本 tick 跳过的不可达终端(`down`)、没派出去的 Todo 项及原因(`skipped`:`[{project, item, terminal, reason}]`)、错误;超过 10 分钟不更新 = agentd 没在跑 |
| `<log_dir>/webhook.log` | webhook 接收端:每次投递的决定、每次触发的 tick(见上节) |
| `<log_dir>/remote.log` | 每次远程调用一行:`term=win10 via=direct\|warpgate\|fallback op=start\|fetch\|read\|busy\|probe\|kill\|forget… ms=… rc=…`,失败时末尾带 `unreached`/`hostkey`/`unknown`(PAD-12)。不回显到 cron.log;没有轮转,需要时手工截断 |
| `agentd.ssh_dir`(LXC `/run/plane-agentd/ssh`) | direct-ssh 的 ControlMaster 套接字(`<host_key 指纹 8 位>-<%C 哈希>`,空闲 60 s 后自动退出;手工关闭:`for s in /run/plane-agentd/ssh/*-*; do [ -S "$s" ] && ssh -O exit -o ControlPath="$s" x; done`)和由钉住公钥生成的 `known_hosts.<终端>`;root 0700,agentd 自己建。ssh-agent 在 `/tmp/agentd-agent-*`(每个 tick 进程一个,退出时删除) |
| `state/agentd.lock` | tick 互斥锁 |
| `state/runs/<run_id>/` | `transport: local` 终端的运行目录:`prompt.md run.sh pid.txt started.txt exit.txt stderr.txt` + 执行器输出 |
| oregon `/home/ubuntu/.gwent-board/agentd-runs/<run_id>/` | 远程 Linux 终端(`run_root`):同上,另有 `launch.pid`(启动时的 `$!`,与 `pid.txt` 相同);claude 输出 `out.json` |
| `<repo>\runtime\agentd\<run_id>\`(win10) | 同上,`run.ps1`;codex 输出 `events.jsonl last.md` |
## 工作项生命周期
Todo + 指派给终端用户 → agentd 选最小编号、终端有空位、串行组空闲的项 → In Progress,评论 `[worker] 第 k 轮开始…` → 执行器结束后回帖 `[<executor>] 第 k 轮结束(exit, run, 终端, 日志)` + 最后消息:
- `[review]` → Review,指派 `reviewer`;`[question]`/`[blocked]` → Waiting,指派 `answerer`(默认 reviewer);`[progress]` → Todo,留在原终端,下一轮续线程;首行无标签 → 按 review 处理并注明。
- 回到 Todo 的项总会被重新拾起;续线程时只发送上次之后的新评论(按评论 ID 判断,`[worker]` 评论永远跳过)。
- 线程重建:换终端/执行器、续线程失败、线程轮数 ≥ `max_rounds_per_thread` → 新线程,首轮输入附交接摘要(各轮标签 + 上一轮最后消息),开始评论里注明原因与"附交接摘要"。临时性错误的自动重试算同一轮,不计入线程轮数(run id 照常递增)。
- 连续 `max_progress_rounds`(项目配置,默认 12)轮 `[progress]` → 不再续跑,回帖说明并转 Waiting 指派 answerer;人工移回 Todo 后重新计数。
- 运行中工作项被移出 In Progress(任何状态)→ 下一 tick 结束该运行(kill),回帖 `[worker] 运行已取消:工作项状态被改为 <状态>`,释放终端槽位与串行组,保留线程 ID(移回 Todo 时续原线程)。kill 失败则保留槽位,下一 tick 再试。
- **未实现(TODO)**:Plane 的 "blocked by" 关系不参与调度——被阻塞的项只要在 Todo 且指派给终端就会被拾起。需要先后顺序时,先只把前一项放进 Todo。
## 常见故障
| 现象 | 原因 / 处理 |
|---|---|
| `no Plane token in the environment` / tick 报 `no Plane token` | 没在 `agent-access.py exec --secret-env …` 下运行,或键名不对;`check-config` 看 MISSING |
| `skip: another tick holds state/agentd.lock` | 上一 tick 还在跑(win10 上传慢);正常(`tick` 此时退出码 75)。持续出现则查是否有卡死的 agentd 进程 |
| Todo 项一直不动、心跳看着空闲 | 看 `heartbeat.json` 的 `skipped`:`concurrency n/m (GW-x)` 槽位被谁占着;`serialize_group … held by …`;`busy_process codex (… not started by agentd)` = win10 上有不是 agentd 起的(或上一轮还没退干净的)codex.exe;`relay down`;`no route for assignee …` = 指派的用户不是终端。原因第一次出现时 `agentd.log`/`cron.log` 有一行 `GW-n not dispatched (<终端>): <原因>` |
| `webhook.log` 里 `decision=401` | 签名不对:Plane 侧密钥轮换后接收端没重启,或不是 Plane 发的。`systemctl restart plane-agentd-webhook` |
| Plane 建 webhook 报 "The webhook URL is not allowed…" | Plane 的 SSRF 防护拒绝内网地址:Plane api/worker 环境需 `WEBHOOK_ALLOWED_IPS=10.77.10.249/32`(design §10) |
| `[worker] 第 k 轮失败:类型=crash(临时性)…重试 n/N` | 网络/中继/429/5xx:按 `backoff_minutes` 自动重试,续原线程;超过 `transient_max` 转 Waiting |
| `类型=timeout` | 超过 `timeout_hours`(或 `budget:2h` 标签);进程树已结束,转 Waiting。看运行目录日志决定拆分任务或加预算 |
| `类型=crash`(非临时)且是续线程 | 自动在新线程重跑一次(附交接摘要);再失败转 Waiting |
| `类型=env` | 匹配 `retry.env_patterns`(焦点丢失/弹窗/断网等),转 Waiting,同串行组暂停到该项离开 Waiting |
| `win10 unreachable; skipping` / `busy (N codex running…)` | 中继不通,或 win10 上有不是 agentd 起的 codex(`busy_processes`);等它结束 |
| `schtasks launch … failed` / `upload … failed` | 中继或 Windows 计划任务问题;手工 `win10-ps.sh "schtasks /Query /TN agentd-<run>"` 查看 |
| `relay oregon exit 255: …` / `relay timeout` / `<terminal> unreachable, skipping it for the rest of this tick` | 中继(Vault、Warpgate、目标 sshd)不通;该终端本 tick 余下的 poll/启动全部跳过(`heartbeat.down`),其他终端照常。poll 失败下一 tick 再试。手工:`agent-access.py --config … run --target oregon --timeout 30 -- bash -c 'echo ok'` |
| `run GW-n-k: unconfirmed launch found …; adopting it` | 启动调用失败/超时(或 agentd 在启动中途退出),但终端上已有 `exit.txt`/`launch.pid`/`pid.txt`:接管该运行,不重复启动;开始评论注明"启动未确认、已接管" |
| `run GW-n-k: nothing at …; relaunching in GW-n-k-r2` | 启动没有落地:同一 run id、新目录 `<run_id>-r2`(再失败 -r3…)重新启动,旧目录不清空。运行目录里已有 pid 文件时 transport 拒绝清空(`HASPID`),下一 tick 探测后接管 |
| heartbeat `launching` 一直不消失 | 终端探测不通(中继坏),标记保留并占着该终端的槽位;中继恢复后自动接管或重启。人工处理:停 cron,在终端上看 `<run_root>/<run_id>/`,再从状态文件删掉该项的 `launching` |
| `upload of … size mismatch` / `staging … size mismatch` | 远程写文件不完整(磁盘满、`run_root` 不可写);看远程 `run_root` |
| 远程终端每步都慢 | `warpgate-ssh`(或 `via=fallback`)每次调用约 9–14 s(取 Vault 凭据 + 网关握手 + 密码认证 + 二跳)。`direct-ssh` 首次握手 win10 约 0.5 s、oregon 约 2 s,之后 60 s 内复用连接。看 `remote.log` 的 `ms=`。一轮的调用次数:启动 1 次(Warpgate 上 prompt 超过约 45 KB 时多几次分块上传;win10 经 Warpgate 约 8–10 次中继调用),每 tick poll 1 次,收尾 1 次(win10 另加删计划任务 1 次) |
| `SECURITY <终端>: … host key verification FAILED …` | 终端出示的主机公钥和配置里钉住的不一致:可能是重装、sshd 重新生成了主机密钥,也可能是中间人。**不会回退到 Warpgate**,该终端本 tick 标为 down(`heartbeat.down`),每个 tick 都会再报一次。先经第二通道核对(Warpgate:`agent-access.py … run --target oregon -- cat /etc/ssh/ssh_host_ed25519_key.pub`,win10 `win10-ps.sh "Get-Content C:\ProgramData\ssh\ssh_host_ed25519_key.pub"`),确认是合法变更后再更新 `terminals.<t>.ssh.host_key`(deploy-lxc §9 第 4 步)。新的 pin 会换一个 ControlMaster 套接字名(`<pin 指纹>-%C`),不会复用按旧 pin 校验过的连接。终端根本不提供 ssh-ed25519 主机密钥(`no matching host key type`)也按这一行处理。等核对期间要继续派单:把 `transport` 改回 `warpgate-ssh` |
| `<终端>: direct-ssh failed before the command ran (…); using the warpgate-ssh fallback for the rest of this tick` | 直连在命令运行前就失败了(tailscale 断、sshd 停、authorized_keys 缺或 ACL 不对、密钥被拒)。这个操作已在 Warpgate 上重做,本 tick 余下的也走 Warpgate,下个 tick 再试直连。`remote.log` 里会出现 `via=fallback`。查原因:`cli/agentd ping <终端> --transport direct-ssh -n 1`,括号里是 ssh 的原话(如 `Permission denied (publickey)`、`connect to host … Connection timed out`) |
| `direct-ssh unavailable in this process: AGENTD_SSH_KEY is not set …` / `ssh-add failed …` | 进程没拿到密钥(手工运行没加 `--secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY`,或 cron 脚本因为 Vault 取不到键而去掉了它),或 Vault 里的值不是无口令的 OpenSSH 私钥。直连终端整次都走兜底;没配 `fallback` 的终端本 tick down |
| cron.log:`AGENTD_SSH_KEY not available from Vault: tick repeated without it …` | Vault 键 `AGENTD_SSH_KEY` 被删、改名或不可读,agent-access.py 拒绝了整个 exec。tick 已不带密钥重跑,直连终端每个 tick 都走 Warpgate 兜底(慢,但不停派单)。修复:按 deploy-lxc §9 第 1 步恢复这个键;`agent-access.py … list` 能看到键名是否还在 |
| 手工 `exec … --secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY` 只打印 `access-hub: credential_unavailable` | 同上,Vault 里没有这个键(也可能是某个 Plane token 键)。手工运行时去掉 `AGENTD_SSH_KEY` 那个参数即可 |
| `ssh administrator@… timeout after 150s` / `ssh … exit 255: …`(无 fallback 行) | 直连的命令**可能已经运行**(超时、会话中途断开、认不出的 255),按规则不换路径重试:终端本 tick down;启动的话保留 `launching` 标记,下一 tick 探测后接管或在 `-r2` 重启。win10 上每次都超时:多半是 stdin 没到 PowerShell(sshd 默认 shell 被改成了 PowerShell),见 deploy-lxc §9 第 3 步 |
| 工作项在运行中被人改了状态 | 运行还没结束:下一 tick 取消(kill + `[worker] 运行已取消…` 评论),不改状态。已经结束(`exit.txt` 已写):结果照常回帖,但不再改状态/重试 |
| Plane 回帖失败 | 在途记录保留,下一 tick 重新 collect 并回帖(可能重复一条评论) |
| 想手工终止一轮 | 把工作项移出 In Progress(如 Waiting)即可,下一 tick agentd 自己 kill 并释放槽位。中继不通、kill 一直失败时:在终端上手工结束(Linux 终端 `kill -TERM -- -$(cat pid.txt); pkill -TERM -s $(cat pid.txt)`,win10 `taskkill /T /F /PID <pid.txt>`),停 cron 后从 `state/agentd-state.json` 删掉该项的 `inflight` |
| `已连续 N 轮 [progress]` | 达到 `max_progress_rounds`:看最近几轮是否在原地打转;拆分任务或补充说明后移回 Todo |
## 回滚到 oregon(LXC 调度器出问题时)
1. 停 LXC:`agent-access.py … run --target agentd-01 -- bash -c 'crontab -r; flock /var/lib/plane-agentd/cron.lock true'`(删 crontab,等正在跑的 tick 结束)。
2. 状态拷回 oregon:LXC `/var/lib/plane-agentd/agentd-state.json` → oregon `~/.gwent-board/agentd/agentd-state.json`(base64 经中继读回,比对 sha256)。在途运行照常被 oregon 接着 poll。
3. oregon 恢复 cron:`crontab ~/.gwent-board/crontab.backup-20261021-pad11`(即 `*/2 * * * * /home/ubuntu/Documents/wjj/project/plane-agentd/cron/agentd-tick.sh`)。`cron/agentd-tick.sh` 用 `config/agentd.oregon.yaml`(`state_dir: ~/.gwent-board/agentd`,日志 `~/.gwent-board/agentd/agentd.log`、`~/.gwent-board/agentd-cron.log`),与 LXC 配置只差路径。
4. 看 oregon 的 `~/.gwent-board/agentd/heartbeat.json` 更新。再切回 LXC = 反过来做一遍(docs/deploy-lxc.md §5)。
## 上线记录(2026-10-22,PAD-12:直连 SSH + 单次调用派单)
- 代码 5192e04:direct-ssh 传输(会话建立前失败才回落 Warpgate,主机公钥不符绝不回落)、主机公钥固定、Vault 密钥载入 ssh-agent、复用连接、win10 一次调用启动和一次调用收尾、`remote.log`、`agentd ping`。148 个单测通过;经正确性、安全两轮 opus 审查后修正。
- 密钥:在 LXC 的 `/dev/shm` 生成 ed25519,私钥只存在 Vault `AGENTD_SSH_KEY`(指纹 `SHA256:+6uooRFFB3tX4fQ+Sfl+3FmLm9KJpPYpSPzoTcCq/2M`),临时文件已删。公钥行 `restrict,from="100.69.78.124" ssh-ed25519 … plane-agentd@agentd-01`:
- oregon:`~ubuntu/.ssh/authorized_keys`,备份 `.bak-pad12-20261022-025955`;
- win10:`C:\ProgramData\ssh\administrators_authorized_keys`,备份 `.bak-pad12-20261022-110041`,ACL 未变(SYSTEM 和 Administrators 完全控制)。win10 的 sshd 是 OpenSSH_9.5p1,DefaultShell 未设(即 cmd.exe)。
- 两台的主机公钥经 ssh-keyscan 和经 Warpgate 读公钥文件两路核对一致后,固定写进配置。
- 部署 03:01(持 `cron.lock` 执行 git pull 和 ping)。
- ping:oregon 首次 3.9 s(含建立主连接),复用 0.77 s;win10 首次 0.52 s,复用 0.17 s。
- 部署时 win10 的第一次调用有一次 60 s 超时。随后 3 轮新建主连接的首次调用都是 0.52 s,复现不出,推测是 tailnet 线路空闲后刚恢复。这类超时不回落,该终端本 tick 算不可用、下个 tick 重试;在 `remote.log` 里继续观察。
- 冒烟(真实 webhook):
- GW-27 win10:建项 03:06:17.9 → 开工评论 03:06:28.4,**10.5 s**(之前 95 s)。其中 busy 0.22 s、start 0.32 s;收尾 read、fetch、forget 三次调用共 0.9 s。
- GW-28 oregon:建项 03:10:09.5 → 开工 03:10:23.6,**14.1 s**(start 4.1 s,含新建主连接);03:10:32.6 结果已贴回。
- 剩下的延迟在收尾:运行结束后要等下一次 tick(cron 2 分钟,或别的投递触发)才收。迁 Temporal(PAD-8)时用定时器轮询解决。
- 回滚:把该终端的 `transport` 改回 `warpgate-ssh` 再 pull。撤销访问:删掉两台 authorized_keys 里的那一行,再对 `/run/plane-agentd/ssh/` 下的主连接执行 `ssh -O exit`(见 deploy-lxc §9)。
## 上线记录(2026-10-22,PAD-7:Plane 侧 webhook 启用)
- 01:14 Plane VM 506:`plane.env` 与渲染 compose 设 `WEBHOOK_ALLOWED_IPS=10.77.10.249/32`(备份 `*.bak-pad7-20261022-011453`),`--dry-run` 确认后只重建 api、worker、webhook-consumer、outbox-poller、automation-consumer、beat-worker(01:15:43–01:16:33);01:18:26 实测 API 已返回 200(之前未测,停机上限约 2.5 分钟),api 容器 4 分钟内转 healthy。
- 01:19 `tools/plane-webhook.py create http://10.77.10.249:8790/plane` → 201(白名单生效),密钥写入 Vault `PLANE_WEBHOOK_SECRET`(替换临时值);01:22:30 `systemctl enable --now plane-agentd-webhook`(LXC 代码 1b4d965,单元文件与仓库一致);01:22:57 `patch c039c471-… is_active=true`。
- Plane 建出的是 **v2** webhook:负载 `{version: v2, event, entity_type, data, …}`,由 `plane/webhook/bgtasks/webhook_task.py` 的 `send_webhook` 发出,签名 `HMAC-SHA256(secret, json.dumps(payload))`,与接收端一致;`data.project` 路由到 GWENT 正常。
- 实测(真实投递):评论 GW-25 → 01:24:34 投递到达 → 01:24:37 tick → 4.6 s 完成;GW-25 改 Done 同样 3 s 后 tick。建项 GW-26(01:25:14)→ 01:25:15 投递 → 01:25:18 tick 开始 → 01:26:49 `[worker] 第 1 轮开始`(win10 codex/astra):**建项 → 开工 95 s,其中 tick 本身 91 s**(中继、上传任务书、计划任务启动);只靠 cron 时在这之前还要平均多等约 60 s。剩下的成本都在中继调用上(PAD-12)。
- cron `*/2` 保留为兜底(同一把 `cron.lock`);oregon crontab 仍为空,只有一个调度器。
## 上线记录(2026-10-21,PAD-7:webhook 接收端)
- 13:25 LXC 上 `plane-agentd-webhook` 启用(`6dc7e72`),cron 不变。**Plane 侧 webhook 未建成**:Plane 3.3.3 对 `http://10.77.10.249:8790/plane` 返回 400 "The webhook URL is not allowed. Provide a publicly reachable HTTP or HTTPS URL."(SSRF 防护拒绝内网地址,见 design §10);需要在 Plane 的 api/worker 环境加 `WEBHOOK_ALLOWED_IPS=10.77.10.249/32`,之后按 deploy-lxc §8 第 2–5 步建 webhook。在此之前 Vault 里的 `PLANE_WEBHOOK_SECRET` 是临时随机值(`plane_wh_interim_…`),`plane-webhook.py create` 会用 Plane 生成的密钥覆盖它。
- 验收用的是在 LXC 上按 Plane 格式签名的合成投递(`/root/lxc_send.sh good|bad|missing|other`):错签名、缺签名 → 401,不触发;别的项目 → 202;正确签名 → 200,3 s 后 tick 开始。tick 运行中又来一条 → 结束后补跑一次(13:27 两条投递:224 s 的 tick——收尾 GW-15、在 win10 启动 GW-5——之后接着 24 s 的补跑);那段时间 cron 的 tick 撞锁立即退出,cron 本身照常每 2 分钟触发。
- 冒烟 GW-17(oregon claude-code):建项 13:43:15.8 → 合成投递 13:43:41(经 oregon→LXC 中继发出,人为多了约 25 s)→ tick 开始 13:43:44 → `[worker] 第 1 轮开始` 13:44:56 → `[review] ok` 13:47:54,PASS 并移到 Done。**投递 → tick 开始 = 3 s;tick 开始 → 开始评论 = 72 s**(这一 tick 先 poll win10 上在途的 GW-5,再经中继启动 oregon;同期 cron 的启动 tick 也要 70–77 s)。真实 Plane 投递约 1 s 到达时,建项 → 开始评论预计约 75 s,而只靠 cron 是平均约 60 s 等待 + 同样的 tick 时长。≤10 s 的目标在"开始评论"这个口径上达不到:瓶颈是中继调用(每次约 10 s,vault + Warpgate SSH)和启动流程,不是触发方式。
- GW-16(第一次冒烟):webhook 触发的 tick 里 LXC→中继报 `access-hub: config_private_transport_failed`(LXC 经 tailnet 取 `nas…/identity/agent.json` 15 s 超时;13:38 的 cron tick 也撞上一次),启动失败;下一 cron tick 探测无运行目录 → `GW-16-1-r2` 重新启动,PASS(防重复启动机制按设计工作)。
- 重启:tick 运行中 `systemctl restart plane-agentd-webhook`,tick(在 `systemd-run --scope` 里)没被杀、照常结束(heartbeat 13:50:10,errors=[]);接收端约 19 s 后重新监听(取 vault 密钥 + 启动)。
## 切换记录(2026-10-21,PAD-11:调度器迁到 LXC agentd-01)
- 代码:防重复启动(`launching` 标记 + 探测接管/换目录重启)与"中继不通的终端本 tick 跳过"随本次一起上线(4f45d36);LXC 用仓库里的 `config/agentd.yaml`,oregon 回滚用 `config/agentd.oregon.yaml`。
- 切换前 LXC 手工 `tick --once`(oregon cron 仍在跑,Plane 上无 Todo 项):空闲、无错误;LXC 经 helper 探测 win10(`codex` 计数)与经中继探测 oregon 运行目录均正常。
- 切换时 oregon 刚收尾 GW-11-1(11:53 由 oregon 派出、11:57 `[review]`),无在途运行。11:59 删除 oregon crontab 行(备份 `~/.gwent-board/crontab.backup-20261021-pad11`,oregon crontab 现为空),状态文件(sha256 `106c2bce…`)复制到 LXC,12:00 装 LXC crontab,12:02:09 第一个 cron tick(heartbeat 更新,errors=[])。
- 冒烟(oregon 上 `smoke … --watch`,由 LXC cron 派发):GW-12 oregon claude-code/sonnet 12:04:20 启动 → 12:06:47 `[review] ok` PASS;GW-13 win10 codex/gpt-6.1-sol/fast 12:05:58 启动 → 12:07:45 `[review] ok` PASS。续线程:GW-12 评论"回复 `[review] ok 2`"后移回 Todo,12:08:20 第 2 轮续 session `2348d52f…`(两轮 `out.json` 的 `session_id` 相同),12:10:44 `[review] ok 2`。两项均移到 Done。
## 切换记录(2026-10-21,P1-5)
- 旧 worker `gwent-design/tools/board-worker-plane.py`(cron `board-worker-plane-cron.sh`)已从 crontab 移除;脚本与 `~/.gwent-board/plane-state.json`、`plane-worker.log` 保留作历史,不要再启用(两者会抢同一批 window10 的 Todo 项)。
- 切换时旧 worker 无在途运行(GW-6 的 board-6-2 已于 06:30 以 `[blocked]` 结束,项在 Waiting)。GW-6 的 codex 线程 `01a15ce3…` 已迁入 agentd 状态(`runs=2`、`thread_on=[win10, codex]`、旧评论标为已见),移回 Todo 并指派 window10 时 agentd 续这个线程,run id 从 `GW-6-3` 起。
- 运行目录变了:旧 worker 用 win10 `runtime\board\board-<n>-<k>`,agentd 用 `runtime\agentd\GW-<n>-<k>`。
- 冒烟:oregon(GW-7,claude-code/sonnet,约 30 s)与 win10(GW-8,codex/gpt-6.1-sol/fast,约 3.5 min)均 PASS。win10 上验证过:`codex exec resume` 也会输出 `thread.started`(与原线程同 ID);续线程时 `-c sandbox_mode="workspace-write"` 生效,能在仓库内写文件;把运行中的项移出 In Progress 后,`taskkill /T /F /PID <pid.txt>` 结束了整棵树(runner powershell → codex.exe → code-mode-host / command-runner → powershell),`Get-Process codex` 归零,计划任务被删除。
- 差异(P1-5 时):旧 worker 对没有 model 标签的项用 sol + `service_tier="fast"`;agentd 当时只在有 `model:sol` 标签时加 fast。P1-6 起 codex 执行器有 `tier_defaults: {gpt-6.1-sol: fast}`,无标签项也是 sol + fast;`tier:<x>` 标签改档位,`tier:none` 去掉档位参数。
- win10 上 codex 的 stderr 常见 websocket 连接被拒(`os error 10061`,`wss://chatgpt.com/...`)后回退成功;只有运行失败时它才出现在失败说明的尾部,不影响分类(不匹配临时性模式)。
## 切换记录(2026-10-21,P1-6:oregon 改为远程启动)
- `config/agentd.yaml` 的 oregon 从 `transport: local` 改为 `warpgate-ssh`(`target: oregon, os: linux, launch: nohup, run_root: /home/ubuntu/.gwent-board/agentd-runs`)。agentd 仍在 oregon 上跑,但启动执行器走 Warpgate 回到本机,与将来 LXC 上的路径完全相同。切换时 oregon 无在途运行;GW-7/GW-10 的 claude 线程(`thread_on=[oregon, claude-code]`)不受影响——会话存在 oregon 的 `~/.claude`,与 transport 无关。
- win10 不变(helper 脚本 + schtasks),只是中继子进程改由共用的 `call()` 启动;切换后 GW-9 的 poll/collect 已经走新代码并正常结束。
- 冒烟(经 Warpgate 远程启动):GW-10 第 1 轮 claude-code/sonnet PASS(`[review] ok`,启动 08:08:54 → 结束 08:10:17,session `fa3be0f7…`);评论"请回复 `[review] ok 2`"并移回 Todo 后第 2 轮续同一 session(`out.json` 的 `session_id` 不变),回 `[review] ok 2` 转 Review。启动只用 1 次中继调用(约 12 s);72 KB 的 prompt 走分块上传(约 75 s)。
- kill:直接经中继对 oregon 起 `sleep 600 & sleep 500` 的运行再 kill,两个 sleep 都被结束(进程组 + 会话),`pid.txt` 与 `launch.pid` 相同(setsid 没有再 fork)。经 Plane 的取消冒烟(运行中移到 Waiting)因 Plane 当时 502(08:14 起)未做,待补。
## 回滚记录(2026-10-21:调度器回到 oregon)
- 原因:LXC agentd-01 经 tailnet 到 NAS 的路径走 DERP 中继,取 vault 身份端点(`nas…/identity/agent.json`)TCP 连接 30/30 超时,tick 里的中继调用不稳定。网络修好后再切回 LXC(docs/deploy-lxc.md §5)。
- 做法:LXC 删 cron 行、停用 `plane-agentd-webhook`、等 `agentd.lock`/`cron.lock` 空闲;`agentd-state.json`(sha256 7201077b…eb87,两端一致)拷到 oregon `~/.gwent-board/agentd/`(旧文件备份为 `.bak-<时间戳>`);oregon 恢复 `*/2` 的 `cron/agentd-tick.sh`。LXC 上的文件、服务单元均未删除。`config/agentd.oregon.yaml` 无需改动(`check-config` 通过)。
- 验证:oregon 首个 tick 14:20:01 接管,在途的 GW-5-1(win10 Codex)已结束,结果回帖并进入 Review;heartbeat `errors=[]`、`inflight=[]`,下一 tick(14:22)同样。
- 重新切回 LXC 之前:先 `crontab -r`(oregon)并等 tick 结束,再按 deploy-lxc §5 把状态拷回 LXC,并重新 enable webhook 服务;任何时刻只能有一个调度器。
## 切换记录(2026-10-21,网络修复后重新切回 LXC)
- 网络:OPNsense 增加出站 NAT 静态端口规则 `Tailscale agentd-01 static-port`(10.77.10.249/32,UDP,WAN),LXC tailscaled 改听 UDP 41642;LXC 到 NAS 直连(`tailscale ping` 5 ms via 223.102.82.206:1549,身份端点 30/30 成功、中位 0.089 s)。详见 deploy-lxc §2.1。
- 切换:oregon crontab 清空(备份 `~/.gwent-board/crontab.backup-20261021-152423-recut`),状态文件(sha256 `54b747db…7155`,两端一致;LXC 旧文件备份 `agentd-state.json.bak-20261021-152504`)拷到 LXC,装 LXC crontab `*/2 … cron/agentd-tick-lxc.sh`。在途的 GW-18 带过去(LXC 上为 GW-18-2),之后的 run-1 结果已回帖。heartbeat errors=[]、down=[]、skipped=[],只有一个调度器。
- Webhook(PAD-7)仍未启用:`plane-agentd-webhook` inactive/disabled;Plane 的 `WEBHOOK_ALLOWED_IPS` 尚未设置(见 deploy-lxc §8 的具体改法,Plane 实为 VM 506 / 10.77.10.69)。PLANE_WEBHOOK_SECRET 仍是临时值。待办:应用该改动后做 §8 第 2–5 步;考虑轮换 Plane 的 AES_SECRET_KEY(排查时被打印进了一次会话记录)。