# 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 条) | `"" -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|…` 行)。