From 9b839e7a02104d555c6fa09c48a54a8367c10522 Mon Sep 17 00:00:00 2001 From: maogeigei Date: Thu, 15 Oct 2026 22:44:59 +0800 Subject: [PATCH] =?UTF-8?q?session-mechanism=EF=BC=9A=E5=86=B3=E7=AD=96?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E8=A1=A5=E6=AF=8F=E8=BD=AE=E9=80=9A=E8=B7=AF?= =?UTF-8?q?=20=EF=BC=8B=20preflight-lock=20=E8=A1=A5=E5=9B=9E=E8=90=BD?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户问:「为什么决策方法 每次都要说一遍才会加载,是否需要把它纳入 hook」 一、根因(实测,⛔ 不是猜) `decision-rules-hook.py` 早就存在、也早就接线(SessionStart,install.py 声明表第 11 条), 但它**只挂 SessionStart** ⇒ 会话开头注入一次,而**长会话被压缩后那段会一起丢掉**。 本机实测:一个由压缩接续而来的会话,上下文里**没有**那段注入。 用户的会话基本都很长 ⇒ 每次都丢 ⇒ 外在就是「不点名就不加载」, 而 `skill-load-guard.py` 只在**点名时**才提醒 ⇒ 形成「必须说一遍」的循环。 二、修法(零新增进程) `decision-rules-hook.py` 增加 `UserPromptSubmit` 分支,注入极短版 `RULES_BRIEF`(四行); 由 `prompt-guards.py`(UserPromptSubmit 的多守卫合并入口)把本文件作为**第 5 个 guard** 合进同一进程调用 —— 实测 `decision=0.002 s`,⛔ 不新增 Python 冷启动 (该入口存在的理由正是"别再往 UserPromptSubmit 上串新进程")。 全量仍在 SessionStart 那份;每轮只补"最常被违反的四条"。 三、preflight-lock.sh 补回落修复(22799fc 漏了它) `DOCS_ROOT` 的回落原与 `handoff-guard.sh` 同款(`$_SM_HERE/..` = 回落进技能包 `scripts/`) ⇒ 现改为「取不到就如实报错 + 留痕 + 退出码 2」。 ⚠️ `handoff-guard.sh` / `lock-guard-hook.py` / `collabd.py` / `install.py` 四处已由 22799fc 落地。 验收: · 语法:两个 .py 走 ast.parse、两个 .sh 走 bash -n(PortableGit 1.3.0)—— 全过 · 实跑 `prompt-guards.py`(UserPromptSubmit 载荷):注入 2387 字符,含「决策判据 / 技术实现自己定 / 只提报用户三类 / 回复排版闸门」四项;各 guard 耗时 0.002–0.013 s · 实跑 `handoff-guard.sh --status` ⇒ `root=D:/github/dsh_shenxian/dsh-server-docs`(正确落文档库,⛔ 不是技能包) · 实跑 `collabd.py --domain-status` ⇒ 正常列出在册域锁(读侧已能读到锁) · 全量自测 PASS 110 / FAIL 0 --- .../scripts/hooks/decision-rules-hook.py | 59 ++++++++++++++++--- .../scripts/hooks/prompt-guards.py | 6 ++ .../scripts/lock/preflight-lock.sh | 15 ++++- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/session-mechanism/scripts/hooks/decision-rules-hook.py b/session-mechanism/scripts/hooks/decision-rules-hook.py index 2620128..59b3f4f 100644 --- a/session-mechanism/scripts/hooks/decision-rules-hook.py +++ b/session-mechanism/scripts/hooks/decision-rules-hook.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""decision-rules-hook.py —— `SessionStart` 钩子:**把决策判据常驻注入每一次对话**。 +"""decision-rules-hook.py —— 决策判据**双通路注入**:`SessionStart` 全量 + `UserPromptSubmit` 极短。 ## 🔴 为什么要有这个脚本(2026-10-06 用户明令) @@ -8,6 +8,19 @@ > 「**决策方法 必须想办法 加载到每次对话中**」 > 「要把这个动作 加到 会话技能配置环境的时候一并处理」 +## 🔴 2026-10-15 补每轮通路(用户报障:「为什么决策方法 每次都要说一遍才会加载」) + +**根因(实测)**:原先只挂 `SessionStart` —— 它在**会话开头注入一次**,而 +**长会话被压缩后那段注入会一起丢掉**(本机实测:一个由压缩接续而来的会话, +上下文里**没有**那段 SessionStart 注入)。用户的会话基本都很长 ⇒ **每次都丢** +⇒ 外在就是「不点名就不加载」,而 `skill-load-guard.py` 只在**点名时**才提醒, +于是形成"必须说一遍"的循环。 + +**修法**:本脚本增加 `UserPromptSubmit` 分支,注入 `RULES_BRIEF`(极短版)。 +⚠️ 它**不单独占一条进程** —— 由 `prompt-guards.py`(UserPromptSubmit 的多守卫合并入口) +把本文件作为第 5 个 guard 合进同一进程调用(实测 `decision=0.002s`,零新增冷启动)。 +⇒ 全量仍在会话开头那份,每轮只补"最常被违反的四条"。 + **改前的缺口(实测取证)**: · 决策判据(`U1–U28` / `A1–A25`)原本只住在 `skills/dsh-decision/references/` 里; @@ -28,11 +41,14 @@ ⇒ 本次逐字搬入 `references/04-决策方法论.md`(+ `dsh-decision-method/` 三份素材库), 本脚本的指针**指向包内**,⛔ 不再指向 `dsh-decision`。 -## 为什么挂 `SessionStart` 而不是 `UserPromptSubmit` +## 两个通路各管什么(2026-10-15 定,⛔ 别再按"只挂 SessionStart"读) - · `SessionStart` **每开一个会话注入一次** ⇒ 规矩"从会话开头就在", - 且**每轮不重复注入**(不白烧 token); - · `UserPromptSubmit` 每轮都跑 ⇒ 同一段文字被灌 N 遍,开销大且会稀释注意力。 + · **`SessionStart`**:注入**全量** `RULES`(U27/A6 那一整套)。会话开头一次, + **每轮不重复**(不白烧 token)。 + · **`UserPromptSubmit`**:注入**极短** `RULES_BRIEF`(四行,见文件下方)。 + 存在的唯一理由是上面那条实测 —— SessionStart 那份**在长会话压缩后会丢**。 + 成本实测 **0.002 s**,且**不新增进程**(由 `prompt-guards.py` 合并调用)。 + ⚠️ 两者**不重复也不冲突**:全量那份讲全,极短那份只放"最常被违反的四条"。 · ⚠️ 与既有 `decision_bridge.py`(也挂 SessionStart)**职责不重叠**: 那个注入的是**《提问规范》骨架**("要问的时候怎么写"); 本脚本注入的是**决策判据**("怎么想、怎么定、什么不许")。 @@ -166,6 +182,25 @@ RULES = ( ) +# ── 🔴 2026-10-15 补:`UserPromptSubmit` 用的**极短版**(走每轮通路)────────────── +# 为什么必须补这一条(实测根因,2026-10-15): +# `SessionStart` 的注入**只在会话开头发生一次**,而**长会话被压缩后那段会一起丢掉** +# (本机实测:一个由压缩接续而来的会话,上下文里**没有**那段 SessionStart 注入)。 +# 用户的会话基本都很长 ⇒ **每次都丢** ⇒ 外在表现就是「不点名就不加载」, +# 而 `skill-load-guard.py` 只在**点名时**才提醒 ⇒ 形成"必须说一遍"的循环。 +# 为什么这里只放**极短版**:`UserPromptSubmit` **每轮都跑**, +# 全文灌 N 遍既烧 token 又稀释注意力(原注释的顾虑是对的,本条是"够用就停"的折中)。 +# 全文仍由 `SessionStart` 那份承担;这里只放"最常被违反的三条"。 +RULES_BRIEF = ( + "【决策判据 · 每轮提醒(正本 references/04-决策方法论.md)】\n" + "· **技术实现自己定**(框架/库/目录结构/命名/参数/部署/排查)⇒ 写成陈述句" + "(「我选了什么,可推翻」),⛔ **不做成选项让用户选**。\n" + "· **只提报用户三类**:① 功能语义分叉(用户能感知)② 红线门禁 ③ 超出决策边界。\n" + "· ⛔ 已定项混进待拍板清单 = 变相征询;⛔ 不用「你看呢/等你发话」收尾。\n" + "· **先取证再结论**:用户原话 > 本机实测 > 文件在 > 文档说 > 推断。\n" +) + + def main() -> int: raw = _read_stdin_text() try: @@ -178,10 +213,20 @@ def main() -> int: event = str(payload.get("hook_event_name") or payload.get("hookEventName") or "") session_id = str(payload.get("session_id") or "") - # 🔴 只认 SessionStart。别的档位走到这里 ⇒ 直接合法地什么都不做 + # 🔴 2026-10-15 补:**每轮通路**(`UserPromptSubmit`)—— 只注入极短版。 + # 理由见 `RULES_BRIEF` 上方注释:SessionStart 那份在长会话被压缩时会丢,必须有一条每轮的兜底。 + if event == "UserPromptSubmit": + _log("inject-brief session_id=%s len=%d" % (session_id or "-", len(RULES_BRIEF))) + _emit({"hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": RULES_BRIEF, + }}) + return 0 + + # 🔴 全文档:只认 `SessionStart`。别的档位走到这里 ⇒ 直接合法地什么都不做 # (本脚本会被挂到哪个档由 `install.py` 的声明表决定;⛔ 不在这里猜)。 if event != "SessionStart": - _log("skip event=%s(本钩子只处理 SessionStart)" % (event or "-")) + _log("skip event=%s(本钩子只处理 SessionStart + UserPromptSubmit)" % (event or "-")) return 0 _log("inject session_id=%s len=%d" % (session_id or "-", len(RULES))) diff --git a/session-mechanism/scripts/hooks/prompt-guards.py b/session-mechanism/scripts/hooks/prompt-guards.py index a6b6983..97c76ab 100644 --- a/session-mechanism/scripts/hooks/prompt-guards.py +++ b/session-mechanism/scripts/hooks/prompt-guards.py @@ -58,11 +58,17 @@ HERE = os.path.dirname(os.path.abspath(__file__)) # 🔴 合并的守卫清单(⛔ 顺序=执行顺序;都是 fail-open 的"补充信息/提醒"类) # ⚠️ **不含** `supervise-ensure-hook` / `wb-result-hook` / `decision_bridge` —— # 它们有副作用(投递/常驻确保)或属别处接线,**合并会改变语义**,一律不碰。 +# 🔴 2026-10-15 增第 5 条 `decision-rules-hook.py`(**决策判据的每轮通路**): +# 它**零副作用**(纯本地字符串,不读文件/不起进程/不写任何东西)⇒ 符合本入口的合并条件。 +# 为什么必须走每轮:该钩子原先只挂 `SessionStart`(会话开头注入一次), +# 而**长会话被压缩后那段注入会一起丢掉** ⇒ 实测出现「不点名就不加载决策方法」的循环。 +# 本文件承担它的**极短版**(`RULES_BRIEF`);全文仍由 `SessionStart` 那份注入。 GUARDS = ( "reply-style-guard.py", "stop-dialog-guard.py", "skill-load-guard.py", "session-log-guard.py", + "decision-rules-hook.py", ) diff --git a/session-mechanism/scripts/lock/preflight-lock.sh b/session-mechanism/scripts/lock/preflight-lock.sh index 452ae3f..940a8a9 100644 --- a/session-mechanism/scripts/lock/preflight-lock.sh +++ b/session-mechanism/scripts/lock/preflight-lock.sh @@ -27,7 +27,20 @@ _SM_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" _SM_PKG="$(cd "$_SM_HERE/../.." && pwd)" if [ -f "$_SM_PKG/env/roots.env" ]; then . "$_SM_PKG/env/roots.env" elif [ -f "$_SM_PKG/roots.env" ]; then . "$_SM_PKG/roots.env"; fi -DOCS_ROOT="${DSH_DOCS_ROOT:-$(cd "$_SM_HERE/.." && pwd)}" +# 🔴 2026-10-15 修(第 11 条禁令 + D 盘实测事故):原写 +# `DOCS_ROOT="${DSH_DOCS_ROOT:-$(cd "$_SM_HERE/.." && pwd)}"` —— 回落值是 +# `$_SM_HERE/..` = **本技能包自己的 `scripts/`** ⇒ 锁根落进技能包。 +# ✅ 现行:取不到 ⇒ **如实报错 + 留痕 + 退出码 2**,⛔ 不回落到包内、⛔ 不猜路径。 +if [ -z "${DSH_DOCS_ROOT:-}" ]; then + _SM_ERR="${_SM_PKG}/logs/preflight-lock.err.log" + mkdir -p "$(dirname "$_SM_ERR")" 2>/dev/null || true + printf '%s ⛔ 取不到 DSH_DOCS_ROOT(本包 env/roots.env 缺该键)⇒ 退出码 2\n' \ + "$(date '+%Y-%m-%d %H:%M:%S')" >> "$_SM_ERR" 2>/dev/null || true + printf '⛔ 取不到文档库根(DSH_DOCS_ROOT)—— 锁根无法定位 ⇒ 停手(退出码 2)。\n' >&2 + printf ' ✅ 修法:把本工作区根写进 <技能包>/env/roots.env 的 DSH_DOCS_ROOT,或跑 install.py --apply。\n' >&2 + exit 2 +fi +DOCS_ROOT="$DSH_DOCS_ROOT" CODE_ROOT="${DSH_CODE_REPO:-${DOCS_ROOT%/*}}" # ⛔ 不留盘符:代码库=文档库的上一级(纯 bash 展开,⛔ 不调 dirname) WS_ROOT="${DSH_WS_ROOT:-$(pwd)}" # ⛔ 不留盘符:默认=当前工作目录(本文件内未被使用) LOCKROOT="$DOCS_ROOT/05-交接单/.locks"