Files
dsh_ai1net_server/交付物/唤醒-无人值守可行性-20260930.md
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

18 KiB
Raw Blame History

唤醒 · 无人值守可行性(2026-09-30 19:4x)

线:机制线 · 唤醒机制探索 | 域:ai1net-dsh-server/ | 会话:a202550c(19:33 起的接续会话) 上游:交付物/唤醒-idle钩子坐实-20260930.md(本棒执行其 §9.1 留下的决定性一问) 配图:交付物/唤醒-无人值守可行性-20260930-图.html(泳道图 + 事故链图,SVG 版)


0. 判定(先给答案)

🔴 不能。 「把已经退出的那个会话从外部叫起来续跑」——做不到,不是"没做好",是结构上没有这条路。

依据一句话:会话的网关口就长在那个 worker 进程里(/api/v1/health 回的 pid 就是 worker 的 pid)⇒ worker 一退出,端口跟着消失;而唯一的外部投递入口 POST /api/v1/runs 根本不带"选哪个会话"的能力(它的 id 只当消息 id/conversation.id,路由先取该宿主自己的 primarySession)。

但"无人值守"本身仍然做得到 —— 只是它的形态不是"叫醒旧会话",而是"无人值守地新起一个人/一个新会话",也就是今天已经在跑的东西:自动化派棒。代价是粒度最细 1 小时(FREQ 硬白名单)。

⇒ 结论对上一棒 §7「闭环方案③(idle 钩子 → runs)」的影响:钩子只能投给"此刻活着的"会话,而 worker 存活时长实测中位数约 4–5 分钟、最短 28 秒、且宿主不保证复活⇒ 该闭环不具备无人值守所需的稳定性。


1. 三个小问,逐条作答

# 问题 答案 判据级别
1 worker 退出后,该会话的网关口还在不在? 不在。端口随进程消失。 实测本机只剩 3 个网关,与"此刻正在跑的会话"一一对应;706d2476 / 12732a10 / ac8de40d 在任何网关上都不是 live,它们的 CLI 进程也不存在了 实测+源码
2 对 worker 已退出的会话 POST /api/v1/runs({id,type:"message",text})会怎样? 既不是排队、也不是把它拉起来 —— 是"被接受,然后落到投递方自己那个会话"。实测 id=706d2476 的请求 → HTTP 202 {runId,status:"accepted"}、GET /runs/{runId} = active:true,但 /status.activeSessionId 全程=本网关自己的会话;706d2476 的 transcript 一字未变(230 行 / md5 7c404bfd… / mtime 19:09:15.929,与投递前完全一致) 端到端
3 是不是只剩"POST /api/v1/process/start"或"重新起一个会话"? process/start 不是这条路的解(它是"在网关宿主里起一个任意进程",对齐 E2B Process.Start,与"启动/唤醒某个会话"无关;GET 打它 → 404 ⇒ 路由确实是 POST)。唯一出路 = 新起一个会话 —— 且这已经是现状:自动化是唯一能开新会话的外部通道(钩子开不了会话) 实测+源码

2. 端到端证据链(本棒实测,19:40–19:45)

# 证据 读数
1 投递前基线 706d2476…jsonl:230 行,md5 7c404bfd6c297c2bfe350f4b1554431e,mtime 19:09:15.929(冻结)
2 POST /api/v1/runs(body 的 id = 706d2476-93fb-439a-9742-22e009915331) HTTP 202 {"data":{"runId":"c2f4941d-3a0e-42bf-88af-d84790c85ffa","status":"accepted"}}
3 GET /api/v1/runs/c2f4941d-… {"runId":"c2f4941d-…","active":true} ⇒ 任务确实进了该宿主的任务表(不是被拒)
4 投递后 /sessions/live 与 /status sessionId / activeSessionId 仍是 a202550c(本会话),没有切到 706d2476
5 投递后基线复测(4 分钟后) 706d2476…jsonl:仍是 230 行,md5 仍 7c404bfd…,mtime 仍 19:09:15.929 ⇒ 目标会话零反应
6 网关集合复测(19:44) 3 个网关::55283→11a263a2(忙)、:64288→a202550c(本会话,忙)、:53480 pid=59080 → live=(none)。⚠️ 6ecf6d98 昨天还占着 :54800,19:35:42 其 worker 再死,现在它没有网关了 —— 现场就抓到了 worker 走 ⇒ 网关走

🔴 证据 6 的那个 live=(none) 网关是白送的一条硬证据:它证明"宿主可以活着、但没有任何会话"。 按源码(§3 第 2 条),此时 getOrCreateSession() 不命中 primarySession ⇒ 会走 sessionManager.create() 新建会话。 ⇒ 这就是"退路=新起一个会话"的机制面貌。⚠️ 本棒有意没有对它投递(它是池里的条目,投进去等于劫持一个待分配宿主、还会平白多出一个会话),只作机制判据记录。


3. 源码判据(为什么做不到)

# 事实(源码位置均为 app.asar → cli/dist/codebuddy-headless.js) 含义
1 网关路由与 worker 同体:api/v1/{sessions, sessions/live, sessions/:id/reply, sessions/:id/{history,replay}, runs, runs/:runId, status, health, info} 全部注册在同一个 CLI 进程内;/api/v1/health 回的 pid = 该 worker 的 pid(实测 81372 = 本会话的 prewarm 条目 pid) worker 退出 ⇒ 端口关闭 ⇒ 无入口可投
2 runs 不选会话:createRun() → generic 适配器 parseInbound() 只校验 id 与 type,把 id 拿去做消息 id/conversation.id;随后 getOrCreateSession(conversation.id) 的第一句就是 if (this.primarySession) return this.primarySession 请求体里写谁的 id 都不能把消息投给那个会话
3 primarySession 怎么来的:网关起来时 sessionManager.sessionSubject.pipe(filter(L=>L!==undefined), take(1)).subscribe(L=>gatewayBridge.setPrimarySession(L)) —— 只取第一个非空值,一辈子不变 一个 worker 只服务它自己那一个会话;runs 实际语义 = "把消息发给这个宿主自己的会话"
4 replySession() 硬闸门:canDeliverFollowReply(live.id, target) = (live && live===target),不等则 409 SESSION_FOLLOW_NOT_LIVE 与既有结论一致:reply 只认 live,且必须是它自己
5 宿主不自愈:CliPrewarmPool 的 child.on("exit") 里,status==="activated" 的分支只 this.log("warn", "activated prewarm entry exited unexpectedly …") —— 不补池、不重拉、不通知(replenish 只在 status!=="activated" 的 idle/spawning 分支里调) 没人会因为"某个会话死了"而把它重新拉起来
6 复活动因在桌面端,不在 HTTP 面:api/v1 下没有任何"打开/切换/激活会话"的端点(会话控制器只有 list/live/across-projects/workspaces/pins/remove/rename/history/replay/reply);而 daemon app server 与主进程之间走 pipe(源码注释:"均为 pipe"),不是 TCP 实测到"某会话被反复重新激活"(§4)确有其事,但那根线外部程序接不上

4. 泳道图(角色 × 阶段)

              ①会话被激活          ②回合进行中           ③回合结束            ④worker 自行退出        ⑤外部想叫醒它
──────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────
宿主主进程     │ tryAcquire→doActivate  (不干预)        (不干预)           child.on("exit")        ✗ 无入口
(CliPrewarmPool)│ +spawn 补池                                                ⇒ 只打一条 warn        ⛔ 不补池/不重拉
               │                                                            ⛔ 不补池/不重拉         ⛔ 不通知任何东西
               │
CLI worker     │ 起进程·挂网关口        处理消息·跑工具       生成收尾             ◄── code=1 静默退出 ──  已不存在
(= 会话宿主)    │ :PORT 随进程诞生       (addHistory 不停)   addHistory ✔        stderr 空·非人为 kill   ⇒ 端口也没了
               │
网关(HTTP)     │ /live = 本会话          writerOccupied=true  (同左)            ✗ 随进程消失           ✗ 连不上
               │
外部程序       │ —                      —                    —                    —                      ┌──────────────┐
(想唤醒的一方)  │                                                                                       │ POST /runs   │
               │                                                                                       │  → 202 接受  │
               │                                                                                       │  → 落到「它  │
               │                                                                                       │    自己那个  │
               │                                                                                       │    会话」    │
               │                                                                                       └──────┬───────┘
               │                                                                                              ▼
目标旧会话      │  ←── 唯一一次被激活 ──   (跑完了它的回合)   transcript 冻结      transcript 就此冻结    ▓▓ 零反应 ▓▓
               │                                                                                      (md5 一字未变)

红格:第 ④ 列(worker 退出)与第 ⑤ 列(外部叫醒)—— 前者把网关口带走,后者因此没有可投的地址; 即使对别的活着的网关投递,也只会落在它自己的会话上(右下角那格)。


5. 事故链图(这次"想唤醒旧会话"实际怎么断的 + 哪几道防线缺席)

19:00:19.720  宿主 tryAcquire + doActivate ⇒ 会话 706d2476 拿到 worker(pid=47188),网关口起来
                  │
                  ▼
19:00–19:09   该会话跑完它的整轮工作(会话日志连续增长)
                  │
19:09:16.304  [SessionRuntimeConfigResolver] resolveConfig input {sessionId:706d2476…}
                  │   ▲ 宿主这边**确实**为它准备过一次运行配置(=有人在请求给它派活)
                  ▼
19:09:16.570  ⛔ [CliPrewarmPool] activated prewarm entry exited unexpectedly
                  (pid=47188, sessionId=706d2476…, code=1, signal=none) | stderr tail: <empty>
                  │   ▲ 静默退出:无 stderr、非人为 kill ⇒ 那次"派活"的准备**就地蒸发**
                  ▼
19:09:16.570→ 此后再无任何 706d2476 的 doActivate / resolveConfig
                  │   ⇒ 宿主**没有**把它重新拉起来(符合 §3 第 5 条:activated 退出不补不拉)
                  ▼
19:40:56     本棒把 id=706d2476 的 POST /api/v1/runs 打到**另一个活着的网关**
                  │   ▲ 期望:把它叫起来
                  ▼
19:40:56     回包 202 accepted、run 表 active=true …… 但 activeSessionId 仍是**投递方自己的会话**
                  │
                  ▼
19:45:00     706d2476 的 transcript 230 行 / md5 7c404bfd… / mtime 19:09:15.929
                  ▲ **一字未变** ⇒ 这一棒想做的事,结构上就做不成

哪几道防线缺席(关键)

# 缺席的防线 后果
① "消息投给了谁"没有回执:runs 回 202 只说明"受理了",不回"最终落到哪个会话"。/status.activeSessionId 也不会因为这次投递而变 调用方完全无法从响应判断"它到底有没有投到我想要的那个会话" —— 这正是"以为唤醒了、其实投给自己"的来源
② activated 条目意外退出=只写一条 warn(不补池、不重拉、不通知) 会话的宿主没了,系统里没有任何一环会察觉,更不会自愈
③ 网关没有"打开/激活会话"的端点;桌面端那根复活动因走的是 pipe,不是 TCP 外部程序拿不到那条唯一有效的路 ⇒ "无人值守"只能退化成"新起会话"
④ worker 存活时长无下限保障(实测 28 s 起) 任何依赖"这个会话此刻活着"的方案(含 idle 钩子)都建在一段随时会断的窗口上

6. worker 能活多久(回答上棒 §9.1 的"是周期性常态还是偶发")

⇒ 是常态,不是偶发。 activated prewarm entry exited unexpectedly 三天计数:

日期 次数
2026-09-28 40
2026-09-29 55
2026-09-30(今天) 28(08:09 → 19:36 分布在整日)

今日"激活 → 退出"配对(activate 时刻取自 doActivate,退出时刻取自 exit warn):

会话 激活 worker pid 退出 存活
706d2476 19:00:19.720 47188 19:09:16.570 8m57s(🔴 此后永未再激活)
12732a10 19:09:59.762 63272 19:10:27.935 28 s
ac8de40d 19:24:22.463 85352 19:30:18.186 5m56s
6ecf6d98 19:31:13.556 62308 19:35:42.381 4m29s
6ecf6d98 19:36:10.229 82656 19:36:41.683 31 s
a202550c(本会话) 19:33:26.129 81372 仍活(>12 min) —

读法:

  • 典型存活 ≈ 30 s ~ 6 min,多数落在 4–5 min;正在干活的会话可以活很久(本会话 >12 min)。
  • ⇒ idle 钩子要的"连续空闲 ≥60 s",正好要落在 worker 生命的尾巴上。今天全天唯一一次成功(18:55:00.210)就是这个窗口的一次幸运命中。
  • ⚠️ 唯一一次"该活没活"的反例:706d2476 在 19:09:16.304 有过一次 resolveConfig(有人在给它派活),0.27 s 后 worker 静默退出,那次派活就地丢失、且没有任何重试 ⇒ 这是"无人值守"最怕的失败形态:静默丢单。

7. 替代路径与代价("能做"的那一半)

路径 能做什么 代价 / 硬约束
A 自动化派棒(现状,唯一在用) 可以无人值守地让"下一棒"跑起来 —— 新起一个会话,不是续跑旧会话 粒度最细 1 小时(FREQ 硬白名单 HOURLY/DAILY/WEEKLY/MONTHLY/YEARLY);会话跑完即 completed,不保留为长期 main
B idle 钩子 → runs(上一棒 §7 方案③) 只能投给"此刻活着的"会话;钩子侧 0 token、0 会话 🔴 命中率受 §6 的窗口限制;被活的后台任务静默压制;且投递目标必须活着 ⇒ 不能当无人值守的主干,最多当"兜底加急"
C 外部常驻 → runs 同样只能投给活着的会话 要长期养一个常驻进程(本项目一向慎入:独立进程读不到宿主口令)
D 把某个会话"钉住"不让它退 若能让目标会话的 worker 不退出,A/B 才有稳定落点 目前没有外部手段能做到(复活动因在桌面端 pipe 上,外部接不上);且"钉住"本身需要改宿主行为 ⇒ 属改全局/改产品,须用户拍板

我选了什么(可推翻):以 A 为唯一主干(继续用"自动化派棒",本棒就是它叫起来的),B 降级为可选兜底、且不建任何新的常驻。 理由:A 的可靠性来自它不依赖任何"活着的会话";B/C/D 三条都建在同一段随时会断的窗口上,把它们当主干等于把整条链的稳定性押在 28 s–6 min 的运气上。


8. 未决 / 留给下一棒(本棒不做完)

  1. code=1 静默退出的根因未定 —— 已知:stderr tail: <empty>、signal=none、非人为 kill;未知:是"回合结束被主动回收"还是"崩溃"。这是唯一能改变上面判定的未知量:若它是"回合结束即回收",则 B 路线结构性不成立(不是概率问题);若它是崩溃且为周期性,则要另找稳定性讨论。建议下一棒只做一件事:拿 6ecf6d98(今天退出最频的会话)在其一次退出前后取全套四件套 + 它的 sdk/conversations/6ecf6d98….log,判定"退出是主动还是被动"。
  2. resolve_main 前缀判据缺陷 A/B(旧账,机制层,须独占锁单独一棒)。
  3. stop-collab.py::_gw_port() 会把 20090(设备接入垫片)误认成网关(真网关是动态口)。⚠️ 本棒复测又踩到同一型:20090 的根页同样带网关标记,但 health(no-auth)=200 ⇒ 唯一分水岭仍是"不带凭据时 health 必须 401"。

9. 出处(按需读)

  • 端到端脚本(本棒临时件):tmp/_wake4_runs_e2e.py;网关枚举:.workbuddy/collab/wake-session.py --list
  • 目标会话基线:E:/ProgramData/.workbuddy/projects/e-ProgramData-AIProject-ai1net-dsh-server/706d2476-93fb-439a-9742-22e009915331.jsonl
  • 宿主日志:E:/ProgramData/.workbuddy/logs/2026-09-30/workbuddyMainThread__22be230a230dcc2f1d23d3d261812acb.log ([CliPrewarmPool] doActivate / activated prewarm entry exited unexpectedly;今天 28 条)
  • 源码:app.asar → cli/dist/codebuddy-headless.js(网关控制器 ≈@7.10M;createRun ≈@6435743;GatewayAcpBridge.getOrCreateSession ≈@6232k) + app.asar → main/cli-prewarm-pool.js(child.on("exit") ≈@922 行·折行后)
  • 取证工具:tmp/wb-phone/asar-peek.mjs(find / slice / extract)+ tmp/wb-phone/reflow.mjs
  • 上游:交付物/唤醒-idle钩子坐实-20260930.md(§3 机制全貌 / §6 修正 / §9.1)|接续包_唤醒机制探索_20260930.md