@shgroup/dsh-serenity-hooks 1.79.4 → 1.79.5

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.79.4",
3
+ "version": "1.79.5",
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 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 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
@@ -3,8 +3,8 @@ import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebSe
3
3
  import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CyYJL4F2.js";
4
4
  import { A as sessionEvents, B as isoLocal, C as listSessions, E as parseTrajectoryCode, F as trajectoryDescOf, H as localHuman, I as useSession, L as TRAJECTORY_FILENAME, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as TRAJECTORY_LEGACY_FILENAMES, S as listActiveTrajectories, T as parseSessionContextFromEvents, U as localIdStamp, V as localFileStamp, _ as createSession, a as readTrajectoryConstraintAt, b as getActiveSessionInfo, c as trajectoryConstraintForSession, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, m as UID_FILENAME, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, r as listTrajectoryConstraints, s as replacesDefaultIdentity, t as TRAJECTORY_CONSTRAINT_FILENAME, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as readTrajectoryFile } from "./profile-5TQ_C1xi.js";
5
5
  import { n as checkLocalstoreGitCompliance, o as readStore, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./service-DDmHSglV.js";
6
- import { _ as ACC_VERSION, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as resolveConstraintSystemPrompt, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as findPackageRoot } from "./message-source-DK2CnXlh.js";
7
- import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-BYC1uWi0.js";
6
+ import { _ as ACC_VERSION, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as resolveConstraintSystemPrompt, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as findPackageRoot } from "./message-source-DiP3Uve3.js";
7
+ import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-tdlJDS9i.js";
8
8
  import { n as hasSessionLogById, r as sessionsRootDir } from "./cleanup-C-92qnDs.js";
9
9
  import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-B-vn8j4g.js";
10
10
  import { REGISTERED_TOOLS } from "./invariant.js";
@@ -7114,102 +7114,6 @@ function createCheapWorkerTool(ctx) {
7114
7114
  });
7115
7115
  }
7116
7116
  //#endregion
7117
- //#region src/trajectory/alive.ts
7118
- /**
7119
- * trajectory/alive.ts — **"这个 dsh 会话还活着吗"的唯一判据**(owner 2026-10-20 裁决)
7120
- *
7121
- * ## owner 的原话(本模块的全部设计依据)
7122
- *
7123
- * > 「死应该包含归档判断;只有 ① 或者 ②且不在归档集 才算活」
7124
- * > 「use 只要被调用了,可以无条件覆盖绑定到当前会话(这说明当前会话肯定活着啊)」
7125
- *
7126
- * ## 为什么必须收成一个判据(在此之前有三个互不一致的判据)
7127
- *
7128
- * | # | 判据 | 旧实现位置 | 单独使用时的漏洞 |
7129
- * |---|---|---|---|
7130
- * | ① | 进程内 loaded agent | `hostAgents(ctx).get(id)` | 冷会话全判死 |
7131
- * | ② | 磁盘上有会话日志 | `hasSessionLogById()` | **归档后日志逐字节不动** ⇒ 归档会话判活 |
7132
- * | ③ | 宿主归档集 | `workspaceRegistry.archivedSessionIds` | 旧 dsp **零引用**(整个仓库 0 处) |
7133
- *
7134
- * 宿主的归档语义是**只往集合里加一个 id、不动日志**(`dsh-workspace` 的
7135
- * `archiveSession` 注释:「Archiving never touches workspace accounting」)。
7136
- * ⇒ 只问 ② 的实现在归档上**必然判错**,而那个错误没有任何机械信号。
7137
- *
7138
- * ## 判据(owner 给定,逐字实现)
7139
- *
7140
- * ```text
7141
- * 活 ⟺ ① (进程内 loaded ⇒ 一定在)
7142
- * ∨ (② ∧ 不在归档集) (日志在、且没人把它归档掉)
7143
- * ```
7144
- *
7145
- * 🔴 **一处对原话的收紧(必须说出来,不许默默改)**:owner 的 ① 未附加"不在归档集",
7146
- * 而本模块把**归档判定排在最前** —— 即 `归档 ∧ live ⇒ 判死`。理由:归档是**人对这条会话
7147
- * 的处置声明**(宿主会把它从所有分组面隐藏),若仍被当成载体,唤醒调度器会去驱动一条
7148
- * **使用者以为已经收起来**的会话。⚠️ 与字面规则的差异仅此一格;`use` 不受影响
7149
- * (见下方 §use 逃生门),因此真需要复活一条已归档会话时**仍然有受支持的路**。
7150
- *
7151
- * ## 🔴 `use` 的逃生门(owner 第二条,写在调用点)
7152
- *
7153
- * **`use` 不查存活**:它被调用这件事本身证明**当前会话活着**(本模块的被测对象是
7154
- * "别人写的那个 id",而 `use` 手里拿的是**自己**)。⇒ `use` 走无条件覆盖
7155
- * (`claimBinding` 的 `takeover`),因此**判据再严也不会把一条轨迹锁死**。
7156
- * 这条逃生门是判据可以取严的前提,两者必须一起存在。
7157
- *
7158
- * ## 依赖纪律
7159
- *
7160
- * 纯判据 `isSessionAlive` 零依赖、可单测;宿主面只在 `hostAliveProbe` 里,
7161
- * 且**不抛错**(服务缺失 ⇒ 该维度作废,而不是让 ACC 工具整块失败)。
7162
- */
7163
- /** 宿主侧的归档集读取(不可用 ⇒ 空集 = 该维度作废,**不是**把所有会话判死) */
7164
- function archivedIdsOf(ctx) {
7165
- try {
7166
- const ids = hostService(ctx, "workspaceRegistry")?.archivedSessionIds;
7167
- return new Set(Array.isArray(ids) ? ids : []);
7168
- } catch {
7169
- return /* @__PURE__ */ new Set();
7170
- }
7171
- }
7172
- /**
7173
- * **宿主面的判活探针**(`(sessionId) => boolean`)——所有需要"这条会话还活着吗"的
7174
- * 生产代码都应当用这一个,不要自己拼 `agents.get` + `hasSessionLogById`。
7175
- *
7176
- * 语义 = {@link isSessionAlive},其中:
7177
- * · `liveIds` 因为宿主只给 `get(id)` 而**按 id 逐次问**(不是集合);
7178
- * · `archivedIds` 在**首次调用时**取一次快照并在闭包内复用(每 tick 一个探针,
7179
- * 归档集在一个 tick 内不变)—— 避免为每条轨迹重复读存储域。
7180
- *
7181
- * ⚠️ **快照的代价**:同一探针内若有人中途归档了某会话,本探针直到下次新建才看见。
7182
- * 调度器是每 tick 新建一个探针(见 `wake/scheduler.ts`),故实际窗口 ≤1 tick。
7183
- *
7184
- * @param ctx 插件上下文(取 `agents` 与 `workspaceRegistry`)
7185
- * @returns 探针;**永不抛**
7186
- */
7187
- function hostAliveProbe(ctx) {
7188
- let archived;
7189
- const archivedOnce = () => archived ??= archivedIdsOf(ctx);
7190
- return (sessionId) => {
7191
- if (typeof sessionId !== "string" || sessionId === "") return false;
7192
- if (archivedOnce().has(sessionId)) return false;
7193
- try {
7194
- if (hostAgents(ctx)?.get?.(sessionId)) return true;
7195
- } catch {}
7196
- try {
7197
- return hasSessionLogById(sessionsRootDir(), sessionId);
7198
- } catch {
7199
- return true;
7200
- }
7201
- };
7202
- }
7203
- /**
7204
- * 判据只有**两个**出口,别再开第三个:
7205
- * · {@link isSessionAlive} —— 纯判据(给定事实),单测的靶子;
7206
- * · {@link hostAliveProbe} —— 生产入口(自己去宿主与磁盘取事实)。
7207
- *
7208
- * ⚠️ 曾有一个 `aliveFactsFor`(把探针摊平成 `SessionAliveFacts`),**全仓零调用点**,
7209
- * 已删除 —— 留一个"看起来有用、实际无人调用"的导出,会让下一个人以为存在第二条判活路径
7210
- * (本仓同族教训:`cheapWorkerPresetInheritance` 全仓零调用点 ⇒ 接线断了却看不出来)。
7211
- */
7212
- //#endregion
7213
7117
  //#region src/wake/registry.ts
7214
7118
  /**
7215
7119
  * wake/registry.ts — 认知轨迹唤醒注册表(D58,S142 2026-09-13)
@@ -7670,6 +7574,102 @@ function deliverCarrierOpening(agent, text) {
7670
7574
  }
7671
7575
  }
7672
7576
  //#endregion
7577
+ //#region src/trajectory/alive.ts
7578
+ /**
7579
+ * trajectory/alive.ts — **"这个 dsh 会话还活着吗"的唯一判据**(owner 2026-10-20 裁决)
7580
+ *
7581
+ * ## owner 的原话(本模块的全部设计依据)
7582
+ *
7583
+ * > 「死应该包含归档判断;只有 ① 或者 ②且不在归档集 才算活」
7584
+ * > 「use 只要被调用了,可以无条件覆盖绑定到当前会话(这说明当前会话肯定活着啊)」
7585
+ *
7586
+ * ## 为什么必须收成一个判据(在此之前有三个互不一致的判据)
7587
+ *
7588
+ * | # | 判据 | 旧实现位置 | 单独使用时的漏洞 |
7589
+ * |---|---|---|---|
7590
+ * | ① | 进程内 loaded agent | `hostAgents(ctx).get(id)` | 冷会话全判死 |
7591
+ * | ② | 磁盘上有会话日志 | `hasSessionLogById()` | **归档后日志逐字节不动** ⇒ 归档会话判活 |
7592
+ * | ③ | 宿主归档集 | `workspaceRegistry.archivedSessionIds` | 旧 dsp **零引用**(整个仓库 0 处) |
7593
+ *
7594
+ * 宿主的归档语义是**只往集合里加一个 id、不动日志**(`dsh-workspace` 的
7595
+ * `archiveSession` 注释:「Archiving never touches workspace accounting」)。
7596
+ * ⇒ 只问 ② 的实现在归档上**必然判错**,而那个错误没有任何机械信号。
7597
+ *
7598
+ * ## 判据(owner 给定,逐字实现)
7599
+ *
7600
+ * ```text
7601
+ * 活 ⟺ ① (进程内 loaded ⇒ 一定在)
7602
+ * ∨ (② ∧ 不在归档集) (日志在、且没人把它归档掉)
7603
+ * ```
7604
+ *
7605
+ * 🔴 **一处对原话的收紧(必须说出来,不许默默改)**:owner 的 ① 未附加"不在归档集",
7606
+ * 而本模块把**归档判定排在最前** —— 即 `归档 ∧ live ⇒ 判死`。理由:归档是**人对这条会话
7607
+ * 的处置声明**(宿主会把它从所有分组面隐藏),若仍被当成载体,唤醒调度器会去驱动一条
7608
+ * **使用者以为已经收起来**的会话。⚠️ 与字面规则的差异仅此一格;`use` 不受影响
7609
+ * (见下方 §use 逃生门),因此真需要复活一条已归档会话时**仍然有受支持的路**。
7610
+ *
7611
+ * ## 🔴 `use` 的逃生门(owner 第二条,写在调用点)
7612
+ *
7613
+ * **`use` 不查存活**:它被调用这件事本身证明**当前会话活着**(本模块的被测对象是
7614
+ * "别人写的那个 id",而 `use` 手里拿的是**自己**)。⇒ `use` 走无条件覆盖
7615
+ * (`claimBinding` 的 `takeover`),因此**判据再严也不会把一条轨迹锁死**。
7616
+ * 这条逃生门是判据可以取严的前提,两者必须一起存在。
7617
+ *
7618
+ * ## 依赖纪律
7619
+ *
7620
+ * 纯判据 `isSessionAlive` 零依赖、可单测;宿主面只在 `hostAliveProbe` 里,
7621
+ * 且**不抛错**(服务缺失 ⇒ 该维度作废,而不是让 ACC 工具整块失败)。
7622
+ */
7623
+ /** 宿主侧的归档集读取(不可用 ⇒ 空集 = 该维度作废,**不是**把所有会话判死) */
7624
+ function archivedIdsOf(ctx) {
7625
+ try {
7626
+ const ids = hostService(ctx, "workspaceRegistry")?.archivedSessionIds;
7627
+ return new Set(Array.isArray(ids) ? ids : []);
7628
+ } catch {
7629
+ return /* @__PURE__ */ new Set();
7630
+ }
7631
+ }
7632
+ /**
7633
+ * **宿主面的判活探针**(`(sessionId) => boolean`)——所有需要"这条会话还活着吗"的
7634
+ * 生产代码都应当用这一个,不要自己拼 `agents.get` + `hasSessionLogById`。
7635
+ *
7636
+ * 语义 = {@link isSessionAlive},其中:
7637
+ * · `liveIds` 因为宿主只给 `get(id)` 而**按 id 逐次问**(不是集合);
7638
+ * · `archivedIds` 在**首次调用时**取一次快照并在闭包内复用(每 tick 一个探针,
7639
+ * 归档集在一个 tick 内不变)—— 避免为每条轨迹重复读存储域。
7640
+ *
7641
+ * ⚠️ **快照的代价**:同一探针内若有人中途归档了某会话,本探针直到下次新建才看见。
7642
+ * 调度器是每 tick 新建一个探针(见 `wake/scheduler.ts`),故实际窗口 ≤1 tick。
7643
+ *
7644
+ * @param ctx 插件上下文(取 `agents` 与 `workspaceRegistry`)
7645
+ * @returns 探针;**永不抛**
7646
+ */
7647
+ function hostAliveProbe(ctx) {
7648
+ let archived;
7649
+ const archivedOnce = () => archived ??= archivedIdsOf(ctx);
7650
+ return (sessionId) => {
7651
+ if (typeof sessionId !== "string" || sessionId === "") return false;
7652
+ if (archivedOnce().has(sessionId)) return false;
7653
+ try {
7654
+ if (hostAgents(ctx)?.get?.(sessionId)) return true;
7655
+ } catch {}
7656
+ try {
7657
+ return hasSessionLogById(sessionsRootDir(), sessionId);
7658
+ } catch {
7659
+ return true;
7660
+ }
7661
+ };
7662
+ }
7663
+ /**
7664
+ * 判据只有**两个**出口,别再开第三个:
7665
+ * · {@link isSessionAlive} —— 纯判据(给定事实),单测的靶子;
7666
+ * · {@link hostAliveProbe} —— 生产入口(自己去宿主与磁盘取事实)。
7667
+ *
7668
+ * ⚠️ 曾有一个 `aliveFactsFor`(把探针摊平成 `SessionAliveFacts`),**全仓零调用点**,
7669
+ * 已删除 —— 留一个"看起来有用、实际无人调用"的导出,会让下一个人以为存在第二条判活路径
7670
+ * (本仓同族教训:`cheapWorkerPresetInheritance` 全仓零调用点 ⇒ 接线断了却看不出来)。
7671
+ */
7672
+ //#endregion
7673
7673
  //#region src/cro/core.ts
7674
7674
  /**
7675
7675
  * cro/core.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
@@ -9462,7 +9462,8 @@ async function createAndBindTarget(ctx, root, dirName) {
9462
9462
  });
9463
9463
  const openingNote = deliverCarrierOpening(agent, buildCarrierOpening(parseTrajectoryCode(dirName) ?? dirName, trajectoryDescOf(dirName))) ? ", opening-as-user" : ", ⚠️ 开场白未投递";
9464
9464
  const wsNote = workspaceId !== void 0 ? "" : await attachToWorkspace(ctx, root, sessionId);
9465
- const how = `auto-create(${sessionId}${bound ? ", bound" : ", ⚠️ 绑定写入失败"}${workspaceId !== void 0 ? ", via-workspaceId(=用户同一形态)" : ", via-cwd+attach"}${openingNote}${wsNote})`;
9465
+ const viaNote = workspaceId !== void 0 ? ", via-workspaceId(=用户同一形态)" : ", via-cwd+attach";
9466
+ const how = `auto-create(${sessionId}${bound.ok ? ", bound" : `, ⚠️ 绑定写入失败(${bound.code})`}${viaNote}${openingNote}${wsNote})`;
9466
9467
  return {
9467
9468
  agent,
9468
9469
  how
@@ -10475,7 +10476,7 @@ function createCognitiveTrajectoryTool(ctx) {
10475
10476
  dirName: info.dirName,
10476
10477
  mdPath: info.mdPath,
10477
10478
  sessionId: info.sessionId,
10478
- alive: hostAliveProbe(ctx)
10479
+ takeover: true
10479
10480
  });
10480
10481
  const advisory = advisoryHint(targetEntry.dirName, join(targetEntry.path, "SESSION.md"));
10481
10482
  const out = {
@@ -10484,10 +10485,16 @@ function createCognitiveTrajectoryTool(ctx) {
10484
10485
  context: active.context
10485
10486
  };
10486
10487
  if (advisory) out.advisory = advisory;
10487
- if (bound === true) out.binding = "written";
10488
- else if (bound === false) {
10488
+ if (bound !== null && bound.ok) {
10489
+ out.binding = "written";
10490
+ if (bound.replaced !== void 0) {
10491
+ out.bindingReplaced = bound.replaced;
10492
+ out.bindingHint = `已顶掉原载体 ${bound.replaced}(owner 裁决:use 无条件覆盖到当前会话)。若那条会话还活着,它此刻**不再**承载「${info?.dirName ?? targetEntry.dirName}」——这是单槽位的必然结果,不是错误;需要它继续承载就再对它 \`use\` 一次。`;
10493
+ }
10494
+ } else if (bound !== null) {
10489
10495
  out.binding = "not-written";
10490
- out.bindingHint = `绑定未写入:本会话取不到可用的 header(id / cwd 缺一不可)⇒ 按契约「绑定声明是唯一真相」,「${info?.dirName ?? targetEntry.dirName}」此刻仍是**未绑定**。 会话标题变了**不能**当作绑定成功的证据(改名走的是另一条通路)。 详见宿主日志里的 \`✗ 绑定未写入(SESSION_HEADER_UNRESOLVED)\`。`;
10496
+ out.bindingCode = bound.code;
10497
+ out.bindingHint = `绑定未写入(${bound.code}):${bound.detail} ⇒ 按契约「绑定声明是唯一真相」,「${info?.dirName ?? targetEntry.dirName}」此刻仍是**未绑定**。 会话标题变了**不能**当作绑定成功的证据(改名走的是另一条通路)。`;
10491
10498
  }
10492
10499
  return out;
10493
10500
  }
@@ -521,7 +521,12 @@ function findTrajectoryHolding(sessionsRoot, sessionId) {
521
521
  * @param decl 要写入的声明
522
522
  * @param opts.takeover 允许顶掉本条轨迹现有的另一个载体
523
523
  * @param opts.alive 探针:**旧载体**是否还活着(缺省 ⇒ 一律当活 = 旧语义)
524
- * @returns `{ ok: true }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
524
+ * @returns `{ ok: true, replaced?, released? }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
525
+ *
526
+ * 🔴 **2026-10-20 追加 `replaced`(由一条 skiff 轨迹的实测逼出)**:`released` 只覆盖
527
+ * 「这个会话此前安家在**另一条**轨迹」那一格(跨轨迹清理);而**同一轨迹换载体**时被顶掉的那个
528
+ * 会话 id 此前**没有任何出口** ⇒ 调用方无法在回执里说出"你顶掉了谁"。
529
+ * 静默换代正是"一条轨迹感觉有两个载体"的成因 ⇒ 顶掉谁必须能被读出来。
525
530
  */
526
531
  function claimBinding(sessionsRoot, dirName, decl, opts = {}) {
527
532
  const holder = findTrajectoryHolding(sessionsRoot, decl.sessionId);
@@ -547,9 +552,11 @@ function claimBinding(sessionsRoot, dirName, decl, opts = {}) {
547
552
  code: "WRITE_FAILED",
548
553
  detail: `写 ${BIND_FILENAME} 失败:${join(sessionsRoot, dirName)}`
549
554
  };
550
- return released === void 0 ? { ok: true } : {
555
+ const replaced = cur !== null && cur.sessionId !== decl.sessionId ? cur.sessionId : void 0;
556
+ return {
551
557
  ok: true,
552
- released
558
+ ...replaced === void 0 ? {} : { replaced },
559
+ ...released === void 0 ? {} : { released }
553
560
  };
554
561
  }
555
562
  //#endregion
@@ -718,14 +725,26 @@ function listBoundSessionIds(root, dirName, alive) {
718
725
  * 旧签名 `appendBound(session, action, rec)` 保持不变:`session` 提供 `header.id` / `header.cwd`,
719
726
  * `rec.dirName` 指明写哪条轨迹的交代。
720
727
  *
721
- * @returns 写入成功 ⇒ true(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**)
728
+ * @returns 写入成功 ⇒ `{ ok: true, replaced?, released? }`(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**);
729
+ * 失败 ⇒ `{ ok: false, code, detail }`,**`code` 指名是哪一条守卫拒的**
730
+ *
731
+ * 🔴 **为什么从 `boolean` 改成分级结果(2026-10-20,一条 skiff 轨迹实测逼出)**:
732
+ * 本函数有**两条完全不同的失败路径** —— ① header 守卫(id / root 取不到)② `claimBinding` 拒绝
733
+ * (`SESSION_TAKEN` / `TRAJECTORY_OCCUPIED` / `WRITE_FAILED`)。日志侧早已分开(D186),
734
+ * **但回执侧只有一句 header 文案** ⇒ 调用方被指向错误的方向(那位实测者第一轮就照文案去找 header,
735
+ * 真因是槽位被占)。⇒ 结果必须**带码**,让回执能说出是哪一半。
722
736
  */
723
737
  function appendBound(session, action, rec) {
724
738
  const id = sessionHeader(session)?.id;
725
739
  const root = rootOf(session);
726
740
  if (typeof id !== "string" || id === "" || root === null) {
727
- console.warn(`[serenity-hooks] ✗ 绑定未写入(SESSION_HEADER_UNRESOLVED):会话对象取不到可用的 header(id=${typeof id === "string" && id !== "" ? id : "(空/非字符串)"},root=${root ?? "null(header.cwd 缺失或上溯不到 .serenity)"}) ⇒ 轨迹「${rec.dirName}」的 current.dsh_session_bind **未被改写**。 排查:该会话的 header.cwd 是否指向某个含 .serenity 的目录; 无 cwd 的会话(如某些程序性创建的载体)**不会**写绑定。`);
728
- return false;
741
+ const detail = `会话对象取不到可用的 header(id=${typeof id === "string" && id !== "" ? id : "(空/非字符串)"},root=${root ?? "null(header.cwd 缺失或上溯不到 .serenity)"}) ⇒ 轨迹「${rec.dirName}」的 current.dsh_session_bind **未被改写**。 排查:该会话的 header.cwd 是否指向某个含 .serenity 的目录; 无 cwd 的会话(如某些程序性创建的载体)**不会**写绑定。`;
742
+ console.warn(`[serenity-hooks] ✗ 绑定未写入(SESSION_HEADER_UNRESOLVED):${detail}`);
743
+ return {
744
+ ok: false,
745
+ code: "SESSION_HEADER_UNRESOLVED",
746
+ detail
747
+ };
729
748
  }
730
749
  const result = claimBinding(sessionsRoot(root), rec.dirName, {
731
750
  sessionId: id,
@@ -739,9 +758,17 @@ function appendBound(session, action, rec) {
739
758
  });
740
759
  if (!result.ok) {
741
760
  console.warn(`[serenity-hooks] ✗ 绑定未写入(${result.code}):${result.detail}`);
742
- return false;
761
+ return {
762
+ ok: false,
763
+ code: result.code,
764
+ detail: result.detail
765
+ };
743
766
  }
744
- return true;
767
+ return {
768
+ ok: true,
769
+ ...result.replaced === void 0 ? {} : { replaced: result.replaced },
770
+ ...result.released === void 0 ? {} : { released: result.released }
771
+ };
745
772
  }
746
773
  /**
747
774
  * 这条轨迹**是否曾经有过载体**(判"要不要允许自建")。
@@ -2,7 +2,7 @@ import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roo
2
2
  import { g as readUtf8, o as isSafeModeOn, p as readCognitiveTrajectorySkills, u as readBlacklist } from "./ccc-CyYJL4F2.js";
3
3
  import { B as isoLocal, D as readActiveSessionMd, a as readTrajectoryConstraintAt, c as trajectoryConstraintForSession, d as unregisterTrajectorySession$1, j as sessionsRoot, l as registerTrajectorySession$1, p as DEFAULT_SESSION_SCOPE, u as trajectoryDirNameForSession } from "./profile-5TQ_C1xi.js";
4
4
  import { a as readGitTrack, i as localstorePath } from "./service-DDmHSglV.js";
5
- import { _ as ACC_VERSION, d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt } from "./message-source-DK2CnXlh.js";
5
+ import { _ as ACC_VERSION, d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt } from "./message-source-DiP3Uve3.js";
6
6
  import { basename, dirname, join, resolve } from "node:path";
7
7
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -241,13 +241,19 @@ export declare function findTrajectoryHolding(sessionsRoot: string, sessionId: s
241
241
  * @param decl 要写入的声明
242
242
  * @param opts.takeover 允许顶掉本条轨迹现有的另一个载体
243
243
  * @param opts.alive 探针:**旧载体**是否还活着(缺省 ⇒ 一律当活 = 旧语义)
244
- * @returns `{ ok: true }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
244
+ * @returns `{ ok: true, replaced?, released? }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
245
+ *
246
+ * 🔴 **2026-10-20 追加 `replaced`(由一条 skiff 轨迹的实测逼出)**:`released` 只覆盖
247
+ * 「这个会话此前安家在**另一条**轨迹」那一格(跨轨迹清理);而**同一轨迹换载体**时被顶掉的那个
248
+ * 会话 id 此前**没有任何出口** ⇒ 调用方无法在回执里说出"你顶掉了谁"。
249
+ * 静默换代正是"一条轨迹感觉有两个载体"的成因 ⇒ 顶掉谁必须能被读出来。
245
250
  */
246
251
  export declare function claimBinding(sessionsRoot: string, dirName: string, decl: BindDeclaration, opts?: {
247
252
  takeover?: boolean;
248
253
  alive?: (sessionId: string) => boolean;
249
254
  }): {
250
255
  ok: true;
256
+ replaced?: string;
251
257
  released?: string;
252
258
  } | {
253
259
  ok: false;
@@ -120,13 +120,37 @@ export declare function supersedeOtherBindings(_root: string, _dirName: string,
120
120
  * 保留本函数只为不打断既有调用点,恒返回空数组。
121
121
  */
122
122
  export declare function pruneMissingBindings(_root: string, _isMissing: (sessionId: string) => boolean): string[];
123
+ /**
124
+ * 绑定写入的结果(分级,**不是 boolean**)。
125
+ *
126
+ * 分级的意义 = **调用方能说出是哪一半失败**(见 `appendBound` 头注)。
127
+ */
128
+ export type BindWriteCode = 'SESSION_HEADER_UNRESOLVED' | 'SESSION_TAKEN' | 'TRAJECTORY_OCCUPIED' | 'WRITE_FAILED';
129
+ export type BindWriteResult = {
130
+ ok: true;
131
+ /** 同一轨迹上被顶掉的旧载体 id(`use` 无条件覆盖时要写进回执 —— 静默换代是不允许的) */
132
+ replaced?: string;
133
+ /** 被清掉的**另一条**轨迹的残留声明(`claimBinding` 的安全网) */
134
+ released?: string;
135
+ } | {
136
+ ok: false;
137
+ code: BindWriteCode;
138
+ detail: string;
139
+ };
123
140
  /**
124
141
  * 把一条轨迹的当前载体写成 `sessionId`(**唯一写入口**,内部走 `claimBinding` 的两条不变量)。
125
142
  *
126
143
  * 旧签名 `appendBound(session, action, rec)` 保持不变:`session` 提供 `header.id` / `header.cwd`,
127
144
  * `rec.dirName` 指明写哪条轨迹的交代。
128
145
  *
129
- * @returns 写入成功 ⇒ true(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**)
146
+ * @returns 写入成功 ⇒ `{ ok: true, replaced?, released? }`(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**);
147
+ * 失败 ⇒ `{ ok: false, code, detail }`,**`code` 指名是哪一条守卫拒的**
148
+ *
149
+ * 🔴 **为什么从 `boolean` 改成分级结果(2026-10-20,一条 skiff 轨迹实测逼出)**:
150
+ * 本函数有**两条完全不同的失败路径** —— ① header 守卫(id / root 取不到)② `claimBinding` 拒绝
151
+ * (`SESSION_TAKEN` / `TRAJECTORY_OCCUPIED` / `WRITE_FAILED`)。日志侧早已分开(D186),
152
+ * **但回执侧只有一句 header 文案** ⇒ 调用方被指向错误的方向(那位实测者第一轮就照文案去找 header,
153
+ * 真因是槽位被占)。⇒ 结果必须**带码**,让回执能说出是哪一半。
130
154
  */
131
155
  export declare function appendBound(session: unknown, action: SessionBoundAction, rec: {
132
156
  dirName: string;
@@ -144,7 +168,7 @@ export declare function appendBound(session: unknown, action: SessionBoundAction
144
168
  * 生产调用点应当传 {@link hostAliveProbe}(`trajectory/alive.ts`)。
145
169
  */
146
170
  alive?: (sessionId: string) => boolean;
147
- }): boolean;
171
+ }): BindWriteResult;
148
172
  /**
149
173
  * 这条轨迹**是否曾经有过载体**(判"要不要允许自建")。
150
174
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.79.4",
3
+ "version": "1.79.5",
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 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {