session-mechanism:决策判据补每轮通路 + preflight-lock 补回落修复

用户问:「为什么决策方法 每次都要说一遍才会加载,是否需要把它纳入 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
This commit is contained in:
admin committed 2026-10-15 22:44:59 +08:00
1 parent 22799fc9c2
commit 9b839e7a02
3 files changed
+72 -8

No files matched your search

@@ -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)))
@@ -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",
)