193 lines
13 KiB
Markdown
193 lines
13 KiB
Markdown
# 查「唤醒为什么断了」· 结论与读数
|
||||
|
|
|
|||
|
|
- 出具:接续会话(接主会话)· 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`):
|
|||
|
|
|
|||
|
|
```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 才不被截断) |
|