@shgroup/dsh-serenity-hooks 1.69.2 → 1.69.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.69.2",
3
+ "version": "1.69.4",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/官方 ACP 面/认知轨迹唤醒注册表。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/index.js CHANGED
@@ -13574,6 +13574,97 @@ const EXCLUSIVE_TOOL_NAMES = ["acc-diag"];
13574
13574
  /** `${sessionId}::${toolName}` → restrict disposer */
13575
13575
  const exclusiveRestrictions = /* @__PURE__ */ new Map();
13576
13576
  /**
13577
+ * `sessionId` → { signature, dispose }(角色工具面 restrict 状态)
13578
+ *
13579
+ * `signature` = 本轮实际发出的 deny 清单(排序后 join)。用它做**幂等判据**:
13580
+ * 每次 pre-step 都重算白名单,只有**算出来的结果变了**才重新发 restrict
13581
+ * —— 配置不变就不重复发(restrict 会重建 schema,没必要每步都做)。
13582
+ */
13583
+ const skiffRoleRestrictions = /* @__PURE__ */ new Map();
13584
+ /**
13585
+ * 同步 **Skiff 会话的工具面**:把角色白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
13586
+ *
13587
+ * ## 为什么必须有这条(owner 2026-10-19:「工具清单也要热加载」)
13588
+ *
13589
+ * 在它之前,skiff 角色白名单**只由运行时守卫拦截**(`decideGuard` 的 skiff 分支):
13590
+ * 工具全部出现在模型 schema 里,调用时才被 `tool not allowed in this skiff role` 打回。
13591
+ * ⇒ 两个真实后果:
13592
+ * ① **模型看得见它不该有的工具** —— 它会去试,试了才撞墙(一轮白烧,且错误文案泛化,
13593
+ * 看不出"是不在清单里"还是"临时性拒绝");
13594
+ * ② **配置变动在 schema 上永远不生效** —— 改 `skiff.roles.<r>.tools` 只影响"调用是否放行",
13595
+ * 不影响"模型看得见什么"。这正是 owner 报的"工具清单不热加载"。
13596
+ *
13597
+ * ## 判据与降级
13598
+ *
13599
+ * - deny 集合 = **`REGISTERED_TOOLS` − 角色白名单**(只对**我们自己注册的**工具算,
13600
+ * 不去动宿主的工具 —— 宿主的工具由各自的条件同步管)。
13601
+ * - ⚠️ **未注册的名字会让整次 restrict 失败**(宿主契约逐字 "unknown names … fail"),
13602
+ * 而 `diagram` 是按开关注册的 ⇒ 与 `hideAllFor` 同款**两档尝试**(先全覆盖,失败退常驻段)。
13603
+ * - 读不到角色(配置删了/角色被移除)⇒ **不动**(fail-open):此时守卫仍会拦,
13604
+ * 最坏是"又变成看得见但调不动",而不是把工具全删光。
13605
+ * - 幂等:算出来的 deny 签名不变就跳过(不每步重建 schema)。
13606
+ *
13607
+ * @param agent 目标会话
13608
+ * @param root CCC 根
13609
+ */
13610
+ function syncSkiffRoleToolVisibility(agent, root) {
13611
+ const sessionId = agent.session.id ?? "global";
13612
+ if (!isSkiffSessionId(sessionId)) return;
13613
+ const roleName = skiffRoleFor$1(sessionId);
13614
+ if (!roleName) return;
13615
+ let allowed;
13616
+ try {
13617
+ const role = readSkiffRoles(root).get(roleName);
13618
+ if (!role) return;
13619
+ allowed = roleToolWhitelist(role);
13620
+ } catch {
13621
+ return;
13622
+ }
13623
+ const resident = REGISTERED_TOOLS.slice(0, REGISTERED_TOOLS.length - 3);
13624
+ const tiers = [REGISTERED_TOOLS, resident];
13625
+ const existing = skiffRoleRestrictions.get(sessionId);
13626
+ for (const names of tiers) {
13627
+ const deny = names.filter((n) => !allowed.has(n));
13628
+ const signature = [...deny].sort().join(",");
13629
+ if (existing !== void 0 && existing.signature === signature) return;
13630
+ if (deny.length === 0) {
13631
+ if (existing !== void 0) {
13632
+ try {
13633
+ existing.dispose();
13634
+ } catch {}
13635
+ skiffRoleRestrictions.delete(sessionId);
13636
+ }
13637
+ return;
13638
+ }
13639
+ try {
13640
+ if (existing !== void 0) {
13641
+ try {
13642
+ existing.dispose();
13643
+ } catch {}
13644
+ skiffRoleRestrictions.delete(sessionId);
13645
+ }
13646
+ const dispose = agent.ctx.tools.restrict({ deny: [...deny] });
13647
+ skiffRoleRestrictions.set(sessionId, {
13648
+ signature,
13649
+ deny,
13650
+ dispose
13651
+ });
13652
+ return;
13653
+ } catch {
13654
+ continue;
13655
+ }
13656
+ }
13657
+ }
13658
+ /** 会话销毁清理(lifecycle 调用) */
13659
+ function forgetSkiffRoleToolVisibility(sessionId) {
13660
+ const existing = skiffRoleRestrictions.get(sessionId);
13661
+ if (!existing) return;
13662
+ try {
13663
+ existing.dispose();
13664
+ } catch {}
13665
+ skiffRoleRestrictions.delete(sessionId);
13666
+ }
13667
+ /**
13577
13668
  * 同步专属工具可见性(v1.33)。
13578
13669
  *
13579
13670
  * 判据 = CCC 配置 `exclusiveTools`(`readExclusiveTools`):
@@ -13818,6 +13909,13 @@ const DEFAULT_COMPACTION_TOOLS = [
13818
13909
  */
13819
13910
  const DEFAULT_ANCHOR_MESSAGES = ["You are the operator agent of Serenity — a cognitive container governed by the Abstract Cognitive Container (ACC) protocol.\nWe operate under the Explicit Abstraction Principle (EAP): the functional value of a thought equals its external reconstructability — every output we produce must be explicit (E↑), reconstructable (R↓), and stable (S↑).\nThe personal pronoun is us/we.\nWe anchor first, then act: the abstract layer precedes the concrete.\nPlease simply reply \"acknowledge\" — no action needed.", "Before we proceed, align on how we work:\n1. We read before we write — every decision grounds in what already exists in the container.\n2. Every output records its reasoning (R↓): decisions carry reasons and alternatives.\n3. We never jump levels — abstract layer first, then specifics.\n4. Every artifact we create is a durable cognitive anchor for the work that follows.\n5. We keep the container's state coherent (SESSION.md) as we advance.\nPlease simply reply \"acknowledge\" — no action needed."];
13820
13911
  /**
13912
+ * 锚定交换里「模型那半」的回复文本。
13913
+ *
13914
+ * 只写一个词:锚定话语明确要求 `reply "acknowledge" — no action needed`,
13915
+ * 伪造一条更长的回复反而与提示词不符(模型会据历史学样)。
13916
+ */
13917
+ const ANCHOR_ACK_TEXT = "acknowledge";
13918
+ /**
13821
13919
  * 会话里是否已有模型回复(= 已经不是首轮)。
13822
13920
  *
13823
13921
  * 🔴 这是锚定的**形状判据**(v1.62):锚定只能在**首轮**造。
@@ -13833,14 +13931,24 @@ function hasAssistantHistory(session) {
13833
13931
  *
13834
13932
  * ## 🔴 两条硬约束(宿主 v4 格式校验器,实测逐条踩过)
13835
13933
  *
13836
- * **约束 A — 只能写 `user/message`**:`system/message` 与 `assistant/message` 都在
13837
- * `STEP_EVENT_TYPES` 里 ⇒ 必须过 `requireStep()`(`data.turn`/`data.step` 精确匹配
13838
- * **当前打开的** turn 与 step,且为 null 时同样拒绝)。而本函数在 `agent/pre-step`
13839
- * 前后被调用时 **turn/step 都还没打开**(`turn/start` 在 preStep 之前,`step/start`
13840
- * 在 preStep 之后)⇒ 代写它们**结构性非法**。
13841
- * ⚠️ 症状:写通道不校验 ⇒ **首轮正常**;**重新进入**时重放才抛
13842
- * `system/message does not match an open turn and step` ⇒ 会话 corrupt、界面空白。
13843
- * ⇒ **v1.60 那版「先写占位 system 等宿主 replace」的整套方案因此作废**。
13934
+ * **约束 A — `system/message` 绝不能代写;`assistant/message` 可以,但必须落在已打开的 turn/step 内**。
13935
+ *
13936
+ * 🔴 **D140 的理由已由本文件 `tests/anchor-history-shape.test.ts` 当场更正**(探针直调宿主
13937
+ * `assertReleasedV4Relationships()`,不靠读源码推断):
13938
+ * 原记「`system/message` 与 `assistant/message` 都在 `STEP_EVENT_TYPES` 里」—— **只对前半**。
13939
+ * 实测 `STEP_EVENT_TYPES` = `{ system/message, developer/message, assistant/attempt }`,
13940
+ * **`assistant/message` 不在其中**。它仍要过 `requireStep`,但走的是另一条路
13941
+ * (`Relationships.accept()` → `case "assistant/message"` → `tool()` → `requireStep`)。
13942
+ *
13943
+ * ⇒ **结论保留、理由作废**:`assistant/message` 在**该 system/message 之前**写是非法的
13944
+ * (那时 turn/step 尚未打开);而在**之后**写是**合法的**(那时两者都已打开)。
13945
+ * 探针三态实测:A(仅 user)✅ / **B(user + assistant 交替,turn/step 匹配)✅** /
13946
+ * C(assistant 落在未打开的 turn/step)❌ `does not match an open turn and step`。
13947
+ *
13948
+ * ⚠️ 症状(不许省这条):写通道不校验 ⇒ **首轮看着正常**;**重新进入**时重放才抛
13949
+ * `does not match an open turn and step` ⇒ 会话 corrupt、界面空白。
13950
+ * ⇒ **v1.60 那版「先写占位 system 等宿主 replace」的整套方案仍然作废**
13951
+ * (它写的是 `system/message`,那**确实**在 `STEP_EVENT_TYPES` 里)。
13844
13952
  *
13845
13953
  * **约束 B — 必须写在宿主提交 `system/message` 之后**:格式层有 `protectedHead`
13846
13954
  * 机制 —— `foldSurface()` 里 `system/message` 若在 **surface 非空且尚无 protectedHead**
@@ -13853,38 +13961,69 @@ function hasAssistantHistory(session) {
13853
13961
  * ⇒ 因此调用时机 = **`session/event` 里看到该会话第一条 `system/message` 之后**(见下方注册处),
13854
13962
  * 而不是 `agent/inbox/inserted`(那太早)。
13855
13963
  *
13856
- * ## 为什么去掉 assistant 那半不损失什么
13964
+ * ## 为什么必须成对(user + assistant ack)—— v1.69.4 修正
13965
+ *
13966
+ * 只造 user 的那一版在**界面上读不成对话**:两条"用户"消息连排在模型回复之前,
13967
+ * 看起来像用户自己连说了两句协议、而模型没理。owner 2026-10-19 报的两点
13968
+ * (「助手的回应也需要造出来」+「用户的怎么跑到第一个了」)其实是**同一个缺陷的两面**。
13969
+ * ⇒ 造**成对交换**:每条锚定话语后面跟一条 `acknowledge` 回复。
13857
13970
  *
13858
- * 锚定话语的**唯一目的**是让模型认为自己已经同意过协作协议。
13859
- * 保留 **2 条 `user/message`** 即可达成;丢掉的只是"模型自己的 ack 回复"——
13860
- * 而那原本也是**我们伪造的**,不是模型真说过的。
13971
+ * 🔴 **前提条件**:`assistant/message` 必须落在**已打开**的 turn/step 内
13972
+ * (探针 S3/S4 实测:未打开/已关闭都抛 `does not match an open turn and step`)。
13973
+ * 本函数由 `session/event` 的 `system/message` 分支触发,而**那条事件自己就带着
13974
+ * `data.turn` / `data.step`** —— 且实测此刻 turn/step 确实处于打开态
13975
+ * (真机日志:`step/start` → `system/message` → 锚定写入 → `step/end`)。
13976
+ * ⇒ turn/step **从触发事件透传进来**,不自己猜。
13977
+ *
13978
+ * ⚠️ **拿不到 turn/step 时退回只造 user**(不抛、不阻断):宁可少伪造一条 ack,
13979
+ * 也绝不在非法坐标上写 `assistant/message` —— 那会让会话在重放时 corrupt。
13861
13980
  *
13862
13981
  * ## 判据(改这个函数前后都必须跑)
13863
13982
  *
13864
- * ① 单测钉形状(只许 user/message);
13865
- * ② **容器内真跑一轮 → 重启宿主 → 重新进入该会话** —— 只有这条路能证明日志合法
13983
+ * ① 形状判据 = `tests/anchor-history-shape.test.ts`(直调宿主校验器,正控 + 两种负控);
13984
+ * ② 单测钉事件序列(user/assistant 交替、turn/step 逐条匹配);
13985
+ * ③ **容器内真跑一轮 → 重启宿主 → 重新进入该会话** —— 只有这条路能证明日志合法
13866
13986
  * (前两次修复都是"单测全绿 + 首轮正常"却仍然 corrupt)。
13867
- * ③ 直接调宿主自己的 `assertReleasedV4Relationships()` 判形状,**不要自写复刻**。
13868
13987
  *
13869
13988
  * @param session 会话(需有 append)
13870
13989
  * @param anchorMessages 锚定话语序列(缺省协议固有两条)
13990
+ * @param slot 触发本轮 `system/message` 所在的 turn/step(缺省或非法 ⇒ 只造 user)
13871
13991
  * @returns 是否成功写入
13872
13992
  */
13873
- function appendAnchorHistory(session, anchorMessages = DEFAULT_ANCHOR_MESSAGES) {
13993
+ function appendAnchorHistory(session, anchorMessages = DEFAULT_ANCHOR_MESSAGES, slot) {
13874
13994
  const target = session;
13875
13995
  if (typeof target?.append !== "function") return false;
13876
13996
  const append = target.append.bind(target);
13877
13997
  const rid = (prefix) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
13998
+ const turn = typeof slot?.turn === "number" ? slot.turn : 0;
13999
+ const step = typeof slot?.step === "number" ? slot.step : 0;
14000
+ const canPlaceAck = turn > 0 && step > 0;
13878
14001
  try {
13879
- for (let i = 0; i < anchorMessages.length; i++) append("user/message", {
13880
- id: rid(`anchor-user-${i + 1}`),
13881
- role: "user",
13882
- content: [{
13883
- type: "text",
13884
- text: anchorMessages[i]
13885
- }],
13886
- source: { kind: "user" }
13887
- }, { surfaceOp: "append" });
14002
+ for (let i = 0; i < anchorMessages.length; i++) {
14003
+ append("user/message", {
14004
+ id: rid(`anchor-user-${i + 1}`),
14005
+ role: "user",
14006
+ content: [{
14007
+ type: "text",
14008
+ text: anchorMessages[i]
14009
+ }],
14010
+ source: { kind: "user" }
14011
+ }, { surfaceOp: "append" });
14012
+ if (!canPlaceAck) continue;
14013
+ append("assistant/message", {
14014
+ turn,
14015
+ step,
14016
+ message: {
14017
+ id: rid(`anchor-ack-${i + 1}`),
14018
+ role: "assistant",
14019
+ content: [{
14020
+ type: "text",
14021
+ text: ANCHOR_ACK_TEXT
14022
+ }],
14023
+ source: { kind: "model" }
14024
+ }
14025
+ }, { surfaceOp: "append" });
14026
+ }
13888
14027
  return anchorMessages.length > 0;
13889
14028
  } catch {
13890
14029
  return false;
@@ -14061,24 +14200,30 @@ function registerBootstrap(ctx) {
14061
14200
  const root = cccRootForCwd(cwd);
14062
14201
  if (!root) return;
14063
14202
  trackerFor(root).observe(session, event);
14064
- if (event.type === "system/message") setTimeout(() => {
14065
- try {
14066
- maybeAnchor(session, root);
14067
- } catch {}
14068
- }, 0);
14203
+ if (event.type === "system/message") {
14204
+ const slot = event.data;
14205
+ setTimeout(() => {
14206
+ try {
14207
+ maybeAnchor(session, root, slot);
14208
+ } catch {}
14209
+ }, 0);
14210
+ }
14069
14211
  } catch {}
14070
14212
  });
14071
14213
  /** 造锚定历史(幂等;防重靠 `anchoredSessions`)。 */
14072
- const maybeAnchor = (session, _root) => {
14214
+ const maybeAnchor = (session, _root, slot) => {
14073
14215
  const s = session;
14074
14216
  s?.header?.delegationDepth;
14075
14217
  const sid = typeof s?.id === "string" ? s.id : void 0;
14076
14218
  if (sid !== void 0 && anchoredSessions.has(sid)) return;
14077
14219
  if (sid !== void 0 && (sid.startsWith("cheap-worker-") || isSkiffSessionId(sid))) return;
14078
14220
  if (hasAssistantHistory(session)) return;
14079
- const wrote = appendAnchorHistory(session, SETTINGS.anchorMessages);
14221
+ const wrote = appendAnchorHistory(session, SETTINGS.anchorMessages, slot);
14080
14222
  if (wrote && sid !== void 0) anchoredSessions.add(sid);
14081
- if (wrote) warnOnce(`anchor history fabricated: ${SETTINGS.anchorMessages.length} 条 user 消息(不再真跑)`);
14223
+ if (wrote) {
14224
+ const paired = typeof slot?.turn === "number" && typeof slot?.step === "number" && slot.turn > 0 && slot.step > 0;
14225
+ warnOnce(`anchor history fabricated: ${SETTINGS.anchorMessages.length} 条 user 消息` + (paired ? ` + ${SETTINGS.anchorMessages.length} 条 ack(turn ${String(slot.turn)} / step ${String(slot.step)})` : "(无打开坐标 ⇒ 未造 ack)") + "(不再真跑)");
14226
+ }
14082
14227
  };
14083
14228
  ctx.on("agent/inbox/inserted", ({ agent, message }) => {
14084
14229
  try {
@@ -14750,6 +14895,9 @@ function registerContext(ctx, opts = {}) {
14750
14895
  const root = cccRootForCwd(cwd);
14751
14896
  const key = agentKey(agent);
14752
14897
  const downstream = await next();
14898
+ if (root) try {
14899
+ syncSkiffRoleToolVisibility(agent, root);
14900
+ } catch {}
14753
14901
  if (isSkiffSessionId(key)) return downstream;
14754
14902
  try {
14755
14903
  syncCccToolVisibility(agent, root);
@@ -21759,6 +21907,9 @@ function cleanupSessionState(sessionId) {
21759
21907
  try {
21760
21908
  forgetHostScheduleToolHiding(sessionId);
21761
21909
  } catch {}
21910
+ try {
21911
+ forgetSkiffRoleToolVisibility(sessionId);
21912
+ } catch {}
21762
21913
  try {
21763
21914
  forgetLogbookCompactionState(sessionId);
21764
21915
  } catch {}
@@ -50,6 +50,13 @@ export declare const DEFAULT_COMPACTION_TOOLS: string[];
50
50
  * 文本 = 原 home-serenity CCC 配置(anchorMessages),随零配置化固化进代码。
51
51
  */
52
52
  export declare const DEFAULT_ANCHOR_MESSAGES: string[];
53
+ /**
54
+ * 锚定交换里「模型那半」的回复文本。
55
+ *
56
+ * 只写一个词:锚定话语明确要求 `reply "acknowledge" — no action needed`,
57
+ * 伪造一条更长的回复反而与提示词不符(模型会据历史学样)。
58
+ */
59
+ export declare const ANCHOR_ACK_TEXT = "acknowledge";
53
60
  /**
54
61
  * 根会话锚定重入判定(v1.28.1 提取导出以便单测):会话是否已有真实用户消息。
55
62
  * resume/续跑的会话已有对话历史 → **不重锚**(first-anchor 只注入一次)。
@@ -73,14 +80,24 @@ export declare function hasAssistantHistory(session: unknown): boolean;
73
80
  *
74
81
  * ## 🔴 两条硬约束(宿主 v4 格式校验器,实测逐条踩过)
75
82
  *
76
- * **约束 A — 只能写 `user/message`**:`system/message` 与 `assistant/message` 都在
77
- * `STEP_EVENT_TYPES` 里 ⇒ 必须过 `requireStep()`(`data.turn`/`data.step` 精确匹配
78
- * **当前打开的** turn 与 step,且为 null 时同样拒绝)。而本函数在 `agent/pre-step`
79
- * 前后被调用时 **turn/step 都还没打开**(`turn/start` 在 preStep 之前,`step/start`
80
- * 在 preStep 之后)⇒ 代写它们**结构性非法**。
81
- * ⚠️ 症状:写通道不校验 ⇒ **首轮正常**;**重新进入**时重放才抛
82
- * `system/message does not match an open turn and step` ⇒ 会话 corrupt、界面空白。
83
- * ⇒ **v1.60 那版「先写占位 system 等宿主 replace」的整套方案因此作废**。
83
+ * **约束 A — `system/message` 绝不能代写;`assistant/message` 可以,但必须落在已打开的 turn/step 内**。
84
+ *
85
+ * 🔴 **D140 的理由已由本文件 `tests/anchor-history-shape.test.ts` 当场更正**(探针直调宿主
86
+ * `assertReleasedV4Relationships()`,不靠读源码推断):
87
+ * 原记「`system/message` 与 `assistant/message` 都在 `STEP_EVENT_TYPES` 里」—— **只对前半**。
88
+ * 实测 `STEP_EVENT_TYPES` = `{ system/message, developer/message, assistant/attempt }`,
89
+ * **`assistant/message` 不在其中**。它仍要过 `requireStep`,但走的是另一条路
90
+ * (`Relationships.accept()` → `case "assistant/message"` → `tool()` → `requireStep`)。
91
+ *
92
+ * ⇒ **结论保留、理由作废**:`assistant/message` 在**该 system/message 之前**写是非法的
93
+ * (那时 turn/step 尚未打开);而在**之后**写是**合法的**(那时两者都已打开)。
94
+ * 探针三态实测:A(仅 user)✅ / **B(user + assistant 交替,turn/step 匹配)✅** /
95
+ * C(assistant 落在未打开的 turn/step)❌ `does not match an open turn and step`。
96
+ *
97
+ * ⚠️ 症状(不许省这条):写通道不校验 ⇒ **首轮看着正常**;**重新进入**时重放才抛
98
+ * `does not match an open turn and step` ⇒ 会话 corrupt、界面空白。
99
+ * ⇒ **v1.60 那版「先写占位 system 等宿主 replace」的整套方案仍然作废**
100
+ * (它写的是 `system/message`,那**确实**在 `STEP_EVENT_TYPES` 里)。
84
101
  *
85
102
  * **约束 B — 必须写在宿主提交 `system/message` 之后**:格式层有 `protectedHead`
86
103
  * 机制 —— `foldSurface()` 里 `system/message` 若在 **surface 非空且尚无 protectedHead**
@@ -93,24 +110,39 @@ export declare function hasAssistantHistory(session: unknown): boolean;
93
110
  * ⇒ 因此调用时机 = **`session/event` 里看到该会话第一条 `system/message` 之后**(见下方注册处),
94
111
  * 而不是 `agent/inbox/inserted`(那太早)。
95
112
  *
96
- * ## 为什么去掉 assistant 那半不损失什么
113
+ * ## 为什么必须成对(user + assistant ack)—— v1.69.4 修正
114
+ *
115
+ * 只造 user 的那一版在**界面上读不成对话**:两条"用户"消息连排在模型回复之前,
116
+ * 看起来像用户自己连说了两句协议、而模型没理。owner 2026-10-19 报的两点
117
+ * (「助手的回应也需要造出来」+「用户的怎么跑到第一个了」)其实是**同一个缺陷的两面**。
118
+ * ⇒ 造**成对交换**:每条锚定话语后面跟一条 `acknowledge` 回复。
119
+ *
120
+ * 🔴 **前提条件**:`assistant/message` 必须落在**已打开**的 turn/step 内
121
+ * (探针 S3/S4 实测:未打开/已关闭都抛 `does not match an open turn and step`)。
122
+ * 本函数由 `session/event` 的 `system/message` 分支触发,而**那条事件自己就带着
123
+ * `data.turn` / `data.step`** —— 且实测此刻 turn/step 确实处于打开态
124
+ * (真机日志:`step/start` → `system/message` → 锚定写入 → `step/end`)。
125
+ * ⇒ turn/step **从触发事件透传进来**,不自己猜。
97
126
  *
98
- * 锚定话语的**唯一目的**是让模型认为自己已经同意过协作协议。
99
- * 保留 **2 条 `user/message`** 即可达成;丢掉的只是"模型自己的 ack 回复"——
100
- * 而那原本也是**我们伪造的**,不是模型真说过的。
127
+ * ⚠️ **拿不到 turn/step 时退回只造 user**(不抛、不阻断):宁可少伪造一条 ack,
128
+ * 也绝不在非法坐标上写 `assistant/message` —— 那会让会话在重放时 corrupt。
101
129
  *
102
130
  * ## 判据(改这个函数前后都必须跑)
103
131
  *
104
- * ① 单测钉形状(只许 user/message);
105
- * ② **容器内真跑一轮 → 重启宿主 → 重新进入该会话** —— 只有这条路能证明日志合法
132
+ * ① 形状判据 = `tests/anchor-history-shape.test.ts`(直调宿主校验器,正控 + 两种负控);
133
+ * ② 单测钉事件序列(user/assistant 交替、turn/step 逐条匹配);
134
+ * ③ **容器内真跑一轮 → 重启宿主 → 重新进入该会话** —— 只有这条路能证明日志合法
106
135
  * (前两次修复都是"单测全绿 + 首轮正常"却仍然 corrupt)。
107
- * ③ 直接调宿主自己的 `assertReleasedV4Relationships()` 判形状,**不要自写复刻**。
108
136
  *
109
137
  * @param session 会话(需有 append)
110
138
  * @param anchorMessages 锚定话语序列(缺省协议固有两条)
139
+ * @param slot 触发本轮 `system/message` 所在的 turn/step(缺省或非法 ⇒ 只造 user)
111
140
  * @returns 是否成功写入
112
141
  */
113
- export declare function appendAnchorHistory(session: unknown, anchorMessages?: string[]): boolean;
142
+ export declare function appendAnchorHistory(session: unknown, anchorMessages?: string[], slot?: {
143
+ turn?: unknown;
144
+ step?: unknown;
145
+ }): boolean;
114
146
  interface PromotionStatus {
115
147
  /** 最后一次 compaction/end 的 seq(-1 = 未压缩过) */
116
148
  boundary: number;
@@ -95,6 +95,40 @@ export declare function forgetHostScheduleToolHiding(sessionId: string): void;
95
95
  export declare function getExclusiveToolsDiagnostics(): {
96
96
  hidden: string[];
97
97
  };
98
+ /** Skiff 角色工具面诊断(排查"某工具为什么不在 skiff 的清单里") */
99
+ export declare function getSkiffRoleVisibilityDiagnostics(): {
100
+ keys: string[];
101
+ denyByKey: Record<string, string[]>;
102
+ };
103
+ /**
104
+ * 同步 **Skiff 会话的工具面**:把角色白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
105
+ *
106
+ * ## 为什么必须有这条(owner 2026-10-19:「工具清单也要热加载」)
107
+ *
108
+ * 在它之前,skiff 角色白名单**只由运行时守卫拦截**(`decideGuard` 的 skiff 分支):
109
+ * 工具全部出现在模型 schema 里,调用时才被 `tool not allowed in this skiff role` 打回。
110
+ * ⇒ 两个真实后果:
111
+ * ① **模型看得见它不该有的工具** —— 它会去试,试了才撞墙(一轮白烧,且错误文案泛化,
112
+ * 看不出"是不在清单里"还是"临时性拒绝");
113
+ * ② **配置变动在 schema 上永远不生效** —— 改 `skiff.roles.<r>.tools` 只影响"调用是否放行",
114
+ * 不影响"模型看得见什么"。这正是 owner 报的"工具清单不热加载"。
115
+ *
116
+ * ## 判据与降级
117
+ *
118
+ * - deny 集合 = **`REGISTERED_TOOLS` − 角色白名单**(只对**我们自己注册的**工具算,
119
+ * 不去动宿主的工具 —— 宿主的工具由各自的条件同步管)。
120
+ * - ⚠️ **未注册的名字会让整次 restrict 失败**(宿主契约逐字 "unknown names … fail"),
121
+ * 而 `diagram` 是按开关注册的 ⇒ 与 `hideAllFor` 同款**两档尝试**(先全覆盖,失败退常驻段)。
122
+ * - 读不到角色(配置删了/角色被移除)⇒ **不动**(fail-open):此时守卫仍会拦,
123
+ * 最坏是"又变成看得见但调不动",而不是把工具全删光。
124
+ * - 幂等:算出来的 deny 签名不变就跳过(不每步重建 schema)。
125
+ *
126
+ * @param agent 目标会话
127
+ * @param root CCC 根
128
+ */
129
+ export declare function syncSkiffRoleToolVisibility(agent: Agent, root: string): void;
130
+ /** 会话销毁清理(lifecycle 调用) */
131
+ export declare function forgetSkiffRoleToolVisibility(sessionId: string): void;
98
132
  /**
99
133
  * 同步专属工具可见性(v1.33)。
100
134
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.69.2",
3
+ "version": "1.69.4",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/官方 ACP 面/认知轨迹唤醒注册表。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {