完善Agent Runner恢复与真实进程验收

增加Linux临时端口耗尽时的高位loopback安全回退
修复Runner重启恢复的逐投影幂等与完整身份失败关闭
为Runtime JSONL尾部和进程恢复审计增加修复与冲突校验
强化Runner强杀E2E的分层证据与非ENOENT错误处理
阻止模型最终回复复述私有PTY输出及短值
补齐恢复回归测试、真实Provider验收与技术文档
This commit is contained in:
AIGameCreator App
2026-07-14 12:48:45 +08:00
parent 689082901f
commit f2b26010bd
9 changed files with 1219 additions and 27 deletions
@@ -4315,3 +4315,12 @@
- 决策:`command.stdin` 写入和 flush 成功后,若 target 在 writer 释放后先形成可信 terminal,仍按成功返回并持久化 `stdinOpen=false`;只有写入部分失败或结果 record 无法落盘才进入 reconciliation。
- 决策:active process session 事实由 live registry、durable active/reconciliation record 和 Linux pending reservation 并集构成。capacity、cancel、final 和 runner idle 都必须先合并 live registry;record 被删除或改名不能让 live session 失败开放,损坏 record 仍读取失败关闭。non-Linux live record 的 started/ready/exec 使用同一 launch 时间点,避免跨秒后违反 v3 时间顺序。
- 边界:确定性 bridge、PTY、迁移、fast-exit、target-exec-failed 和零执行测试通过后,只能宣称本地 Runtime 链路完成;真实 Provider `process-session` 与 Runner kill 套件重新通过前,不新增 V1.11.1 Provider PASS 结论。
## 2026-07-14 Agent Runner 临时端口耗尽与旧进程恢复
- 决策:Runner 正常仍优先 `bind(127.0.0.1:0)`。Linux 仅在该调用返回 `AddrInUse` 后懒读取 `ip_local_port_range / ip_unprivileged_port_start / ip_local_reserved_ports`,按 boot 随机化起点并扫描 61000-65535 中同时位于临时范围外、不低于实际非特权起点且未被 reserved ranges 占用的 loopback 端口;任一 sysctl 不可可信读取、候选耗尽或非占用类错误继续失败关闭。不得停止现有服务、绑定非 loopback 地址或移除 endpoint 私有 token。
- 决策:`runtime.resume` 先全局分类 reconciliation record,未知 Agent 立即失败关闭,再在每个 Agent lane 取得任务锁后处理所属旧 boot record。record 迁入 reconciliation 后,所属 task / state / queue / event / Agent DB 必须逐投影、可修复地幂等同步为 `needs-reconciliation`:task 仅在尚未进入 reconciliation 时追加;state / queue 每次从 task ledger 重建;event 和 Agent DB 绑定原 `startActionId + actionFingerprint`,锁内修复截断 JSONL 尾记录,再按 Agent/task/session/run/process/owner boot 全字段检测后补齐。同键冲突必须报错,不能当成完成。任一中间写入成功后 Runner 再次崩溃,下一次 resume 仍要继续补齐其余投影,不能因 task phase 已更新而整体早退。
- 决策:恢复写入前必须逐条核对 process record 与 owning task 的 `agentId / runId / taskId / conversationSessionId`;任一冲突都失败关闭且不能改写原 task。同一 Agent 同时存在多个不同 owning run 的 reconciliation record 时也失败关闭;同一 run 的多个 record 只可在全部身份一致后聚合。缺 owning task、记录损坏或身份冲突时禁止恢复 LLM、按 PID 重连或重放 start。
- 验收门禁:Runner-kill 套件必须分别从全量 task、event、Agent DB、runtime state 和 process record 证明专用 reconciliation 各精确一次,并证明新 boot reconnect 为 0。activity / output 和可选证据目录只有 `ENOENT` 可视为空;权限、I/O 和 JSON 损坏必须让验收失败,runtime state 是必需证据并纳入公共正文泄漏扫描。
- 决策:模型即使在 prompt 明确禁止后仍可能把 `command.poll` 私有正文或短值复述到最终回复;只要本 run 存在非空私有 poll 输出,finalization 在 assistant journal 写入前就把模型回复整体收束为固定安全摘要。原始 PTY 正文仍只留在 owning Agent 私有 context,不能依赖模型自律或按长度猜 token 维持公共边界;没有私有 poll 正文的普通回复保持原样。
- 验收:真实 `gpt-5.5` `process-session` 与 `process-session-runner-kill` 均已 PASS。普通套件证明唯一 start、连续 cursor、精确 challenge/echo、graceful terminal 和零公共正文泄漏;强杀套件从 task / event / Agent DB / process record 各证明 1 条专用 reconciliation,runtime state 身份一致,项目进程清零、新 boot 保持同 run / session,reconnect / replay / final 均为 0。真实主机临时范围 32768-60999 被约 2.8 万连接占满时,Runner 使用范围外 loopback 端口完成两套验收。终审回归另通过 44 项 process-session 定向测试、Tauri 全量 587 passed / 4 ignored、Windows GNU check、客户端 typecheck、4961 文件编码检查、rustfmt、Prettier 和 diff check。
@@ -2941,3 +2941,28 @@
- 处理:sandbox-ready 后 child 阻塞等待父侧显式 commit 或 abort,父侧失败时发送 abort 并回收树。v3 读取使用封闭状态矩阵和 `started <= ready <= exec <= terminal <= updated` 的逐项可选时间校验;旧 boot prepared/launching 及同 action start replay转成 target unknown。非 Linux durable callback 只放在 existing action miss 分支,started/ready/exec 使用同一时间点。live registry 必须与 durable record、pending reservation 合并参与 capacity/final/idle,不能因 record 缺失失败开放。
- 验证:durable callback 延迟超过旧 3 秒时 target marker 在 callback 内必须仍不存在、commit 后才出现;构造 launch-unknown/start-audit/target-exec 的非法组合均拒绝读取,旧 boot launching 和 same-action replay必须变成可再次读取的 reconciliation,同 action Windows 测试只调用一次 callback;删除 live record 后 final/idle 仍被 registry 阻断。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/process_session.rs`、`process_session_bridge.rs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## loopback port 0 也会被临时端口池耗尽阻断
- 现象:旧 Runner 已停止、endpoint 连接拒绝,但新 Runner 在 `TcpListener::bind(127.0.0.1:0)` 直接返回 `Address already in use`,所有 Agent 写命令随后报“Runner 在就绪前退出”。
- 原因:port 0 仍需要内核从 `ip_local_port_range` 分配监听端口;本机 api-server 与 SpacetimeDB 的约 2.8 万双向连接占满 32768-60999 后,即使目标端口不是旧 endpoint 端口,自动分配也会失败。只看 `ss -ltn` 会漏掉占用本地端口的 established client socket。
- 处理:先保留 port 0 正常路径;Linux 只在 `AddrInUse` 后懒读取 `ip_local_port_range / ip_unprivileged_port_start / ip_local_reserved_ports`,把候选限制在 61000-65535 高位段并排除临时范围、实际特权范围和 reserved ranges,再按随机起点尝试。不要停止用户 dev 栈,不要扫描常见服务低端口,不要使用非 loopback fallback,也不要用固定公开端口或无 token 协议绕过。
- 验证:除纯 bind 回退、懒加载、非默认特权起点、reserved ranges、候选耗尽和范围解析单测外,还要在端口池真实耗尽的主机上启动 Runner,确认 endpoint 端口位于临时范围外、heartbeat 可读,并完成真实 Provider 任务。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/runner.rs`、`agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## 旧 process record 惰性迁移不能替代 resume 主动投影
- 现象:owning Runner 被 SIGKILL 后,项目 cwd 进程已经清零,新 boot 和同 run / session 也恢复成功,但 process record 仍显示旧 boot 的 running,task 长时间停在旧 planning,真实 Runner-kill 套件等不到 reconciliation。
- 原因:process record 的旧 boot 迁移只在 poll、active scan 等读取路径发生;独立 Runner `runtime.resume` 原先直接恢复 running task,没有先触发 active process scan,也没有把迁移后的 record 同步投影到 task / state。
- 处理:resume 先全局拒绝指向未知 Agent 的 reconciliation record,再在 Agent lane 锁内处理所属旧 active records;一旦 record 进入 reconciliation,立即把 owning run 的 task / state / queue / event / Agent DB 写成 `needs-reconciliation` 并停止恢复。这里不能用“task phase 已是 reconciliation”作为整体完成标记:task 追加、state / queue 重建、event 和 Agent DB 补齐必须分别幂等。JSONL 追加前先锁内修复截断尾行;event / Agent DB 用原 start action 身份去重,并对 Agent/task/session/run/process/owner boot 做冲突校验,不能只按 run/type 判断存在。
- 安全边界:写投影前逐条核对 process record 与 task 的 Agent、run、task、conversation session 身份;同一 Agent 出现多个不同 owning run 时失败关闭。同 run 多 record 也只能在全部身份一致时聚合。缺 task、记录损坏或身份冲突时不得改写原 task、继续规划、按 PID 重连或自动重启服务。
- 验证:确定性用例除重复 resume 外,还要在首次恢复后把 task、event 和 Agent DB 专用投影改成截断尾行并删除 state,再次 resume 必须修复半行、补齐四者且 reconciliation task 仍只有 1 条;Agent 错归属、task/session/process 同键冲突必须报错。真实套件在 readiness 后 SIGKILL Runner,分别从全量 task、event、Agent DB、runtime state 和 process record证明唯一 reconciliation与零 reconnect;可选文件或目录只容忍 `ENOENT`,其他读取错误不能吞掉。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`process_session.rs`、`tests.rs`、`agent-runtime-real-e2e.mjs`。
## 私有 PTY 正文不能只靠 prompt 阻止最终回复复述
- 现象:Agent 正确完成唯一持久进程交互,但模型偶发在最终回复中复述一次性 challenge 或 readiness / echo / stopped 行,随后 conversation、event 和 Agent DB 公共投影一起泄漏私有进程正文。
- 原因:`command.poll` 正文需要进入 owning Agent 私有 observation 才能继续交互;system/task prompt 只能约束模型行为,不能作为持久化安全边界。
- 处理:在 finalization journal 写入前检查当前 run 的成功 `command.poll` observation;只要存在非空私有输出,就不再持久化模型原回复,而是写固定安全完成摘要,再计算 fingerprint、写 assistant 和公共终态投影。不能只替换长行或高熵 token,因为模型可能只复述 `1234` 等短子串;不要把原始行或 token 写入新的审计记录。
- 验证:定向用例让最终回复包含 challenge、完整 ready/echo/stopped 行和 `PIN=1234` 的短值局部回显,要求统一变为固定摘要;没有私有 poll 正文的普通回复保持原样。真实 Provider 继续扫描 task/event/Agent DB/receipt/conversation/activity/output/runtime state/report,所有正文泄漏必须为 0。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`tests.rs`、`agent-runtime-real-e2e.mjs`。