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:
1 parent
22799fc9c2
commit
9b839e7a02
3 files changed
+72
-8
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""decision-rules-hook.py —— `SessionStart` 钩子:**把决策判据常驻注入每一次对话**。
|
"""decision-rules-hook.py —— 决策判据**双通路注入**:`SessionStart` 全量 + `UserPromptSubmit` 极短。
|
||||||
|
|
||||||
## 🔴 为什么要有这个脚本(2026-10-06 用户明令)
|
## 🔴 为什么要有这个脚本(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/` 里;
|
· 决策判据(`U1–U28` / `A1–A25`)原本只住在 `skills/dsh-decision/references/` 里;
|
||||||
@@ -28,11 +41,14 @@
|
|||||||
⇒ 本次逐字搬入 `references/04-决策方法论.md`(+ `dsh-decision-method/` 三份素材库),
|
⇒ 本次逐字搬入 `references/04-决策方法论.md`(+ `dsh-decision-method/` 三份素材库),
|
||||||
本脚本的指针**指向包内**,⛔ 不再指向 `dsh-decision`。
|
本脚本的指针**指向包内**,⛔ 不再指向 `dsh-decision`。
|
||||||
|
|
||||||
## 为什么挂 `SessionStart` 而不是 `UserPromptSubmit`
|
## 两个通路各管什么(2026-10-15 定,⛔ 别再按"只挂 SessionStart"读)
|
||||||
|
|
||||||
· `SessionStart` **每开一个会话注入一次** ⇒ 规矩"从会话开头就在",
|
· **`SessionStart`**:注入**全量** `RULES`(U27/A6 那一整套)。会话开头一次,
|
||||||
且**每轮不重复注入**(不白烧 token);
|
**每轮不重复**(不白烧 token)。
|
||||||
· `UserPromptSubmit` 每轮都跑 ⇒ 同一段文字被灌 N 遍,开销大且会稀释注意力。
|
· **`UserPromptSubmit`**:注入**极短** `RULES_BRIEF`(四行,见文件下方)。
|
||||||
|
存在的唯一理由是上面那条实测 —— SessionStart 那份**在长会话压缩后会丢**。
|
||||||
|
成本实测 **0.002 s**,且**不新增进程**(由 `prompt-guards.py` 合并调用)。
|
||||||
|
⚠️ 两者**不重复也不冲突**:全量那份讲全,极短那份只放"最常被违反的四条"。
|
||||||
· ⚠️ 与既有 `decision_bridge.py`(也挂 SessionStart)**职责不重叠**:
|
· ⚠️ 与既有 `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:
|
def main() -> int:
|
||||||
raw = _read_stdin_text()
|
raw = _read_stdin_text()
|
||||||
try:
|
try:
|
||||||
@@ -178,10 +213,20 @@ def main() -> int:
|
|||||||
event = str(payload.get("hook_event_name") or payload.get("hookEventName") or "")
|
event = str(payload.get("hook_event_name") or payload.get("hookEventName") or "")
|
||||||
session_id = str(payload.get("session_id") 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` 的声明表决定;⛔ 不在这里猜)。
|
# (本脚本会被挂到哪个档由 `install.py` 的声明表决定;⛔ 不在这里猜)。
|
||||||
if event != "SessionStart":
|
if event != "SessionStart":
|
||||||
_log("skip event=%s(本钩子只处理 SessionStart)" % (event or "-"))
|
_log("skip event=%s(本钩子只处理 SessionStart + UserPromptSubmit)" % (event or "-"))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
_log("inject session_id=%s len=%d" % (session_id or "-", len(RULES)))
|
_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 的"补充信息/提醒"类)
|
# 🔴 合并的守卫清单(⛔ 顺序=执行顺序;都是 fail-open 的"补充信息/提醒"类)
|
||||||
# ⚠️ **不含** `supervise-ensure-hook` / `wb-result-hook` / `decision_bridge` ——
|
# ⚠️ **不含** `supervise-ensure-hook` / `wb-result-hook` / `decision_bridge` ——
|
||||||
# 它们有副作用(投递/常驻确保)或属别处接线,**合并会改变语义**,一律不碰。
|
# 它们有副作用(投递/常驻确保)或属别处接线,**合并会改变语义**,一律不碰。
|
||||||
|
# 🔴 2026-10-15 增第 5 条 `decision-rules-hook.py`(**决策判据的每轮通路**):
|
||||||
|
# 它**零副作用**(纯本地字符串,不读文件/不起进程/不写任何东西)⇒ 符合本入口的合并条件。
|
||||||
|
# 为什么必须走每轮:该钩子原先只挂 `SessionStart`(会话开头注入一次),
|
||||||
|
# 而**长会话被压缩后那段注入会一起丢掉** ⇒ 实测出现「不点名就不加载决策方法」的循环。
|
||||||
|
# 本文件承担它的**极短版**(`RULES_BRIEF`);全文仍由 `SessionStart` 那份注入。
|
||||||
GUARDS = (
|
GUARDS = (
|
||||||
"reply-style-guard.py",
|
"reply-style-guard.py",
|
||||||
"stop-dialog-guard.py",
|
"stop-dialog-guard.py",
|
||||||
"skill-load-guard.py",
|
"skill-load-guard.py",
|
||||||
"session-log-guard.py",
|
"session-log-guard.py",
|
||||||
|
"decision-rules-hook.py",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,20 @@ _SM_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
_SM_PKG="$(cd "$_SM_HERE/../.." && pwd)"
|
_SM_PKG="$(cd "$_SM_HERE/../.." && pwd)"
|
||||||
if [ -f "$_SM_PKG/env/roots.env" ]; then . "$_SM_PKG/env/roots.env"
|
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
|
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)
|
CODE_ROOT="${DSH_CODE_REPO:-${DOCS_ROOT%/*}}" # ⛔ 不留盘符:代码库=文档库的上一级(纯 bash 展开,⛔ 不调 dirname)
|
||||||
WS_ROOT="${DSH_WS_ROOT:-$(pwd)}" # ⛔ 不留盘符:默认=当前工作目录(本文件内未被使用)
|
WS_ROOT="${DSH_WS_ROOT:-$(pwd)}" # ⛔ 不留盘符:默认=当前工作目录(本文件内未被使用)
|
||||||
LOCKROOT="$DOCS_ROOT/05-交接单/.locks"
|
LOCKROOT="$DOCS_ROOT/05-交接单/.locks"
|
||||||
|
|||||||
Reference in new issue
Block a user