session-mechanism: 修复钩子静默失效 + 3 处判据缺陷;禁「变相征询」

1) stop-dialog-guard: session_budget() 早退路径返回 2 值、末尾返回 3 值,调用方按 3 值解包
   ⇒ transcript > 64 MiB 时每轮 ValueError。因 fail-open(异常仍 exit 0),
   宿主零报错、install.py --verify 只判 rc=0 ⇒ 假绿;实测 86 条 EXCEPTION,
   死掉的是整条(水位/收口、接续机制起点、预算告警、门禁自检、路径自检)。

2) session-rules-check 三处判据:
   · hook_reg  按旧文件名找 ⇒ 合并成 prompt-guards.py 后每轮假红 ⇒ 改为一组可接受名
   · snap_sync 拿 mtime 当内容判据 ⇒ 连续 4 天假红 ⇒ 改为复用抽取器本体比对内容
               (变异对照:截断快照能报 fail,非恒绿)
   · mem_ptr   只查全局技能根 ⇒ 工作区自带技能被判悬空 ⇒ 改查「全局 ∪ 工作区」

3) pitfalls 新增 P0-95(改判据必须重跑变异对照;fail-open + 只看 rc=0 = 假绿温床)

4) 回复排版核心块新增「变相征询同样禁止」(先只报不动/等你发话/我倾向X你看呢
   这类不带选项的待定清单,一律按待拍板项写:问题+说明+各候选优缺点+倾向)
This commit is contained in:
admin committed 2026-10-06 22:27:03 +08:00
1 parent 19101acd65
commit 64dd82073b
21 files changed
+8444 -4523

No files matched your search

+120 -29
View File
@@ -33,7 +33,7 @@ SK = Path(__file__).resolve().parent
DEFAULT_TITLE = "检查会话协作是否运行正常 + 协作机制问题排查与修复"
def _register_keeper_task(ws: Path, keeper: Path) -> str:
def _register_keeper_task(ws: Path, python_exe: Path) -> str:
"""🔴🔴 给本工作区**登记一条计划任务**(="各工作区都能自己常驻"的落点)。
## 为什么必须有这一步(2026-10-04 用户原话)
@@ -58,42 +58,59 @@ def _register_keeper_task(ws: Path, keeper: Path) -> str:
人类可读的一行结论(调用方直接打印)。⛔ 不抛异常(登记失败要能继续跑完别的步骤)。
"""
_task = "collabd-supervise-%s" % (ws.name or "ws")
_task = "collabd-keepalive-%s" % (ws.name or "ws")
# 🔴🔴 2026-10-06 收口(P0-74):动作=**`pythonw.exe` + `supervise-launch.py`**。
# 旧形态是 `powershell.exe -File start-supervise.ps1` —— PowerShell 是**控制台程序**
# ⇒ 每次触发分配 `conhost.exe` ⇒ **闪一下黑窗**(用户 2026-10-05:「又弹了窗口」)。
# ⚠️ 旧注释的理由「keeper 里要 set 那几个 env ⇒ 直接跑解释器没有设环境变量这一步」
# **已被 `supervise-launch.py` 推翻** —— 那个启动器存在的**全部目的**就是在进程内设 env。
# ⛔ **登记动作仍走 PowerShell**(那是在**登记**,不是在**当看守**,两者别混)。
_pyw = Path(python_exe).with_name("pythonw.exe")
if not _pyw.is_file(): # ⛔ 没同版本 pythonw ⇒ 找全局那份
_cands = sorted(Path(python_exe).parent.parent.parent.glob("*/pythonw.exe"))
_pyw = _cands[-1] if _cands else Path(python_exe)
_launcher = ws / ".workbuddy" / "collab" / "supervise-launch.py"
# 🔴 `-WorkingDirectory` **必须是「工作区根」**(⛔ 不是脚本目录):
# `collabd.load_cfg()` 按 `<cwd>/.workbuddy/collab/collabd.config.json` 找配置;
# cwd 设成脚本目录 ⇒ 它去找 `<ws>/.workbuddy/collab/.workbuddy/collab/…` ⇒ **找不到**
# ⇒ `CFG_MISSING` ⇒ `--supervise` 拒跑(实测 rc=2;任务侧 `LastTaskResult=1`)。
_wd = str(ws).replace("/", "\\")
_ps = (
"$ErrorActionPreference='Stop';"
"$n='%s';"
# 🔴 幂等:先注销同名(`-Confirm:$false` 免交互;宿主可能 -NonInteractive)
"if(Get-ScheduledTask -TaskName $n -ErrorAction SilentlyContinue){"
"Unregister-ScheduledTask -TaskName $n -Confirm:$false};"
# 🔴 动作=**powershell.exe -File <本区 keeper>**(⛔ 不是 pythonw):
# keeper 里要 set 那几个 env(CODEBUDDY_CONFIG_DIR / COLLABD_CONFIG)⇒
# 直接跑解释器没有"设环境变量"这一步。
"$a=New-ScheduledTaskAction -Execute 'powershell.exe' "
"-Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"%s\"' "
"-WorkingDirectory '%s';"
# 🔴 `AtLogOn` = 开机/登录即起(这一步让"跨会话"成立:⛔ 不依赖任何 WorkBuddy 会话)
"$t=New-ScheduledTaskTrigger -AtLogOn;"
# 🔴 四个设置缺一不可(少一个就前功尽弃,逐条理由见 references/supervise-persistence.md §二②):
# ExecutionTimeLimit=0 ⇒ 默认 72h 会把常驻杀掉
# 🔴 动作=`pythonw.exe` + 启动器(GUI 子系统 ⇒ 零 conhost ⇒ **不闪窗**)
"$a=New-ScheduledTaskAction -Execute '%s' -Argument '\"%s\"' -WorkingDirectory '%s';"
# 🔴 `RepetitionInterval` 5 分钟(与 `collabctl.py` 的既定口径一致)
"$t=New-ScheduledTaskTrigger -Once -At (Get-Date) "
"-RepetitionInterval (New-TimeSpan -Minutes 5);"
# 🔴 四个设置缺一不可(逐条理由见 references/supervise-persistence.md §二②):
# ExecutionTimeLimit=0 ⇒ 默认 72h 会把常驻杀掉;
# ⚠️ ⛔ 不许设 2 分钟(那会被调度器到点掐死 ⇒ 每 2 分钟重建一次 = 抖动)
# MultipleInstances=IgnoreNew ⇒ 防双写台账
# RestartCount/Interval ⇒ 脚本非零退出时重拉(⚠️ 真正的看护在 keeper 的 while 里)
# -Hidden ⇒ 计划任务库里不显眼
"$s=New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries "
"-DontStopIfGoingOnBatteries -StartWhenAvailable "
"-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew "
"-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1);"
"Register-ScheduledTask -TaskName $n -Action $a -Trigger $t -Settings $s -Force|Out-Null;"
# 🔴 立刻起一次(⛔ 别等下次登录 —— 否则"配好了"这个结论当场不可验)
"-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew -Hidden;"
"$pr=New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest;"
"Register-ScheduledTask -TaskName $n -Action $a -Trigger $t -Settings $s -Principal $pr -Force|Out-Null;"
# 🔴 立刻起一次(⛔ 别等下次触发 —— 否则"配好了"这个结论当场不可验)
"Start-ScheduledTask -TaskName $n;"
"'OK ' + $n"
) % (_task, str(keeper), str(ws))
) % (_task, str(_pyw), str(_launcher), _wd)
try:
_r = subprocess.run(["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass",
"-Command", _ps],
capture_output=True, timeout=180)
capture_output=True, timeout=180,
# 🔴 `CREATE_NO_WINDOW`(⛔ 缺了每次登记都闪一下)
creationflags=0x08000000)
_o = _r.stdout.decode("utf-8", "replace").strip()
_e = _r.stderr.decode("utf-8", "replace").strip()
if "OK " in _o:
return "✅ 已登记计划任务 `%s`(AtLogOn · ExecutionTimeLimit=0 · IgnoreNew · Restart×999)并已启动" % _task
return ("✅ 已登记计划任务 `%s`(pythonw + supervise-launch.py · 每 5 分钟 · "
"ExecutionTimeLimit=0 · IgnoreNew)并已启动" % _task)
# ⛔ 失败要**报原文**,⛔ 不许编原因
_msg = (_e or _o or "无回显").splitlines()
return ("⛔ 登记失败:%s\n ⚠️ 若提示权限/策略 ⇒ 走 PowerShell 工具手动建"
@@ -105,7 +122,7 @@ def _register_keeper_task(ws: Path, keeper: Path) -> str:
def _keeper_task_state(ws: Path) -> str:
"""读本区计划任务状态(⛔ 只读;用来做"到底建成了没"的行为级验收)。"""
_task = "collabd-supervise-%s" % (ws.name or "ws")
_task = "collabd-keepalive-%s" % (ws.name or "ws")
_ps = ("$n='%s';"
"$t=Get-ScheduledTask -TaskName $n -ErrorAction SilentlyContinue;"
"if(-not $t){'MISSING'}else{"
@@ -185,15 +202,57 @@ def main() -> int:
ws_s = str(ws).replace("\\", "/") # 🔴 正斜杠(红线)
short = a.short or ws.name
topics = [t.strip() for t in a.topics.split(",") if t.strip()]
port = a.port or (20000 + (abs(hash(ws_s)) % 5000)) # ⛔ 每区唯一
# 🔴🔴 端口**跨进程稳定**(2026-10-04 修):原来用 `hash(ws_s)`,而
# **Python 3.11+ 字符串 hash 每进程随机化**(PYTHONHASHSEED)⇒
# **同一工作区每跑一次就换一个端口** ⇒ 旧守卫/防火墙/别的配置里的端口全部对不上。
# ⇒ 改成 `zlib.crc32`(确定性、与版本无关);已有配置里写了端口就沿用它。
import zlib
port = port or (20000 + (zlib.crc32(ws_s.encode("utf-8")) % 5000))
derived = 20000 + (zlib.crc32(ws_s.encode("utf-8")) % 5000) # 按**工作区路径**算,天然各区不同
port = a.port or derived
_port_from = "按路径算出" if not a.port else "命令行指定"
print("=== 新建协作工作区:%s ===" % ws_s)
# ⚠️ 端口目录靠后生成(要读旧配置才能定),但**冲突自检**要提前做 —— 见下方 `_port_clash`。
_port_clash: list[str] = []
def _scan_peer_ports() -> dict:
"""扫**本机其它工作区**的 `singleton_port`(只读)。
🔴🔴 2026-10-05 加(用户报「各工作区环境是否该独立」时坐实):
实测三个区(`ai1net` / `vibe-product` / `agent-product`)的 `collabd.config.json`
**`singleton_port` 全是 20099**(= `collabd.py` 的**默认值**)。
真因不是算法 —— 算法按路径算,三区分别该是 `23924` / `24942` / `21826`;
真因是**前两个区的 config 是早期手工建的,一直抄着默认值**,而
`init_workspace` 的「沿用既有端口」**无条件信任**旧值 ⇒ 抄错的值**永远修不回来**。
⚠️ 后果(`init_workspace.py` 文件头早就写了):
「`singleton_port` 每个工作区一个(⛔ 复用 ⇒ 常驻单例互抢、**静默只有一个活着**)」
⇒ 这是**静默故障**:两个区都以为自己起了常驻,实际只有一个绑上端口。
做法:扫 `AIProject` 下各兄弟工作区的配置(本区**不算**),拿端口 ⇒ 撞了就报警并改用算出来的值。
"""
out: dict = {}
try:
parent = ws.parent # 通常= …/AIProject
if not parent.is_dir():
return out
for sib in parent.iterdir():
if not sib.is_dir() or sib.resolve() == ws:
continue
cp = sib / ".workbuddy" / "collab" / "collabd.config.json"
if not cp.is_file():
continue
try:
pv = int((json.loads(cp.read_text(encoding="utf-8")) or {}).get(
"singleton_port") or 0)
except Exception:
continue
if pv:
out.setdefault(pv, []).append(sib.name)
except Exception:
pass
return out
# ① 骨架
for d in (".workbuddy/collab", "tmp/supervise-inbox", "交付物", ".workbuddy/collab/logs"):
(ws / d).mkdir(parents=True, exist_ok=True)
@@ -213,8 +272,31 @@ def main() -> int:
_old_cfg = json.loads(cfgp.read_text(encoding="utf-8"))
except Exception:
_old_cfg = {}
# 🔴🔴 端口冲突自检 + 纠偏(2026-10-05 加)——
# 「沿用既有」**不能无条件信任**:实测三区 config 里 `singleton_port` 全是 20099
# (早期手工建配置时抄了 `collabd.py` 的默认值),而**谁都没报错**
# ⇒ 常驻单例互抢、静默只有一个活着。⇒ 沿用前**先比对**:
# · 旧值 == 按路径算出的值 ⇒ 正常沿用;
# · 旧值 ≠ 算出值,且**别区占了它** ⇒ 🔴 报警 + **改用算出值**(抄错的值该修);
# · 旧值 ≠ 算出值,但没有别区占用 ⇒ **沿用**(可能是刻意指定的,⛔ 不擅改)。
_bad_port = False
if _old_cfg.get("singleton_port"):
port = int(_old_cfg["singleton_port"]) # 沿用既有端口(⛔ 换了会让别的引用失效)
_old_port = int(_old_cfg["singleton_port"])
_peers = _scan_peer_ports()
if _old_port == derived:
port, _port_from = _old_port, "沿用既有(与算出值一致)"
else:
_who = _scan_peer_ports().get(_old_port) or []
if _who:
_bad_port = True
print(" 🔴 **端口冲突**:本区既有 `singleton_port=%d` 已被 **%s** 占用。"
% (_old_port, "、".join(_who)))
print(" 🔴 后果:两区常驻**互抢单例** ⇒ ⛔ 静默只有一个活着(文件头 §关键约束 早写明)。")
print(" 🔴 处置:改用按本区路径算出的 **%d**(=%s)。" % (derived, ws_s))
port, _port_from = derived, "纠偏(原值与他区冲突)"
_port_clash.append("%d ← %s" % (_old_port, "、".join(_who)))
else:
port, _port_from = _old_port, "沿用既有"
_old_goal = {}
if gp.exists():
try:
@@ -248,10 +330,11 @@ def main() -> int:
cfg = _merged
_added = [k for k in cfg if k not in _old_cfg]
cfg["workspace"] = ws_s # 这两项必须以本次为准(工作区可能搬过家)
cfg["singleton_port"] = port # ⚠️ 纠偏时**必须以本次为准**(旧值可能在 `_old_cfg` 里,会被 update 盖回去)
cfgp.write_text(json.dumps(cfg, ensure_ascii=False, indent=1), encoding="utf-8", newline="")
print("② 部署配置:%s%s(端口 %d%s)"
print("② 部署配置:%s%s(端口 %d,%s)"
% (cfgp.relative_to(ws), (" 补了 %s" % _added) if _added else " 已最新",
port, " 沿用既有" if _old_cfg.get("singleton_port") else ""))
port, _port_from))
# ③ 目标(**补缺式**:已有 goal.json ⛔ 一个字都不改)
goal = {
@@ -329,7 +412,6 @@ def main() -> int:
# ⚠️ 本副本若还不存在(首次初始化、还没分发)⇒ 回落用技能目录那份,
# 但**打印时必须说清"这次是回落"**(⛔ 静默回落=用户以为跑的是副本)。
_own = ws / ".workbuddy" / "collab" / "collabd.py"
_own_ba = ws / ".workbuddy" / "collab" / "board.py"
_fallback = not _own.is_file()
_cd = SK / "collabd.py" if _fallback else _own
if _fallback:
@@ -358,9 +440,18 @@ def main() -> int:
"各区程序各自独立,跑了目录那份会写错地方):")
print(" export COLLABD_CONFIG=%s" % str(cfgp).replace("\\", "/"))
_cd_show = _cd
_bd_show = (SK / "board.py") if not (ws / ".workbuddy" / "collab" / "board.py").is_file() else _own_ba
print(" python %s --supervise" % _cd_show)
print(" python %s --serve 8789 --takeover" % _bd_show)
# 🔴🔴 2026-10-05 改口径(用户当轮原话:「看板共用一份 各个工作区不是都应该有自己独立的看板」——
# 这是在**问**,答案是否定的,且**早有定案**:SKILL.md:809 记的 2026-10-03 16:3x 用户拍板
# 「⛔ 不再为每个工作区各起一个看板 —— **看板只保留一份**(就是主工作区这一个),
# 其它工作区靠 `peer_workspaces` 并列查看」。
# ⇒ 老输出写的是「python board.py --serve 8789 --takeover」(**暗示各区各起一份**)——
# 与定案冲突,且 8789 这个端口本身也是随便举的 ⇒ 会把新区的常驻引导到**错形态**上。
# ⇒ 现在**只说本区要做什么**(起常驻),看板那件事改为一句**指向主视图**的说明。
# ⚠️ 判据:本区**只起常驻**,⛔ 不起看板;要并看 ⇒ 把本区路径加进**主工作区**配置的
# `peer_workspaces`(只读),重启主看板即可。
print(" 📺 看板:**全平台只保留一份**(主工作区那份)⇒ 本区⛔ 不起看板。")
print(" 要并看本区 ⇒ 把 `%s` 加进**主工作区**配置的 `peer_workspaces`(只读),重启主看板。" % ws_s)
if _fallback:
print(" ⚠️ 上面是**技能目录那份**(副本还没分发)⇒ 先跑 deploy_code.py --ws %s" % ws_s)
# ══ ⑥ 🔴🔴 **铺常驻载体**(2026-10-04:用户「任何工作区都要能自己配好」)