Files
dsh_ai1net_server/交付物/查唤醒为什么断-结论-20260930.md
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

13 KiB
Raw Permalink Blame History

查「唤醒为什么断了」· 结论与读数

  • 出具:接续会话(接主会话)· 2026-09-30 12:0x–12:2x
  • 输入口径:用户原话「还是会发呆,上次唤醒 56分钟前」「处理完成后 创建接续会话继续 排查为什么唤醒断了」
  • 接续包:交付物/接续包-查唤醒为什么断-20260930.md
  • 🔴 只做了这一件:查清定时闹钟为什么停

0. 一句话结论

「唤醒」不是断了 —— 它从头到尾一次都没响过。

三个任务(bbaf9baf 08:47 / b08da6c4 11:47 / 91f8baeb 11:53)都是「建了就等于没建」: 网关 API 回 200 + 一个 id,但任务既没落盘、也没进内存,任何东西都不会触发它。

根因(源码级,两处独立证据):

  1. WorkBuddy 桌面端在给每个 CLI agent 进程拼环境时硬编码 CODEBUDDY_DISABLE_CRON: "1" (app.asar → /main/code-cache.js:buildAgentCliRuntimeEnv() 与 buildCliEnvFromResolved() 两处)。
  2. CLI 侧调度器 CronSchedulerService.start(session) 的第一行就是: if ("1" === process.env["CODEBUDDY_DISABLE_CRON"]) return; ⇒ 调度器根本不启动 ⇒ 排进去的任务永远不会到点。
  3. 并且 start() 同时是唯一调用 setStorageDir() 的地方 —— 被这行 return 跳过之后,durable 分支写入 undefined 目录 ⇒ 静默空操作(下节)。

⇒ 结论:WorkBuddy 桌面端下,网关 scheduled-tasks 是一条「返回成功但什么都不发生」的死能力。 ⇒ 因此「发呆 57 分钟」不是"闹钟坏了",而是这台机器上从来没有任何时间驱动的叫醒器。


1. 决定性读数(三条,全部实测)

# 要查的点 读数 判读
1 新任务会不会响(接续包第 1 步) ls tmp/supervise-inbox/_wake.stamp → No such file or directory 到 12:0x 为止,b08da6c4(11:47)与 v3 91f8baeb(11:53)一次都没来过
2 宿主是否关掉了 cron 本机进程环境 CODEBUDDY_DISABLE_CRON = '1' 见 §0 证据 1(宿主拼 env 时写死)
3 三个网关口查任务 55283 / 56510 / 56975 全部 {"data":{"tasks":[]}} 任务确实不在任何存储里(§2 解释为什么"空列表"这次不是假象)

2. durable 的真相 —— 推翻接续包里的假设

CLI 源码 CronStateManagerService.createTask(codebuddy-headless.js):

async createTask(sessionId, cron, prompt, recurring, durable) {
  const id = randomUUID().slice(0, 8);
  const task = { id, cron, prompt, createdAt: Date.now(),
                 ...(recurring ? {recurring:true} : {}),
                 ...(durable   ? {durable:true}   : {}) };
  if (durable) {
    if (this.storageDir) {                       // 🔴 只有一个 if,**没有 else**
      const list = await readTasks(this.storageDir);
      list.push(task);
      await writeTasks(list, this.storageDir);
    }
  } else {
    this.addSessionTask(sessionId, task);        // ← durable 走不到这里
  }
  this.setEnabled(true);
  return id;                                     // ⇒ 照样返回 id,照样 200
}
  • storageDir 只在 CronSchedulerService.start() 里被赋值,而 start() 已被 DISABLE_CRON 提前 return 掉 ⇒ storageDir === undefined ⇒ durable:true ⇒ 两个分支都不进 ⇒ 返回 id,然后什么都没有。
  • ⚠️ 接续包事实 3「durable 的永远不显示在列表里」不准确:源码 getAllTasks() 明确会 [...会话内存任务, ...(storageDir ? 读文件 : []) 并标 permanent:true]。 正确说法是:列表空 ⇒ 文件里真没有(因为 durable 分支压根没写)。 这反而是佐证:.codebuddy/scheduled_tasks.json 全盘不存在(find 无命中,.codebuddy/ 下只有 rules/)。
  • 🔴 另发现:durable:false(默认值)是纯内存、且按进程隔离 —— 会话结束即消失。

实测复现(12:04,用完即删)

动作 结果 说明
POST non-durable,cron="0 3 1 1 *"(1月1日,今天绝不可能触发) 200 + id;随后 GET 立刻看得到 控制器本身正常
在 55283 / 56975 / 56510 各建一条 每个口只看到自己建的那条 ⇒ 任务状态是进程内的,投到哪个口只在那个进程有效
POST durable:true(55283) 200 + id 95087b61;GET 看不到;.codebuddy/scheduled_tasks.json 仍不存在 🔴 静默空操作,与 bbaf9baf/b08da6c4/91f8baeb 完全同型
DELETE 三条 non-durable 204,列表回 [] 已清理干净

3. 顺带纠正的两处旧读数(会误导后续判断)

旧读数 实际 影响
登记册写「08:53 端口已变成 28317」 28317 不是网关 —— 它 /api/v1/health 返回 {"ret":1,"version":"3"},是别家的服务。真网关=56975(pid 50716),uptime≈41,850s(≈11.6 h)从未重启 那条旧读数会让人以为"网关重启导致任务丢",是假线索;真正的死因是 env(§0)
「_gw_port() 扫到的口就是主会话的口」 本机有 3 个都回 AUTH_REQUIRED 的网关口:55283(pid 56072) / 56510(pid 54364) / 56975(pid 50716);_gw_port() 升序取第一个命中 ⇒ 实际命中 55283(一个 --prewarm 池进程),不是托管主会话的那个进程 ⛔ 即便将来 cron 能用,往"扫到的第一个口"投任务也是错的

4. 泳道图 —— 问题落在哪一格

阶段        用户/主会话            网关 API 进程             CLI 调度器            磁盘
────────────────────────────────────────────────────────────────────────────────────────
① 建设   │  POST scheduled-   │  路由命中,校验通过   │                    │
         │  tasks(durable=1)  │  createTask()         │                    │
         │                    │  storageDir === undef │                    │
         │  ← 200 {id:...} ✅  │  ⇒ 什么也没写 🔴      │                    │  无文件 🔴
────────────────────────────────────────────────────────────────────────────────────────
② 到点   │   (发呆 57 分钟)  │   (无关)            │  start() 第一行     │
         │                    │                       │  DISABLE_CRON=1 ⇒   │
         │                    │                       │  ⛔ return,未启动   │
         │  ← 没有心跳 🔴      │                       │  ⇒ 从不触发 🔴      │
────────────────────────────────────────────────────────────────────────────────────────
③ 排查   │  GET scheduled-    │  getAllTasks()        │                    │  读文件
         │  tasks?sessionId=  │  → 内存[] + 文件[]    │                    │  → 空
         │  ← {"tasks":[]} ❌ │  ⇒ 回空列表 🟡        │                    │  ⇒ 看着像"消失了"
────────────────────────────────────────────────────────────────────────────────────────
                                  🔴 红格①:写入被静默吞掉(durable 无 storageDir)
                                  🔴 红格②:调度器被 env 提前 return
                                  🟡 黄格③:读接口回空 ⇒ 与"任务丢了"无法区分

5. 事故链图 —— 这次实际怎么坏的一步步

T1  08:47  主会话 POST 建 bbaf9baf(*/5, durable:true)
            └─ 命中 55283(prewarm 池进程,非主会话宿主)
            └─ storageDir=undefined ⇒ 不落盘、不进内存 🔴  ← 真正的失事点,此刻已经完蛋
T2  08:47→10:44  期间主会话被 --tick(钩子事件驱动)偶尔叫醒 ⇒ 误以为"闹钟在响"
T3  10:44  --supervise 孤儿进程最后一次投递后死亡 ⇒ 唯一还在响的东西没了
T4  10:44→11:42  主会话闲置 57 分钟,一条心跳都没有  ← 用户看到「上次唤醒 56 分钟前」
T5  11:43  排查:GET /scheduled-tasks → tasks:[] ⇒ 误判为"任务被网关弄丢了"
T6  11:47  重建 b08da6c4(同样 durable:true)⇒ 同样静默空操作 🔴 白建
T7  11:53  再建 v3 91f8baeb ⇒ 同样 🔴 白建
T8  12:0x  本棒:直接读源码 + 本机进程 env ⇒ 定位到 DISABLE_CRON 硬编码

本该拦住却缺席的三道防线:
 ① 缺"建完立刻回读" —— POST 后没有 GET 一次核实;有的话 T1 当场就暴露(列表本来就该能看到)
 ② 缺"到点自证" —— 11:47 重建时没先做一次 2 分钟后的最小实验,而是又建了一条 5 分钟循环件
 ③ 缺"成败判据" —— 把"_gw_port 扫到第一个口"当成"主会话的口",从未验证过投递目标

6. 「能」与「不能」—— 下一步建议(⛔ 我没动手,只报)

A. 5 分钟级时间驱动唤醒 —— 当前支持面里做不到

通道 能不能 5 分钟 为什么
网关 scheduled-tasks ❌ 设计上关闭 桌面端硬编码 CODEBUDDY_DISABLE_CRON=1(本报告结论)
WorkBuddy 自动化 ❌ 最小 1 小时 rrule 只支持 HOURLY / DAILY / WEEKLY / MONTHLY / YEARLY(且 HOURLY 的 INTERVAL≥1)
钩子(collabd --tick) ❌ 闲时必不响 事件驱动:只有"有会话在跑工具调用"时才有事件
常驻进程 ❌ 拿不到口令 不在 WorkBuddy 进程树内 ⇒ 读不到网关口令 ⇒ 只能做只读判定/告警,不能派活/投递

B. 1 小时级唤醒 —— 可行,且是官方路径(推荐先报用户拍板)

做法:建一条自动化(recurring,每小时),prompt 只做两步: ① 写 tmp/supervise-inbox/_wake.stamp(让看板可见) ② 用网关 POST /api/v1/sessions/fe146dd9-f5d1-40a2-80e8-113618dc0f63/reply { "text": "…" } 把主会话叫醒 (自动化进程在宿主进程树内 ⇒ 有 CODEBUDDY_GATEWAY_PASSWORD;本棒已实测该变量存在,长度 43)

  • 优点:官方通道、不碰网关 cron、能自动跟随(写死 sessionId 即可)
  • 缺点:最小粒度 1 小时(用户的"5 分钟"满足不了);每小时都会烧一次 token;与用户既有口径 「又给我整到自动任务去了」直接冲突 ⇒ 必须用户拍板,不许自行创建

C. 把这条"隐形执行体"的路封掉 —— 建议直接做(低成本、防再次踩坑)

  1. gateway-schedules.json:现役 91f8baeb(及 _历史 里两条)标注「cron 在桌面端被 env 关闭 ⇒ 从未触发」, 现役栏清空(它已是幽灵)。
  2. 技能 workbuddy-extension-surface §1.4:补一句硬提示 —— 「桌面宿主硬关 cron ⇒ 该 API 在桌面端是死能力, 只能在 CLI --serve 且未设该 env 的场景下用」。(本棒已就地补上。)
  3. 以后不要再用网关 scheduled-tasks 当定时器;要时间驱动就走 §B(自动化)或承认做不到(§A)。

7. 边界遵守情况(逐条)

边界 是否守住
⛔ 不动看板 127.0.0.1:8788 ✅ 未触碰
⛔ 不动中继客户端 DSH-Overlay-Node-Dev ✅ 未触碰
⛔ 口令不落盘 / 不进日志 / 不回显 ✅ 只从 env 读、只放请求头;全部输出均已确认无口令
⛔ 不改 A 方案口令入站通道 ✅ 未改
⚠️ 重启网关/宿主先问用户 ✅ 未重启(未做任何重启动作)
探测用任务用完即删 ✅ 3 条 non-durable 已 DELETE → 204,残留列表均回 [];1 条 durable 本就是空操作
锁 ✅ --claim-exec 接续-查唤醒断因-20260930 --domains ai1net-dsh-server/,收尾已释放

8. 关键文件 / 命令索引

用途 位置
本报告 交付物/查唤醒为什么断-结论-20260930.md
要盯的触发戳(它不存在 = cron 从没响) tmp/supervise-inbox/_wake.stamp
隐形执行体登记册(3 条全部作废) .workbuddy/collab/gateway-schedules.json
探测脚本(可重跑,安全:cron 指向 1月1日) tmp/_wake_probe.py(探)· tmp/_cleanup.py(清)
复现源码证据(已按收尾规矩删掉抽取件,重取只需两条命令) ① CLI:node tmp/wb-phone/asar-peek.mjs find "DISABLE_CRON" 2 "codebuddy-headless";slice "codebuddy-headless" 3813542 26000 / 4828300 14000 / 6587000 9000 ② 宿主:slice "main/code-cache.js" 3366800 3600 与 3459800 3600(命中 buildAgentCliRuntimeEnv / buildCliEnvFromResolved)
源码读取工具 tmp/wb-phone/asar-peek.mjs(按需读 asar,不整包解压)+ reflow.mjs(折行后再 Read 才不被截断)