chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
This commit is contained in:
1 parent
c70d5d860e
commit
5ad755116e
173 files changed
+27632
No files matched your search
@@ -0,0 +1,105 @@
|
||||
# 99-「会话自行判断继续」失效的根因与机制补强(Stop 钩子)
|
||||
|
||||
- 日期:2026-09-15 | 状态:🔄 **已落地待生效**(钩子已装;**待一次完全重启**后当场实测)
|
||||
- 触发:用户报「之前设置的**会话根据决策方法自行判断继续处理任务**的功能执行的不太好,会话**经常忘记这个事**,又让确认很简单的问题」
|
||||
|
||||
> **TL;DR**
|
||||
> 1. **根因 = 拦截点错位**(不是"AI 不听话"):既有「提问闸门」hook 的 matcher 是 `^AskUserQuestion$`,只能拦**工具调用**;实测本工作区宿主日志 `tool=AskUserQuestion` 调用数 = **09-12: 43 / 09-13: 3 / 09-14: 0 / 09-15: 0** ⇒ 真实上抛发生在**回复正文**("要我接着做吗 / 请确认 / 说一声即可"),**零机制覆盖**。
|
||||
> 2. **补强 = `Stop` 事件钩子**(官方 27+ 事件之一,**输入带 `transcript_path`**):扫最后一条回复的**最后一行**,命中征询句式 ⇒ 返回 `{"continue": false, "reason": …}`,让 Agent **自我纠正**(该自己做的做掉 / 该问的写进「需要你拍板」一节)。
|
||||
> 3. **形态定型(实测数据,非推断)**:单 python 脚本 + **`python -S -E`** = **185 ms/轮**(对照 286 ms);**成本与转录大小无关**(70.4 MB 转录仍 286 ms)。⛔ **否决** bash 预筛层 —— 本机 `bash -c exit 0` = **1072 ms/次**,比 python 还慢 3 倍。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与动机
|
||||
|
||||
- 用户报障背景(本机 5 分钟前的会话):`63615df4`「检查项目是否支持用户自定义 skills」(07:19 起,07:30:30 仍在写)。
|
||||
- ⚠️ **诚实边界(拿不到的东西不编)**:**该会话的正文原文我拿不到** —— 会话正文只在云端;本机只有事件级日志(`tool_call` 类型、**无文本**)、`projects/` 无该 sid 的 jsonl、宿主日志只记 `tool=`。**但结论不受影响**:当天(09-15)**没有任何 `AskUserQuestion` 调用** ⇒ 那个问句只可能出现在**正文**里。
|
||||
|
||||
## 二、用户决策(本次三条,均已沉淀)
|
||||
|
||||
| 用户原话 | 沉淀 |
|
||||
|---|---|
|
||||
| 「先解决风险和问题,做到最优效果确认无误后再执行」 | 本档案 §五 的"风险与缓解" |
|
||||
| 「决策中发现方案有风险和问题,需要分析并优化到当前情况和状态下的最优解,然后进行下一步处理」 | **U26**(素材库·用户决策) |
|
||||
| 「项目推进要的是**解决问题**,不是**得过且过、将就妥协**」 | **U27**(同上;并给 **A6** 补边界:"最小代价 ≠ 降低目标") |
|
||||
|
||||
## 三、方案对比(含"不做")
|
||||
|
||||
| 方案 | 判定 | 理由 |
|
||||
|---|---|---|
|
||||
| A 只靠常驻层自检(`CODEBUDDY.md §1`「回话前自检」) | ⚠️ **不足** | **用户最初的报障就是反证**:规则已在,会话仍旧照问 ⇒ 只有"判据"没有"强制"(A24 的教训) |
|
||||
| B 把 `PreToolUse` 扩展到正文 | ❌ 不可行 | `PreToolUse` 只在**工具调用前**触发,正文不是工具 |
|
||||
| **C `Stop` 事件钩子(command)** | ✅ **采用** | **唯一**能覆盖"回复正文"的面;官方事件、输入带 transcript |
|
||||
| D 定期巡检会话日志、事后纠正 | ❌ | 事后纠正 ≠ 当场拦住;且要读会话正文(本机不存) |
|
||||
| E 不做、接受现状 | ❌ | 与 **U27** 冲突(将就妥协) |
|
||||
|
||||
## 四、实现
|
||||
|
||||
| 文件 | 内容 |
|
||||
|---|---|
|
||||
| **`scripts/stop-dialog-guard.py`(新,已入库/入镜像)** | 读 stdin 的 Stop 载荷 → 自作用域(transcript_path 含 `aliyun-dsh-server`)→ 急停双闸 → **防死循环**(`stop_hook_active`)→ 从转录**尾部**取最后一条 assistant 文本(分块扩窗 256 KB ×4,上限 4 MB)→ 只看**最后一行** + 引用豁免 → 命中则限频记账 + 写日志 + 返回 `{continue:false,reason}` |
|
||||
| `settings.json` 的 `hooks.Stop`(新增 1 条) | `"<python>" -S -E "<脚本>"`,timeout 10 s —— **只加一个键**,`PreToolUse`×2 / `SessionStart`×1 **未动**(已断言) |
|
||||
| 配套(本档案同批) | `dsh-decision-method` **v2.6.0** 新增 **U27**、给 **A6** 补边界、项目根 `CODEBUDDY.md §1` 加一行指针 |
|
||||
|
||||
## 五、验证记录(含**失败尝试**)
|
||||
|
||||
**已完成(库外合成样本,5 组 + 4 组)**:
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| 判据(收尾征询句 / 陈述句 / "需要你拍板"节 / 正文引用 / 换 sid) | **5/5** |
|
||||
| 限频(同会话 600 s 内第二次)→ 放行 | ✓(且改为**命中才记账**,修掉"只查也记账导致误吞下一轮") |
|
||||
| 急停 env / 急停闸刀文件 | ✓ / ✓ |
|
||||
| 巨行 14 MB(旧版静默漏判) | 已修:分块扩窗;超窗放行**留痕** |
|
||||
| 性能 | `python -S -E` 185 ms/轮;70.4 MB 转录 286 ms(**与大小无关**) |
|
||||
|
||||
**失败尝试(如实记)**:
|
||||
- ❌ **headless CLI 预验证两次卡死**(`codebuddy-headless.js -p` 无输出,4–5 min 后被 kill)⇒ **放弃该验证路线**(有依据的放弃,不是回避)。
|
||||
⇒ 改用**最直接的验证**:重启后在本会话**故意以征询句结尾**,观察是否被拦回、`reason` 是否注入 —— 30 秒可得结论。
|
||||
|
||||
**待做(唯一未闭环项)**:**一次完全重启** → 当场实测(见 §七)。
|
||||
|
||||
## 六、事故 / 踩坑(都是我自己埋的)
|
||||
|
||||
1. **巨行漏判**:一次性尾窗会切在 JSON 行中间 ⇒ `json.loads` 失败 ⇒ **静默放行**(实测复现)→ 改分块扩窗 + 留痕。
|
||||
2. **限频误吞**:`_rate_limited` 在"只查"时也记账 ⇒ 同会话**下一轮的合法拦截被吞** → 改 `peek=True` 只查不写。
|
||||
3. **误报一次**:正文里**引用**规则("脚本里写着「说一声即可」")被当成收尾句 → 加**引用/转述豁免**。
|
||||
|
||||
## 七、验收(重启后 30 秒)
|
||||
|
||||
1. **我**在任一回复里**故意**以「要我接着做这件事吗?」结尾 ⇒ 若钩子生效,我会被拦回并看到 `reason`(征询句判据)⇒ **L5 用户可见**。
|
||||
2. 反向用例:以陈述句结尾 ⇒ **不应**被拦(验证不误伤)。
|
||||
3 生效后我立刻回报,并把结论追加到本档案(§五)。
|
||||
|
||||
## 八、回滚 / 注意
|
||||
|
||||
- 🧯 **秒级急停(不用重启)**:新建 `<工作区>/.workbuddy/stop-guard.disabled`,或给会话进程设 `DSH_STOP_GUARD_OFF=1`。
|
||||
- 🗑 **彻底卸载**:删 `settings.json` 的 `hooks.Stop` 键 → **一次完全重启**。
|
||||
- 备份:`settings.json.bak-stopverify-075408`(首次验证前)、`settings.json.bak-stopverify2-075909`(第二次验证前)。
|
||||
- ⚠️ **与 `/goal` 并存未验证**(两者都是 Stop 类机制)⇒ 同一会话**别同时用**。
|
||||
- ⚠️ **每轮回话 +~0.2 s**(进程冷启动,压不掉);`Stop` **不支持 matcher** ⇒ 其他项目的会话也会付这一份(脚本自作用域,**行为不变**)。
|
||||
|
||||
---
|
||||
|
||||
## 修正(2026-09-15 · Stop 钩子该版本不生效 ⇒ 第二方案 `UserPromptSubmit`)
|
||||
|
||||
**实测结论(决定性,非推断)**:Stop 钩子装好并**完全重启**后(`last-launch.json` = 08:09:52、新 PID),
|
||||
用**已验证会命中**的探针句结尾,且脚本已改为「**本工作区内每次调用必写一行**」⇒ **日志仍为空**
|
||||
⇒ **WorkBuddy 5.5.6 桌面版不调用 `Stop` 钩子**。按「不留假安全感」原则**已卸载**(`hooks.Stop` 已删,备份 `settings.json.bak-stopuninstall-082503`)。
|
||||
|
||||
**第二方案:`UserPromptSubmit`**(官方文档确认其输入**同样带 `transcript_path`**,且支持
|
||||
`hookSpecificOutput.additionalContext` 注入上下文 ⇒ 覆盖同一条面:**本轮开始前**让模型看到"上一轮收尾是征询句"):
|
||||
|
||||
| 项 | 设计 |
|
||||
|---|---|
|
||||
| 触发时机 | 用户提交提示词后、模型处理前 ⇒ **每轮必触发**(不像 `Stop` 只在模型停止时) |
|
||||
| 两级模式 | **`probe`**(只写日志,零风险,用于判定"有没有被调用")→ **`inject`**(命中则注入上下文让本轮自我纠正) |
|
||||
| 切换方式 | 写 `<工作区>/.workbuddy/stop-guard-mode`(内容含 `inject` 即切)⇒ **改文件即生效、无需再重启**(只有 settings 的 hooks 条目是启动快照) |
|
||||
| 不误伤 | 只在**最后一行**是征询句时注入;陈述句 / 「需要你拍板」节 / 正文引用 均不触发(自测 **4/4**) |
|
||||
| 急停 | `DSH_STOP_GUARD_OFF=1` 或 `<工作区>/.workbuddy/stop-guard.disabled` |
|
||||
|
||||
**当前状态**:已装为 **probe 模式**(只记日志、不注入、不阻断)。**在你下一次自然重启后**它会开始写日志;
|
||||
届时读 `<工作区>/.workbuddy/stop-dialog-guard.log`:出现 `invoked(user-prompt)` 行 ⇒ 该面可用 ⇒ 我把模式切成 `inject`(改一个文件、即时生效);
|
||||
仍无行 ⇒ 该版本连这条也不调用 ⇒ 只剩「常驻规则 + 每轮自检」(已落地)这一层,届时如实收口,不再留任何"看起来装好了"的东西。
|
||||
|
||||
**顺带修掉一个真 bug(A18 教科书案例)**:本脚本 `main()` 的 `except: pass` **把一个 `NameError` 藏了半小时**
|
||||
(我调用了本文件不存在的 `out()`)⇒ 已改为**异常必留痕**(写 `EXCEPTION|…` 行)。
|
||||
Reference in new issue
Block a user