@shgroup/dsh-serenity-hooks 1.70.0 → 1.72.0

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.
Files changed (105) hide show
  1. package/README.md +21 -19
  2. package/dsh.plugin.json +2 -2
  3. package/lib/{ccc-C33QWoWZ.js → ccc-Dkoa2oW3.js} +16 -2
  4. package/lib/{ccc-roots-RzasspMr.js → ccc-roots-jZYR2GVE.js} +16 -10
  5. package/lib/client.js +2 -2
  6. package/lib/{gateway-auth.d.ts → gateway/auth.d.ts} +3 -3
  7. package/lib/{gateway-dsh-auth.d.ts → gateway/dsh-auth.d.ts} +1 -1
  8. package/lib/{gateway-proxy.d.ts → gateway/proxy.d.ts} +3 -3
  9. package/lib/{gateway.d.ts → gateway/server.d.ts} +5 -5
  10. package/lib/{totp.d.ts → gateway/totp.d.ts} +1 -1
  11. package/lib/{output-guard.d.ts → guard/filter.d.ts} +2 -2
  12. package/lib/{output-guard-seam.d.ts → guard/seam.d.ts} +2 -2
  13. package/lib/{weixin-api-BrRcPiQs.js → ilink-DEpmE3yi.js} +6 -6
  14. package/lib/im/accounts.d.ts +105 -0
  15. package/lib/{weixin-bridge.d.ts → im/bridge.d.ts} +16 -9
  16. package/lib/{im-bridge.d.ts → im/channels.d.ts} +3 -3
  17. package/lib/{weixin-send-endpoint.d.ts → im/endpoint.d.ts} +2 -2
  18. package/lib/{weixin-output-guard.d.ts → im/guard.d.ts} +1 -1
  19. package/lib/{weixin-hook.d.ts → im/hook.d.ts} +1 -1
  20. package/lib/{weixin-api.d.ts → im/ilink.d.ts} +3 -3
  21. package/lib/{human-channel.d.ts → im/inbound.d.ts} +4 -4
  22. package/lib/{weixin-route.d.ts → im/route.d.ts} +52 -11
  23. package/lib/{weixin-send-api.d.ts → im/send.d.ts} +3 -3
  24. package/lib/im/weixin-account.d.ts +45 -0
  25. package/lib/{im-weixin.d.ts → im/weixin-channel.d.ts} +2 -2
  26. package/lib/index.js +1008 -815
  27. package/lib/invariant.js +2 -2
  28. package/lib/{ccc-roots.d.ts → kernel/ccc-roots.d.ts} +10 -10
  29. package/lib/{ccc.d.ts → kernel/ccc.d.ts} +114 -44
  30. package/lib/{config-ops.d.ts → kernel/config.d.ts} +2 -2
  31. package/lib/kernel/constants.d.ts +20 -0
  32. package/lib/{invariant.d.ts → kernel/invariant.d.ts} +1 -1
  33. package/lib/{json.d.ts → kernel/json.d.ts} +1 -1
  34. package/lib/{message-source.d.ts → kernel/message-source.d.ts} +1 -1
  35. package/lib/{ports.d.ts → kernel/ports.d.ts} +1 -1
  36. package/lib/{time.d.ts → kernel/time.d.ts} +1 -1
  37. package/lib/message-source-kVoNK0C5.js +432 -0
  38. package/lib/{deepseek-vision-patch.d.ts → patches/deepseek-vision.d.ts} +3 -3
  39. package/lib/{opencode-provider.d.ts → patches/opencode-provider.d.ts} +1 -1
  40. package/lib/{web-fetch-provider.d.ts → patches/web-fetch.d.ts} +1 -1
  41. package/lib/{message-source-DhA1dVBi.js → profile-DyMJ_bfr.js} +277 -463
  42. package/lib/role/skiff/config.d.ts +79 -0
  43. package/lib/role/skiff/core.d.ts +155 -0
  44. package/lib/{skiff-debug.d.ts → role/skiff/debug.d.ts} +4 -4
  45. package/lib/role/skiff/profile.d.ts +115 -0
  46. package/lib/role/skiff/registry.d.ts +41 -0
  47. package/lib/{cheap-worker-ops.d.ts → role/worker/core.d.ts} +1 -1
  48. package/lib/{agent-idle.d.ts → role/worker/idle.d.ts} +2 -2
  49. package/lib/{cheap-worker-preset-inherit.d.ts → role/worker/preset.d.ts} +2 -2
  50. package/lib/{weixin-route-D_DvgPqy.js → route-f7JlinIt.js} +132 -47
  51. package/lib/seams/context.d.ts +3 -3
  52. package/lib/seams/guards.d.ts +18 -12
  53. package/lib/seams/keeper.d.ts +1 -1
  54. package/lib/seams/system-prompt.d.ts +3 -1
  55. package/lib/settings-lt1i3OZv.js +403 -0
  56. package/lib/{skills-discovery.d.ts → skills/discovery.d.ts} +1 -1
  57. package/lib/skills/resource-index.d.ts +2 -2
  58. package/lib/{face-host.d.ts → surface/faces.d.ts} +4 -4
  59. package/lib/{api.d.ts → surface/http.d.ts} +2 -2
  60. package/lib/{container-status.d.ts → surface/observation.d.ts} +16 -16
  61. package/lib/{settings-section.d.ts → surface/settings.d.ts} +1 -1
  62. package/lib/{status.d.ts → surface/status.d.ts} +2 -2
  63. package/lib/{usage-stats.d.ts → surface/usage.d.ts} +4 -4
  64. package/lib/{voyage-page.d.ts → surface/voyage/page.d.ts} +2 -2
  65. package/lib/{voyage-vocab.d.ts → surface/voyage/vocab.d.ts} +1 -1
  66. package/lib/tools/cognitive-trajectory.d.ts +2 -2
  67. package/lib/tools/{acc-diag.d.ts → diag/index.d.ts} +2 -2
  68. package/lib/{diag-ops.d.ts → tools/diag/service.d.ts} +7 -7
  69. package/lib/tools/eap.d.ts +1 -1
  70. package/lib/tools/fs/index.d.ts +7 -0
  71. package/lib/{fs-ops.d.ts → tools/fs/service.d.ts} +2 -2
  72. package/lib/tools/git/index.d.ts +4 -0
  73. package/lib/{git-ops.d.ts → tools/git/service.d.ts} +2 -2
  74. package/lib/tools/im-bridge.d.ts +2 -2
  75. package/lib/tools/{kit.d.ts → kit/index.d.ts} +1 -1
  76. package/lib/{kit-ops.d.ts → tools/kit/service.d.ts} +6 -6
  77. package/lib/tools/{localstore.d.ts → localstore/index.d.ts} +1 -1
  78. package/lib/{localstore-ops.d.ts → tools/localstore/service.d.ts} +2 -2
  79. package/lib/tools/{msm.d.ts → msm/index.d.ts} +1 -1
  80. package/lib/{msm-ops.d.ts → tools/msm/service.d.ts} +2 -2
  81. package/lib/tools/skiff-admin.d.ts +19 -14
  82. package/lib/{cognitive-trajectory-assistant.d.ts → trajectory/assistant.d.ts} +2 -2
  83. package/lib/{cognitive-trajectory-bound.d.ts → trajectory/bound.d.ts} +2 -2
  84. package/lib/{session-cleanup.d.ts → trajectory/cleanup.d.ts} +1 -1
  85. package/lib/{cognitive-trajectory-ops.d.ts → trajectory/core.d.ts} +55 -25
  86. package/lib/{live-sessions.d.ts → trajectory/live.d.ts} +1 -1
  87. package/lib/trajectory/prompt.d.ts +101 -0
  88. package/lib/{rebuild.d.ts → trajectory/rebuild.d.ts} +1 -1
  89. package/lib/{cognitive-trajectory-skills.d.ts → trajectory/skills.d.ts} +1 -1
  90. package/lib/{clock-runtime.d.ts → wake/clock.d.ts} +3 -3
  91. package/lib/{cro-guide.d.ts → wake/cro-guide.d.ts} +1 -1
  92. package/lib/{cro-log.d.ts → wake/cro-log.d.ts} +3 -3
  93. package/lib/{cro-turns.d.ts → wake/cro-turns.d.ts} +5 -5
  94. package/lib/{cro.d.ts → wake/cro.d.ts} +6 -6
  95. package/lib/{wake-registry.d.ts → wake/registry.d.ts} +2 -2
  96. package/lib/{wake-scheduler.d.ts → wake/scheduler.d.ts} +3 -3
  97. package/package.json +2 -2
  98. package/lib/constants.d.ts +0 -6
  99. package/lib/settings-section-0TPYWug9.js +0 -238
  100. package/lib/skiff-core.d.ts +0 -145
  101. package/lib/skiff-registry.d.ts +0 -26
  102. package/lib/skiff-role-cwxAt0gE.js +0 -203
  103. package/lib/skiff-role.d.ts +0 -89
  104. package/lib/tools/cc-fs.d.ts +0 -7
  105. package/lib/tools/git.d.ts +0 -4
@@ -1,9 +1,10 @@
1
- import { r as cccRootForCwd } from "./ccc-roots-RzasspMr.js";
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
+ import { _ as resolveInside, g as readUtf8 } from "./ccc-Dkoa2oW3.js";
2
3
  import { basename, dirname, join } from "node:path";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
- //#region src/time.ts
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
5
+ //#region src/kernel/time.ts
5
6
  /**
6
- * time.ts — ACC **时间呈现的单一真相源**(S142 2026-09-17,所有者令:「统一 ACC 所有使用时间的地方使用当地时区」)
7
+ * kernel/time.ts — ACC **时间呈现的单一真相源**(S142 2026-09-17,所有者令:「统一 ACC 所有使用时间的地方使用当地时区」)
7
8
  *
8
9
  * ── 存在的理由:**把"时刻"与"呈现"分开**(R↓)
9
10
  *
@@ -87,9 +88,9 @@ function localIdStamp(value = Date.now()) {
87
88
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
88
89
  }
89
90
  //#endregion
90
- //#region src/cognitive-trajectory-ops.ts
91
+ //#region src/trajectory/core.ts
91
92
  /**
92
- * cognitive-trajectory-ops.ts — 轨迹工具纯操作层(零 DSH 依赖,可独立单测)
93
+ * trajectory/core.ts — 轨迹工具纯操作层(零 DSH 依赖,可独立单测)
93
94
  *
94
95
  * 行为对齐 osp(opencode-serenity-plugin/src/session/lib.ts)——osp 是 ACC 工具 spec。
95
96
  *
@@ -212,10 +213,44 @@ function readAllSessions(sessionsDir) {
212
213
  return [];
213
214
  }
214
215
  }
215
- /** 提取目录名中的会话 ID(S### 或 issue 名);无匹配返回 '' */
216
+ /** 目录名的日期前缀(`createSession` 恒写;`isSessionDirName` 同源判据) */
217
+ const DIR_DATE_PREFIX_RE = /^\d{4}-\d{2}-\d{2}--/;
218
+ /** 合法编码:`[A-Za-z0-9]` 开头,其后 `[A-Za-z0-9._-]`,≤64,**不含 `--`** */
219
+ const CODE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
220
+ /**
221
+ * 校验轨迹编码形态。**这是写入侧(create --id)与外部输入的唯一闸门。**
222
+ *
223
+ * 判据三条(R↓):
224
+ * ① `CODE_PATTERN` —— 只允许交流安全字符:编码会被逐字打在命令、消息、日志里,
225
+ * 含空格/引号/中文会在 shell、JSON、markdown 各需一套转义(旧 UID 的同一条理由,D158);
226
+ * ② **不得含 `--`** —— 目录名解析用它当分隔符(见上);
227
+ * ③ 长度上限 64(同旧 UID;够长以致不会撞名,够短以致能打出来)。
228
+ *
229
+ * ⚠️ 大小写**敏感**:`S142` 与 `s142` 是两个不同编码(编码不是近似匹配语义)。
230
+ */
231
+ function isValidTrajectoryCode(code) {
232
+ return CODE_PATTERN.test(code) && !code.includes("--");
233
+ }
234
+ /**
235
+ * 从目录名解析轨迹编码(固定位置 = 剥掉日期前缀后的第一段)。
236
+ *
237
+ * @returns 编码;目录名不带日期前缀(非本工具建的 / 损坏)→ `null`
238
+ */
239
+ function parseTrajectoryCode(dirName) {
240
+ if (!DIR_DATE_PREFIX_RE.test(dirName)) return null;
241
+ const rest = dirName.replace(DIR_DATE_PREFIX_RE, "");
242
+ const sep = rest.indexOf("--");
243
+ const code = sep === -1 ? rest : rest.slice(0, sep);
244
+ return code === "" ? null : code;
245
+ }
246
+ /**
247
+ * 展示用会话 ID(`list` 面板 / 绑定记录的 `sessionId` 字段)。
248
+ *
249
+ * 🔴 2026-10-19 起**编码即 ID**,不再只认 `S###` —— 旧实现只匹配 `--S(\d{3,})--`,
250
+ * 自定义编码(owner 令允许的那类)会全被渲染成空串。无编码 → `''`(不是目录名:调用方各有回退)。
251
+ */
216
252
  function extractSessionId(dirName) {
217
- const m = dirName.match(/--S(\d{3,})--/);
218
- return m ? `S${m[1]}` : "";
253
+ return parseTrajectoryCode(dirName) ?? "";
219
254
  }
220
255
  /**
221
256
  * UID 的声明文件名(**固定名、住在轨迹目录内**)。
@@ -233,19 +268,32 @@ function extractSessionId(dirName) {
233
268
  * | **独立小文件**(选定) | 与目录名、SESSION.md **都解耦**;改标识符不动正文、不动目录名;删文件即"回到用编码" |
234
269
  *
235
270
  * ## 语义边界(R↓)
236
- * - **可选**:文件不存在 ⇒ 用编码(`S###`),**不是错误**。
271
+ * - **可选**:文件不存在 ⇒ 用编码,**不是错误**。
237
272
  * - **只用于"被指认"**:它是 `findSession` 的一个**解析分支**,不参与排序、不参与展示优先级、
238
273
  * **不改变**目录名与 SESSION.md 一个字节(与 `archive` 同一条纪律:只换指认方式,不动本体)。
239
- * - **唯一性**:由 `setTrajectoryUid` 在写入时保证(重复即拒绝)。读取侧不做全表唯一性断言 ——
240
- * 外部手工造的重复应由写入侧拦,读取侧只能"第一个命中",那是注入侧的职责不是解析侧的。
274
+ *
275
+ * ## 🔴 2026-10-19:**写入侧退役,读取侧保留**
276
+ *
277
+ * owner 追加令裁 B 案:「取消 `S###` 与 UID 的二元性 —— 编码就是唯一标识,用户可指定」。
278
+ * UID 当初存在的**唯一理由**是「命名很可能由于变化而变化」(D158),而现在的编码
279
+ * **本身就稳定且可指定**(`create --id`)⇒ 该理由被覆盖 ⇒ **二元性取消**。
280
+ *
281
+ * 处置(**不对称,刻意**):
282
+ * - **写**:`setTrajectoryUid()` **已删除**(`uid` 动作改为报"已退役"并指向 `create --id`);
283
+ * - **读**:本文件与 `findSession` 的 UID 分支**保留** —— 存量文件(本仓 = S142 的
284
+ * `serenity-acc-maintainer`)仍能按它指认,**不因退役而失去能力**。
285
+ *
286
+ * ⚠️ **为什么不物理删除那些文件**:删了就丢掉一个仍在被读的指认通道(能力倒退),
287
+ * 而保留的成本是零(文件不存在 ⇒ `readFileSync` 失败 ⇒ null)。
288
+ * 判据:**退役的是"产生新标识"这种能力,不是"旧标识还在被用"这个事实。**
241
289
  */
242
290
  const UID_FILENAME = "TRAJECTORY-UID";
243
- /** UID 合法形态:**保守**(只允许交流安全的字符)—— 它会被用在命令行、消息文本、日志里 */
291
+ /** UID 合法形态(**只读兼容用**;新编码的形态校验见 `isValidTrajectoryCode`) */
244
292
  const UID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
245
293
  /**
246
- * 校验 UID 形态。
294
+ * 校验 UID 形态(**只读兼容路径** —— 写入侧已退役,见 `UID_FILENAME` 的 R↓ 段)。
247
295
  *
248
- * 为什么保守(R↓):UID 是**给人和 agent 在对话里互相指认用的**(owner:「用于交流」)⇒
296
+ * 为什么保守:UID 是**给人和 agent 在对话里互相指认用的**(owner:「用于交流」)⇒
249
297
  * 它必须能**逐字打出来**。含空格/引号/中文的 UID 会在 shell、JSON、markdown 里各自需要一套转义
250
298
  * ⇒ "用于交流"这个目的当场失败。⇒ 限定 `[A-Za-z0-9._-]`,首字符必须字母数字,上限 64。
251
299
  *
@@ -260,7 +308,7 @@ function isValidUid(uid) {
260
308
  *
261
309
  * 🔴 **形态非法的文件按"没设定"处理,而不是报错**:解析侧出现在**每一次** `findSession` 里
262
310
  * (show/use/archive/send-now/send-later 全走它),在那里抛错会让一个坏文件**炸掉整条轨迹的所有操作**
263
- * —— 惩罚远大于问题本身。坏 UID 由 `setTrajectoryUid` 在**写入侧**拦住(那里才该响亮失败)。
311
+ * —— 惩罚远大于问题本身。而写入侧已退役 ⇒ 坏文件只可能来自外部手改,更不该连坐。
264
312
  */
265
313
  function readTrajectoryUid(sessionPath) {
266
314
  try {
@@ -271,58 +319,33 @@ function readTrajectoryUid(sessionPath) {
271
319
  }
272
320
  }
273
321
  /**
274
- * 设定 / 清除一条轨迹的 UID。
275
- *
276
- * @param sessionsDir AGENT_SESSIONS 绝对路径
277
- * @param key 定位用(目录名 / S### / 既有 UID / 唯一模糊串)
278
- * @param uid 新 UID;`null` ⇒ **删除声明文件**(回到"用编码")
279
- * @throws UID 形态非法 / 已被**别的**轨迹占用 / 目标轨迹不存在
280
- */
281
- function setTrajectoryUid(sessionsDir, key, uid) {
282
- const target = findSession(sessionsDir, key);
283
- if (!target) throw new Error(`Trajectory not found: "${key}". Use "list" to see available trajectories.`);
284
- const file = join(target.path, UID_FILENAME);
285
- if (uid === null) {
286
- rmSync(file, { force: true });
287
- return {
288
- dirName: target.dirName,
289
- uid: null,
290
- path: file
291
- };
292
- }
293
- if (!isValidUid(uid)) throw new Error(`Invalid trajectory UID: "${uid}". 允许 [A-Za-z0-9._-],首字符须字母或数字,长度 ≤64(它要被逐字打出来用于交流 ⇒ 不容忍需要转义的字符)`);
294
- const clash = readAllSessions(sessionsDir).find((s) => s.dirName !== target.dirName && readTrajectoryUid(s.path) === uid);
295
- if (clash) throw new Error(`UID "${uid}" 已被 ${clash.dirName} 占用(唯一标识符必须唯一;先解除它或换一个)`);
296
- writeFileSync(file, uid + "\n", "utf-8");
297
- return {
298
- dirName: target.dirName,
299
- uid,
300
- path: file
301
- };
302
- }
303
- /**
304
322
  * 根据 key 查找轨迹。
305
323
  *
306
324
  * 解析顺序(**越精确越靠前**,且**前三级都不做模糊**):
307
- * ① 目录名精确 / ② **UID 精确** / ③ 编码 `S###`(允许 `S31`→`031`)/ ④ 唯一模糊子串(多个则报错)
325
+ * ① 目录名精确 / ② **编码精确** / ③ **UID 精确(只读兼容)** / ④ 唯一模糊子串(多个则报错)
326
+ *
327
+ * 🔴 **2026-10-19 起编码排在 UID 之前**(旧顺序相反):编码现在是**一等标识**
328
+ * (owner:「固定为目录中的固定位置编码」+「允许用户指定」),而 UID 已退役为兼容层。
329
+ * 顺序对结果无实质影响(两者都属精确匹配、形态互斥 —— 编码不含 `--`),
330
+ * 但把顺序写死,避免后来者以为"编码是模糊匹配的一种"或"UID 优先"。
308
331
  *
309
- * 🔴 **UID 排在编码之前**是因为 owner 定的语义:UID 是"单独设定"的**交流标识**
310
- * ("不设定就还是用编码"⇒ 设定之后,用 UID 指认就该与编码同等可靠)。
311
- * 它与目录名都属**精确匹配**、彼此不会混淆(UID 形态不含 `--`,目录名必含)⇒ 顺序对结果无实质影响,
312
- * 但把这个顺序写死,避免后来者以为"UID 是模糊匹配的一种"。
332
+ * 🔵 `S31` → `S031` 的**数字补零**兼容保留(存量用户习惯);它现在只是编码匹配的一个特例:
333
+ * 编码恰为 `S###` 形态时允许省前导零。
313
334
  */
314
335
  function findSession(sessionsDir, key) {
315
336
  const all = readAllSessions(sessionsDir);
316
337
  const byName = all.find((s) => s.dirName === key);
317
338
  if (byName) return byName;
339
+ const byCode = all.find((s) => parseTrajectoryCode(s.dirName) === key);
340
+ if (byCode) return byCode;
341
+ const padded = key.match(/^S(\d{1,})$/);
342
+ if (padded) {
343
+ const want = `S${padded[1].padStart(3, "0")}`;
344
+ const byPadded = all.find((s) => parseTrajectoryCode(s.dirName) === want);
345
+ if (byPadded) return byPadded;
346
+ }
318
347
  const byUid = all.find((s) => readTrajectoryUid(s.path) === key);
319
348
  if (byUid) return byUid;
320
- const searchId = key.replace(/^S/, "").padStart(3, "0");
321
- const byId = all.find((s) => {
322
- const m = s.dirName.match(/--S(\d{3,})--/);
323
- return m && m[1] === searchId;
324
- });
325
- if (byId) return byId;
326
349
  const lower = key.toLowerCase();
327
350
  const fuzzy = all.filter((s) => s.dirName.toLowerCase().includes(lower));
328
351
  if (fuzzy.length === 1) return fuzzy[0] ?? null;
@@ -380,6 +403,12 @@ function createSession(opts) {
380
403
  const datePrefix = localDate(now);
381
404
  if (!desc && !issue) throw new Error("create requires either --desc or --issue");
382
405
  if (desc && issue) throw new Error("--desc and --issue are mutually exclusive");
406
+ const requestedId = opts.id?.trim();
407
+ if (requestedId !== void 0 && requestedId !== "") {
408
+ if (!isValidTrajectoryCode(requestedId)) throw new Error(`Invalid trajectory code: "${requestedId}". 允许 [A-Za-z0-9._-]、首字符须字母或数字、长度 ≤64,且**不得含中文与 "--"**("--" 是目录名分隔符,含它就把"固定位置编码"这个前提破坏掉)`);
409
+ const clash = readAllSessions(sessionsDir).find((s) => parseTrajectoryCode(s.dirName) === requestedId);
410
+ if (clash) throw new Error(`Trajectory code "${requestedId}" is already in use by ${clash.dirName}. Codes are the trajectory's unique identity — pick another, or operate on the existing one by that code.`);
411
+ }
383
412
  if (issue) {
384
413
  if (issue.length > 100) throw new Error(`issue too long: ${issue.length} chars (max 100)`);
385
414
  const dirName = `${datePrefix}--${sanitizeDirName(issue)}`;
@@ -403,31 +432,35 @@ function createSession(opts) {
403
432
  if (!desc || desc.length === 0) throw new Error("description cannot be empty");
404
433
  if (desc.length > 200) throw new Error(`description too long: ${desc.length} chars (max 200)`);
405
434
  const sessions = readAllSessions(sessionsDir);
406
- let maxId = 0;
407
- for (const s of sessions) {
408
- const m = s.dirName.match(/--S(\d{3,})--/);
409
- if (m) {
410
- const num = parseInt(m[1], 10);
411
- if (num > maxId) maxId = num;
435
+ let code;
436
+ if (requestedId !== void 0 && requestedId !== "") code = requestedId;
437
+ else {
438
+ let maxId = 0;
439
+ for (const s of sessions) {
440
+ const m = parseTrajectoryCode(s.dirName)?.match(/^S(\d{3,})$/);
441
+ if (m) {
442
+ const num = parseInt(m[1], 10);
443
+ if (num > maxId) maxId = num;
444
+ }
412
445
  }
446
+ code = `S${String(maxId + 1).padStart(3, "0")}`;
413
447
  }
414
- const nextId = String(maxId + 1).padStart(3, "0");
415
- const dirName = `${datePrefix}--S${nextId}--${sanitizeDirName(desc)}`;
448
+ const dirName = `${datePrefix}--${code}--${sanitizeDirName(desc)}`;
416
449
  const sessionPath = join(sessionsDir, dirName);
417
450
  if (!dryRun && existsSync(sessionPath)) throw new Error(`Session directory already exists: "${dirName}"`);
418
451
  if (dryRun) return {
419
- message: `[dry-run] Would create: ${dirName}/\n goal=${goal ?? "(none)"}`,
452
+ message: `[dry-run] Would create: ${dirName}/\n code=${code}\n goal=${goal ?? "(none)"}`,
420
453
  dirName,
421
454
  sessionPath,
422
- sessionId: `S${nextId}`
455
+ sessionId: code
423
456
  };
424
457
  mkdirSync(sessionPath, { recursive: true });
425
- writeFileSync(join(sessionPath, SESSION_MD), sessionMdTemplate(desc, `S${nextId}`, goal, now), "utf-8");
458
+ writeFileSync(join(sessionPath, SESSION_MD), sessionMdTemplate(desc, code, goal, now), "utf-8");
426
459
  return {
427
- message: `Created: ${dirName}/ (S${nextId})`,
460
+ message: `Created: ${dirName}/ (${code})`,
428
461
  dirName,
429
462
  sessionPath,
430
- sessionId: `S${nextId}`
463
+ sessionId: code
431
464
  };
432
465
  }
433
466
  /**
@@ -564,9 +597,8 @@ function parseSessionContextFromEvents(events) {
564
597
  filePath = join(md, SESSION_MD);
565
598
  } else continue;
566
599
  if (!isSessionDirName(dirName)) continue;
567
- const idMatch = dirName.match(/--S(\d{3,})--/);
568
600
  return {
569
- sessionId: idMatch ? `S${idMatch[1]}` : dirName,
601
+ sessionId: parseTrajectoryCode(dirName) ?? dirName,
570
602
  dirName,
571
603
  mdPath: filePath
572
604
  };
@@ -718,432 +750,214 @@ function archiveSession(root, key, dryRun = false) {
718
750
  };
719
751
  }
720
752
  //#endregion
721
- //#region src/cognitive-trajectory-bound.ts
722
- /**
723
- * cognitive-trajectory-bound.ts — SESSION 绑定持久化(CCC 内 `AGENT_SESSIONS/.bindings.json`)
724
- *
725
- * 目标(S142 用户拍板,方案 v1.0):让「dsh 会话(载体)↔ SESSION(宁静号轨迹)」
726
- * 的绑定坚固——LLM 不能因智力因素(幻觉/理解偏差)在过程中或 cognitive_trajectory rebuild 后
727
- * 自行更换 SESSION。
728
- *
729
- * 存储形态(v1.30.6 起,S142 review F-01):绑定**不再写入 dsh 会话日志**。
730
- * 原因(宿主契约实证):dsp 的自定义事件 `serenity/bound` 既不在宿主的
731
- * `KNOWN_SESSION_EVENT_TYPES`(生成静态集,明确说明 out-of-repo 插件事件不在其中),
732
- * 而 `Session.append` 构造事件时只写 `{type,seq,time,data,surfaceOp?,sourceEventSeqs?}`
733
- * ——**没有写入 envelope `ignorable` 标记的通道**;宿主的 `assertEventsSupported`
734
- * (session-persistence/coordinator.ts:1248-1252,调用点含恢复/HMR 路径)会拒绝任何
735
- * "未知且非 ignorable" 的事件 → 含该事件的会话在冷加载时可能抛
736
- * `SessionFormatUnsupportedError`。v1.29.1~v1.30.5 的设计文档虽写明要带
737
- * `ignorable: true`(docs/session-binding-hardening-research.md:53),但类型层声明
738
- * 不等于持久层标记,实际从未落盘。
739
- *
740
- * 现形态:每 CCC 一个 JSON 文件 `AGENT_SESSIONS/.bindings.json`(原子写:tmp + rename),
741
- * 以 dsh 会话 id 为键,latest-wins 覆盖。**向后兼容**:读取时若文件中无该会话记录,
742
- * 仍回落到扫描会话日志中的旧 `serenity/bound` 事件(老会话的绑定不丢)。
743
- *
744
- * 编码无关(U4):绑定锚 = **完整 AGENT_SESSIONS 目录名**(磁盘唯一存在),
745
- * sessionId(S###/apaas-xxx/自定义)仅是派生展示字段——绝不假设 S 前缀。
746
- */
747
- /** 旧形态事件类型(v1.29.1~v1.30.5 写入会话日志;v1.30.6 起只读不写——兼容存量绑定) */
748
- const SESSION_BOUND_EVENT = "serenity/bound";
749
- /** 绑定文件相对 CCC 根的路径 */
750
- const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
751
- const BINDINGS_VERSION = 1;
752
- let domainStore = null;
753
- /**
754
- * 注入域句柄(装载期调用一次)。传 `null` = 域不可用 ⇒ 全部退回旧文件行为。
755
- * 幂等:重复调用以后者为准(HMR / 重载场景)。
756
- */
757
- function setBindingStore(store) {
758
- domainStore = store;
753
+ //#region src/role/skiff/registry.ts
754
+ const skiffSessions = /* @__PURE__ */ new Map();
755
+ /** 登记/更新一个会话 → 轨迹的映射 */
756
+ function registerSkiffSession(sessionId, trajectoryDirName, ccc) {
757
+ skiffSessions.set(sessionId, {
758
+ trajectoryDirName,
759
+ ccc
760
+ });
759
761
  }
760
- /** 域记录 → 本模块记录形状(同名字段,无需映射表;见 host/storage-domain.ts 头注) */
761
- function fromDomainRecord(rec) {
762
- return {
763
- dirName: rec.dirName,
764
- mdPath: rec.mdPath,
765
- ...rec.sessionId ? { sessionId: rec.sessionId } : {},
766
- action: rec.action ?? "activate",
767
- at: typeof rec.at === "number" ? rec.at : 0,
768
- ...rec.note ? { note: rec.note } : {},
769
- ...rec.supersededAt !== void 0 ? { supersededAt: rec.supersededAt } : {}
770
- };
762
+ /** 解绑(会话销毁 / 轨迹失效 ⇒ 换身份) */
763
+ function unregisterSkiffSession(sessionId) {
764
+ skiffSessions.delete(sessionId);
771
765
  }
772
- /** 本模块记录 → 域记录形状 */
773
- function toDomainRecord(rec) {
774
- return {
775
- dirName: rec.dirName,
776
- mdPath: rec.mdPath,
777
- ...rec.sessionId ? { sessionId: rec.sessionId } : {},
778
- action: rec.action,
779
- at: rec.at,
780
- ...rec.note ? { note: rec.note } : {},
781
- ...rec.supersededAt !== void 0 ? { supersededAt: rec.supersededAt } : {}
782
- };
766
+ /** 查会话承载的轨迹目录名(无 → null);`guards`/`system-prompt` 的同步热路径用 */
767
+ function skiffTrajectoryDirNameFor(sessionId) {
768
+ const b = skiffSessions.get(sessionId);
769
+ return b === void 0 || b.trajectoryDirName === "" ? null : b.trajectoryDirName;
783
770
  }
784
- /**
785
- * §0L **一次性迁移**:把某个 CCC 的旧 `.bindings.json` 全量灌入宿主存储域。
786
- *
787
- * 设计要点(R↓):
788
- * - **幂等**:只写域里**尚不存在**的键(`skip existing`)。⇒ 重复调用(每次启动都调)
789
- * 不会覆盖域里的新状态,也不会因并发而互相踩踏。
790
- * - **不删旧文件**:迁移只读不写旧表(owner「向前兼容」的第二道保险仍在)。
791
- * - **不丢绑定**:逐条迁移,单条失败不影响其余(返回计数供诊断)。
792
- * - **失败静默**:域写不可用 ⇒ 返回 `{migrated:0, skipped:0, failed:0}`,不抛错
793
- * (装载期调用,不能成为启动单点)。
794
- *
795
- * @param root CCC 根(`AGENT_SESSIONS/.bindings.json` 所在)
796
- * @returns 迁移计数(诊断用;`migrated` = 本次真正灌入的条数)
797
- */
798
- function migrateBindingsToDomain(root) {
799
- const out = {
800
- migrated: 0,
801
- skipped: 0,
802
- failed: 0
803
- };
804
- if (!domainStore?.available) return out;
805
- let file;
771
+ /** 查会话的完整绑定(轨迹 + CCC;无 → null)——追问延续时校验 (轨迹, CCC) 未串味 */
772
+ function skiffSessionInfo(sessionId) {
773
+ return skiffSessions.get(sessionId) ?? null;
774
+ }
775
+ //#endregion
776
+ //#region src/role/skiff/profile.ts
777
+ /**
778
+ * role/skiff/profile.ts — **skiff 的唯一判据**:目录下有 `skiff.json` 的认知轨迹(零 DSH 依赖,可独立单测)
779
+ *
780
+ * ## 🔴 owner 2026-10-19 重做令(逐字,本文件存在的全部理由)
781
+ *
782
+ * > 「skiff 复杂的约束和提示词体系都干掉,改为这样的实现:
783
+ * > **skiff 本身是认知轨迹的一个特殊情况,即目录下拥有 `skiff.json` 的认知轨迹自动是一个 skiff**
784
+ * > (json 里配置上个版本就有的各种约束),**会话跑在认知轨迹上且发现 `skiff.json`,则运行时实现各类约束**」
785
+ *
786
+ * ## 旧模型 → 新模型(**一条判据取代四条**)
787
+ *
788
+ * | 项 | 旧 | 新 |
789
+ * |---|---|---|
790
+ * | skiff 是什么 | `serenity.json` 的 `skiff.roles.<name>` 一条配置 | **轨迹目录里有 `skiff.json`** |
791
+ * | 怎么认出 skiff 会话 | dsh sessionId 前缀 `skiff-` | **它绑定的轨迹有 `skiff.json`** |
792
+ * | 工作台从哪来 | `ensureSkiffSession` 另建一个 SESSION | **就是该轨迹自己的 SESSION.md** |
793
+ * | 身份 | 角色名 | **轨迹编码**(`trajectory/core.ts` 的 `parseTrajectoryCode`) |
794
+ *
795
+ * ⇒ 收益(owner:「理解难度大幅下降」的实际内容):四个各自会漂的机制收敛为**一条文件存在性判据**。
796
+ * 删掉 `skiff.json` ⇒ 该轨迹**当场**退化为普通轨迹(工具面、提示词、keeper 全部恢复默认)。
797
+ *
798
+ * ## 归属(D23)
799
+ * **机制在 ACC**(文件叫什么、怎么读、怎么判、缺省值、如何施加约束);
800
+ * **内容在 CCC**(`skiff.json` 里填什么:模型、白名单、提示词路径)。
801
+ * ⇒ ACC 代码里**不出现任何具体角色名**,也不读 `serenity.json` 的 skiff 段(该段已退役)。
802
+ */
803
+ var profile_exports = /* @__PURE__ */ __exportAll({
804
+ SKIFF_PROFILE_FILENAME: () => SKIFF_PROFILE_FILENAME,
805
+ findDirNameByCode: () => findDirNameByCode,
806
+ isSkiffSession: () => isSkiffSession,
807
+ listSkiffProfiles: () => listSkiffProfiles,
808
+ readSkiffProfileAt: () => readSkiffProfileAt,
809
+ readSkiffProfileByCode: () => readSkiffProfileByCode,
810
+ skiffProfileForSession: () => skiffProfileForSession
811
+ });
812
+ /** `skiff.json` 的固定文件名(**判据就是它存在**——不是目录名前缀,也不是配置里的一行) */
813
+ const SKIFF_PROFILE_FILENAME = "skiff.json";
814
+ /**
815
+ * 已告警过的坏 `skiff.json`(进程级去重:同一坏文件不刷屏,但**绝不静默**)。
816
+ *
817
+ * 🔴 为什么必须响亮(同源先例 = `loadSerenityConfig` 的 F-07):一份坏 JSON 会让
818
+ * **工具白名单静默失效**——而白名单正是 skiff 的权限边界 ⇒ 静默 = 权限面悄悄放开或收死,
819
+ * 两种都查不到原因。
820
+ */
821
+ const warnedBrokenProfiles = /* @__PURE__ */ new Set();
822
+ /** 活跃轨迹目录名(跳过 `_`/`.` 前缀 —— 归档区与日志区不是轨迹;判据同 `readAllSessions`) */
823
+ function activeTrajectoryDirNames(root) {
806
824
  try {
807
- file = readBindingsFile(join(root, BINDINGS_REL_PATH));
825
+ return readdirSync(sessionsRoot(root), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
808
826
  } catch {
809
- return out;
810
- }
811
- for (const [id, raw] of Object.entries(file.sessions)) {
812
- const rec = asBoundRecord(raw);
813
- if (!rec) continue;
814
- if (domainStore.get(id)) {
815
- out.skipped += 1;
816
- continue;
817
- }
818
- try {
819
- domainStore.put(id, toDomainRecord(rec)).catch(() => {});
820
- out.migrated += 1;
821
- } catch {
822
- out.failed += 1;
823
- }
827
+ return [];
824
828
  }
825
- if (out.migrated > 0) migratedRoots.add(root);
826
- return out;
827
829
  }
828
- /** 已补过迁移的 CCC 根(进程内去重;跨进程各补一次是可接受的——迁移幂等) */
829
- const migratedRoots = /* @__PURE__ */ new Set();
830
830
  /**
831
- * **按 CCC 惰性补迁移**(§0L 修正,S142 2026-09-19)。
832
- *
833
- * 🔴 为什么需要它(实测缺陷,R↓):
834
- * 装载期那次迁移用的是 `process.cwd()` —— 而 dsh **服务进程的 cwd 是用户家目录**,
835
- * **不是 CCC 根**(实测 `acc-diag`:`进程 cwd: <家目录> | 进程 CCC: (无)`)。
836
- * ⇒ 迁移读的是一个**不存在的文件**,静默迁移 0 条
837
- * (症状:发布后 `~/.dsh/storages/serenity_bindings.json` **没出现**)。
831
+ * 读一个目录下的 `skiff.json`(**纯读,不施加约束**)。
838
832
  *
839
- * 现在改为:**任何一次能定位到 CCC 根的路径**都顺手补一次迁移(幂等 + 进程内去重)。
840
- * 这样无论服务从哪个 cwd 启动,只要那条 CCC 有会话活动,它的绑定就会被带上。
833
+ * @param trajectoryDir 轨迹目录绝对路径
834
+ * @param root CCC 根(仅用于 `systemPromptFile` 的逃逸拒绝)
835
+ * @returns 归一后的约束面;**无该文件 → `null`(= 这不是 skiff,常态,不是错误)**
841
836
  *
842
- * @param root CCC 根
837
+ * ⚠️ **形态容错**:数组元素非字符串 → 丢弃该项(不整份作废);`cognitiveTrajectory`
838
+ * 缺项 → 各自取 **true**(见 R↓);`model` 空白 → undefined(回退缺省)。
839
+ * 坏 JSON ⇒ 告警 + `null`(**按"不是 skiff"处理**:宁可让它退回普通轨迹,也不要带着
840
+ * 半份读不懂的权限面运行)。
843
841
  */
844
- function ensureBindingsMigrated(root) {
845
- if (!domainStore?.available) return;
846
- if (migratedRoots.has(root)) return;
842
+ function readSkiffProfileAt(trajectoryDir, root) {
843
+ const dirName = trajectoryDir.split("/").filter(Boolean).pop() ?? "";
844
+ const code = parseTrajectoryCode(dirName);
845
+ if (code === null) return null;
846
+ const file = join(trajectoryDir, SKIFF_PROFILE_FILENAME);
847
+ if (!existsSync(file)) return null;
848
+ let raw;
847
849
  try {
848
- migrateBindingsToDomain(root);
849
- } catch {}
850
- }
851
- /**
852
- * 域内读一条(同步)。域不可用 → `undefined`(调用方据此回落文件)。
853
- * @param sessionId dsh 会话 id
854
- */
855
- function readFromDomain(sessionId) {
856
- if (!domainStore?.available) return void 0;
857
- const rec = domainStore.get(sessionId);
858
- return rec ? fromDomainRecord(rec) : void 0;
859
- }
860
- /**
861
- * 域内取**某轨迹的全部绑定**(同步;已排除 superseded —— 与 `listBoundSessionIds` 同判据)。
862
- * 域不可用 → `null`(**注意与"空结果"区分**:null = 回落文件,[] = 域里确实没有)。
863
- */
864
- function listFromDomain(dirName) {
865
- if (!domainStore?.available) return null;
866
- const out = [];
867
- for (const [id, raw] of domainStore.entries()) {
868
- const rec = fromDomainRecord(raw);
869
- if (rec.dirName !== dirName) continue;
870
- out.push({
871
- id,
872
- rec
873
- });
874
- }
875
- return out;
876
- }
877
- function sessionHeader(session) {
878
- const header = session?.header;
879
- if (!header || typeof header !== "object") return null;
880
- return header;
881
- }
882
- /** 绑定文件绝对路径(由会话 cwd 上溯 .serenity 定位 CCC 根);无法定位返回 null */
883
- function bindingsPathFor(session) {
884
- const header = sessionHeader(session);
885
- if (typeof header?.cwd !== "string") return null;
886
- const root = cccRootForCwd(header.cwd);
887
- if (!root) return null;
888
- ensureBindingsMigrated(root);
889
- return join(root, BINDINGS_REL_PATH);
890
- }
891
- function readBindingsFile(path) {
892
- if (!existsSync(path)) return {
893
- version: BINDINGS_VERSION,
894
- sessions: {}
895
- };
896
- try {
897
- const sessions = JSON.parse(readFileSync(path, "utf-8")).sessions;
898
- if (!sessions || typeof sessions !== "object") return {
899
- version: BINDINGS_VERSION,
900
- sessions: {}
901
- };
902
- return {
903
- version: BINDINGS_VERSION,
904
- sessions
905
- };
906
- } catch {
907
- return {
908
- version: BINDINGS_VERSION,
909
- sessions: {}
910
- };
911
- }
912
- }
913
- /** 原子写:同目录 tmp + rename(避免半写文件被后续读取) */
914
- function writeBindingsFile(path, data) {
915
- mkdirSync(dirname(path), { recursive: true });
916
- const tmp = `${path}.tmp`;
917
- writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
918
- renameSync(tmp, path);
919
- }
920
- /** 记录形状校验(容忍手改/旧数据) */
921
- function asBoundRecord(value) {
922
- const r = value;
923
- if (!r || typeof r.dirName !== "string" || typeof r.mdPath !== "string") return null;
924
- return r;
925
- }
926
- /** 旧形态:从会话日志扫最后一条 `serenity/bound`(兼容 v1.30.5 及更早写入的绑定) */
927
- function readLegacyBoundFromEvents(session) {
928
- const events = sessionEvents(session);
929
- for (let i = events.length - 1; i >= 0; i--) {
930
- const ev = events[i];
931
- if (!ev || ev.type !== SESSION_BOUND_EVENT) continue;
932
- const rec = asBoundRecord(ev.data);
933
- if (rec) return rec;
850
+ raw = JSON.parse(readUtf8(file));
851
+ } catch (err) {
852
+ if (!warnedBrokenProfiles.has(file)) {
853
+ warnedBrokenProfiles.add(file);
854
+ console.warn(`[serenity-hooks] ✗ ${SKIFF_PROFILE_FILENAME} 解析失败 ⇒ 该轨迹**按普通轨迹处理**(工具白名单与提示词约束全部不施加)。请修 JSON: ${file} — ${String(err?.message ?? err)}`);
855
+ }
856
+ return null;
934
857
  }
935
- return null;
936
- }
937
- /**
938
- * 读取会话当前绑定(权威,latest-wins)。
939
- *
940
- * 读取顺序(§0L,S142 2026-09-19):
941
- * 1. **宿主存储域**(域可用时的权威来源);
942
- * 2. **旧文件** `.bindings.json`(域不可用 ⇒ 零回归回落;域可用但该会话未迁移 ⇒ 兜底);
943
- * 3. **旧事件形态** `serenity/bound`(v1.30.5 及更早的存量)。
944
- * 无绑定返回 null。
945
- */
946
- function readLastBound(session) {
947
- const id = sessionHeader(session)?.id;
948
- if (typeof id === "string") {
949
- const fromDomain = readFromDomain(id);
950
- if (fromDomain) return fromDomain;
858
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
859
+ if (!warnedBrokenProfiles.has(file)) {
860
+ warnedBrokenProfiles.add(file);
861
+ console.warn(`[serenity-hooks] ✗ ${SKIFF_PROFILE_FILENAME} 顶层不是对象 ⇒ 按普通轨迹处理: ${file}`);
862
+ }
863
+ return null;
951
864
  }
952
- const path = bindingsPathFor(session);
953
- if (path && typeof id === "string") {
954
- const rec = asBoundRecord(readBindingsFile(path).sessions[id]);
955
- if (rec) return rec;
865
+ const obj = raw;
866
+ const strList = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.trim()) : [];
867
+ /** 布尔缺省 **true**(R↓ 见接口:工作台就是这条轨迹自己的身体) */
868
+ const boolOrTrue = (v) => typeof v === "boolean" ? v : true;
869
+ const ct = obj.cognitiveTrajectory && typeof obj.cognitiveTrajectory === "object" ? obj.cognitiveTrajectory : {};
870
+ const model = typeof obj.model === "string" && obj.model.trim() !== "" ? obj.model.trim() : void 0;
871
+ const systemPrompt = typeof obj.systemPrompt === "string" ? obj.systemPrompt : void 0;
872
+ const rawFile = typeof obj.systemPromptFile === "string" ? obj.systemPromptFile.trim() : "";
873
+ let systemPromptFile;
874
+ if (rawFile !== "") try {
875
+ resolveInside(root, rawFile);
876
+ systemPromptFile = rawFile;
877
+ } catch (err) {
878
+ console.warn(`[serenity-hooks] ✗ ${SKIFF_PROFILE_FILENAME} 的 systemPromptFile 越界,已忽略(其余约束仍生效): ${file} — ${String(err?.message ?? err)}`);
956
879
  }
957
- return readLegacyBoundFromEvents(session);
958
- }
959
- /** 目录名 → 轨迹标签(`--S###--` 命中取 `S###`;未命中原样返回——编码无关 U4) */
960
- function cognitiveTrajectoryLabel(dirName) {
961
- const m = dirName.match(/--S(\d{3,})--/);
962
- return m ? `S${m[1]}` : dirName;
880
+ return {
881
+ dirName,
882
+ code,
883
+ path: file,
884
+ ...model !== void 0 ? { model } : {},
885
+ tools: strList(obj.tools),
886
+ msms: strList(obj.msms),
887
+ cognitiveTrajectory: {
888
+ session: boolOrTrue(ct.session),
889
+ keeper: boolOrTrue(ct.keeper),
890
+ rebuild: boolOrTrue(ct.rebuild)
891
+ },
892
+ ...systemPrompt !== void 0 ? { systemPrompt } : {},
893
+ ...systemPromptFile !== void 0 ? { systemPromptFile } : {}
894
+ };
963
895
  }
964
896
  /**
965
- * **调用方自身**所属 cognitive trajectory 的标签(审计字段 `createdBy` 的唯一正解)。
966
- *
967
- * 三级来源,逐级回退(R↓:为什么不用单一来源):
968
- * 1. `readLastBound(session)` —— **持久 bound**,权威(重启存活、不受别的会话激活污染)
969
- * 2. `getActiveSessionInfo(scope)` —— 本 dsh 会话的**内存**激活(按 scope 隔离)
970
- * 3. `getLastActiveSessionInfo()` —— **全局**最近激活指针(无绑定的会话兼容回退)
971
- *
972
- * ⚠️ 为何单列成本函数(S142 §30.12.1 实测缺陷):唤醒注册表的 `createdBy` 原先直接取第 3 级
973
- * 全局指针,而它记录的是"**进程内最近一次被激活的 cognitive trajectory**"——多会话并发时必然张冠李戴
974
- * (实测:S142 登记唤醒被写成 `by=S060`)。`createdBy` 是 D60「以可审计替代入口栅」的**唯一凭证**,
975
- * 失真即替代控制失效 ⇒ 必须以"调用方自己是谁"为准,全局指针只作最后兜底。
897
+ * 按**轨迹编码**解析 skiff 约束面(`list`/`admin`/调试页/路由解析用)。
976
898
  *
977
- * @param session 调用方 dsh 会话对象(需 `header.id` + `header.cwd`)
978
- * @param scope 调用方 dsh 会话 id(取不到传空串)
979
- * @returns 轨迹标签(`S###` 或完整目录名);三级全落空 → 空串(渲染为 `?`)
899
+ * @returns 约束面;轨迹不存在 / 无 `skiff.json` → `null`
980
900
  */
981
- function resolveSessionCognitiveTrajectoryLabel(session, scope) {
982
- const bound = session ? readLastBound(session) : null;
983
- if (bound?.dirName) return cognitiveTrajectoryLabel(bound.dirName);
984
- const scoped = scope !== "" ? getActiveSessionInfo(scope) : null;
985
- if (scoped) return scoped.sessionId;
986
- return getLastActiveSessionInfo()?.sessionId ?? "";
901
+ function readSkiffProfileByCode(root, code) {
902
+ const dirName = findDirNameByCode(root, code);
903
+ if (dirName === null) return null;
904
+ return readSkiffProfileAt(join(sessionsRoot(root), dirName), root);
987
905
  }
988
- /**
989
- * 反向查:绑定到某 cognitive trajectory 目录名的 dsh 会话 id(**按绑定时间倒序,最新在前**)。
990
- *
991
- * 用途(wake-scheduler 冷唤醒):.bindings.json 以 **sessionId 为键**,而唤醒条目的
992
- * target 是**目录名**——需要值侧扫描做反向索引。冷会话被唤醒时必须先知道"哪个 dsh
993
- * 会话承载这条轨迹",这是唯一的权威来源(标题只作回退,见 v1.29.2 R2)。
994
- *
995
- * @param root CCC 根
996
- * @param dirName AGENT_SESSIONS 目录名(完整名,如 `2026-09-01--S151--x--auto`)
997
- * @returns 会话 id 列表(最新绑定在前);无绑定 → `[]`
998
- */
999
- function listBoundSessionIds(root, dirName) {
1000
- const hits = [];
1001
- const fromDomain = listFromDomain(dirName);
1002
- if (fromDomain !== null) {
1003
- for (const { id, rec } of fromDomain) {
1004
- if (rec.supersededAt !== void 0) continue;
1005
- hits.push({
1006
- id,
1007
- at: typeof rec.at === "number" ? rec.at : 0
1008
- });
1009
- }
1010
- if (hits.length > 0) return hits.sort((a, b) => b.at - a.at).map((h) => h.id);
1011
- }
1012
- const file = readBindingsFile(join(root, BINDINGS_REL_PATH));
1013
- for (const [id, raw] of Object.entries(file.sessions)) {
1014
- const rec = asBoundRecord(raw);
1015
- if (!rec || rec.dirName !== dirName) continue;
1016
- if (rec.supersededAt !== void 0) continue;
1017
- if (hits.some((h) => h.id === id)) continue;
1018
- hits.push({
1019
- id,
1020
- at: typeof rec.at === "number" ? rec.at : 0
1021
- });
906
+ /** 扫全部活跃轨迹,返回全部 skiff。**顺序按目录名**(确定性,便于对读)。 */
907
+ function listSkiffProfiles(root) {
908
+ const out = [];
909
+ for (const name of activeTrajectoryDirNames(root)) {
910
+ const p = readSkiffProfileAt(join(sessionsRoot(root), name), root);
911
+ if (p) out.push(p);
1022
912
  }
1023
- return hits.sort((a, b) => b.at - a.at).map((h) => h.id);
913
+ return out;
1024
914
  }
1025
915
  /**
1026
- * 🔴 **「只留一条」的唯一判据**(2026-09-21 收进单一实现;此前同一判据在域侧/文件侧各写一遍)。
916
+ * 编码 → 轨迹目录名(活跃轨迹内)。找不到 → `null`。
1027
917
  *
1028
- * 判据:同 `dirName` ∧ **不是** `keepIds` 里的 ∧ **尚未** `supersededAt`。
1029
- * 域侧与文件侧**同判据**——历史缺陷(2026-09-19)正是"只在一侧收集 ⇒ 返回值恒空",
1030
- * 见 {@link supersedeOtherBindings} 的注释。
1031
- *
1032
- * @param fileSessions 旧文件表的记录集
1033
- * @param domainEntries 域侧条目(域不可用时传 `undefined` = 不参与判定)
918
+ * ⚠️ 与 `findSession` 的分工:这里**只按编码精确**匹配(不做模糊),
919
+ * 因为它的调用方(路由 / 约束施加)要的是**确定性**,模糊匹配会把"手抖打错的名字"
920
+ * 静默投到一条相近的轨迹上——那正是最难查的一类串味。
1034
921
  */
1035
- function collectSupersedeTargets(dirName, keepIds, fileSessions, domainEntries) {
1036
- const toMark = /* @__PURE__ */ new Set();
1037
- if (domainEntries) for (const [id, raw] of domainEntries) {
1038
- const rec = fromDomainRecord(raw);
1039
- if (rec.dirName !== dirName) continue;
1040
- if (keepIds.has(id) || rec.supersededAt !== void 0) continue;
1041
- toMark.add(id);
1042
- }
1043
- for (const [id, raw] of Object.entries(fileSessions)) {
1044
- const rec = asBoundRecord(raw);
1045
- if (!rec || rec.dirName !== dirName) continue;
1046
- if (keepIds.has(id) || rec.supersededAt !== void 0) continue;
1047
- toMark.add(id);
1048
- }
1049
- return toMark;
922
+ function findDirNameByCode(root, code) {
923
+ for (const name of activeTrajectoryDirNames(root)) if (parseTrajectoryCode(name) === code) return name;
924
+ return null;
1050
925
  }
1051
926
  /**
1052
- * **(c) 绑定一致性兜底**(S142 §0y,所有者 2026-09-18 裁「c 也算个兜底」):
1053
- * 删除指向**已不存在**的 dsh 会话的绑定记录。
927
+ * **会话级**判据(J2):这个 dsh 会话是不是 skiff 会话 —— 唯一入口。
1054
928
  *
1055
- * 为什么需要:`.bindings.json` **只增不清**,而 DSH 会话目录会被
1056
- * `session-cleanup`(本仓既有机制)物理删除 ⇒ 表里必然残留**悬空 id**。
1057
- * 悬空记录本身无害,但它会**污染反向索引**(`listBoundSessionIds` 是唤醒选人的唯一入口),
1058
- * 让"这条轨迹到底还有没有可用会话"变得不可判。
929
+ * 判据 = **它绑定的轨迹有 `skiff.json`**(不是 sessionId 前缀,也不是配置里有没有角色)。
1059
930
  *
1060
- * 🔴 **安全性由调用方的判据负责**(本函数是纯函数,不认识文件系统):
1061
- * `isMissing` 必须是 **fail-closed** 的(读不到 ⇒ 返回 false = "不当作缺失"),
1062
- * 见 `hasSessionLogById`。判据写错会**清掉整张表**,故此处刻意不提供默认实现。
931
+ * ## 为什么"绑定的轨迹"从两个来源取(R↓,一个失败模式)
932
+ * ① **持久绑定**(`readLastBound`,宿主存储域/`.bindings.json`)—— 权威,跨重启成立;
933
+ * ② **内存注册表**(`skiff/registry`)—— 覆盖绑定的**窗口期**(轨迹刚定、bound 还没落盘)。
934
+ * ⇒ 只取 ① 会在建会话的第一轮判成"不是 skiff"(工具面不收窄,模型看见不该有的工具);
935
+ * 只取 ② 会在进程重启后全判成"不是"(skiff 失去约束,**权限面静默放开**)。
936
+ * ⇒ **两来源并取**,都不中才 `null`。
1063
937
  *
1064
938
  * @param root CCC 根
1065
- * @param isMissing 判据:该会话 id 是否**确认**已不存在(false = 保留)
1066
- * @returns 被删除的会话 id(升序稳定;未删任何一条 → `[]`)
939
+ * @param sessionId dsh 会话 id
940
+ * @param boundDirName 调用方已解析的绑定目录名(可选;给了就省一次读绑定)
941
+ * @returns 约束面;不是 skiff 会话 → `null`
1067
942
  */
1068
- function pruneMissingBindings(root, isMissing) {
1069
- const path = join(root, BINDINGS_REL_PATH);
1070
- const file = readBindingsFile(path);
1071
- const toRemove = /* @__PURE__ */ new Set();
1072
- const judge = (id) => {
1073
- try {
1074
- return isMissing(id);
1075
- } catch {
1076
- return false;
1077
- }
1078
- };
1079
- if (domainStore?.available) {
1080
- for (const [id] of domainStore.entries()) if (judge(id)) toRemove.add(id);
1081
- }
1082
- for (const id of Object.keys(file.sessions)) if (judge(id)) toRemove.add(id);
1083
- if (toRemove.size === 0) return [];
1084
- if (domainStore?.available) for (const id of toRemove) domainStore.delete(id).catch(() => {});
1085
- for (const id of toRemove) delete file.sessions[id];
1086
- try {
1087
- writeBindingsFile(path, file);
1088
- } catch {}
1089
- return [...toRemove].sort();
943
+ function skiffProfileForSession(root, sessionId, boundDirName) {
944
+ const dirName = boundDirName ?? registeredDirName(sessionId);
945
+ if (dirName === null || dirName === "") return null;
946
+ return readSkiffProfileAt(join(sessionsRoot(root), dirName), root);
947
+ }
948
+ /** 内存登记的轨迹目录名(注册表单一真相源在 `./registry.ts`;此处只做一次转发) */
949
+ function registeredDirName(sessionId) {
950
+ if (typeof sessionId !== "string" || sessionId === "") return null;
951
+ return skiffTrajectoryDirNameFor(sessionId);
1090
952
  }
1091
953
  /**
1092
- * 写入绑定(latest-wins 覆盖该会话记录)。
1093
- *
1094
- * 绑定持久化尽力而为:无法定位 CCC 根 / 无会话 id / 写盘失败 → 返回 false,不阻断主流程。
954
+ * 该 dsh 会话**是不是** skiff 会话(布尔形态的 `skiffProfileForSession`)。
1095
955
  *
1096
- * 🔴 **同时是「只留一条」(D69)的唯一写入口**(2026-09-21 起):每写一条,就在**同一次 RMW** 里
1097
- * 把同轨迹的**其它**条标 `supersededAt`(自己除外)。⇒ 任何调用路径(`create`/`activate`/`switch`/
1098
- * `rebuild`/`reconcile`)**都自动满足"只留一条"**,调用方**不需要**(也**不应当**)自己算 selfId。
1099
- *
1100
- * @param session 目标 dsh 会话(需 `header.id` + `header.cwd` 可定位 CCC 根)
1101
- * @param action 绑定动作
1102
- * @param rec 绑定记录(dirName + mdPath 必填;sessionId 可选展示码)
956
+ * 存在的理由:调用点很多只要一个"是/不是"(seams 的旁路判定),
957
+ * 让它们各自写 `!== null` 会把判据散开;**判据只能有一个入口**。
1103
958
  */
1104
- function appendBound(session, action, rec) {
1105
- const path = bindingsPathFor(session);
1106
- const id = sessionHeader(session)?.id;
1107
- if (typeof id !== "string") return false;
1108
- const record = {
1109
- dirName: rec.dirName,
1110
- mdPath: rec.mdPath,
1111
- ...rec.sessionId ? { sessionId: rec.sessionId } : {},
1112
- action,
1113
- at: Date.now(),
1114
- ...rec.note ? { note: rec.note } : {}
1115
- };
1116
- if (domainStore?.available) domainStore.put(id, toDomainRecord(record)).catch(() => {});
1117
- if (!path) return domainStore?.available === true;
1118
- try {
1119
- const file = readBindingsFile(path);
1120
- file.sessions[id] = record;
1121
- const targets = collectSupersedeTargets(rec.dirName, /* @__PURE__ */ new Set([id]), file.sessions, domainStore?.available ? domainStore.entries() : void 0);
1122
- for (const other of targets) {
1123
- const otherRec = asBoundRecord(file.sessions[other]);
1124
- if (!otherRec) continue;
1125
- otherRec.supersededAt = record.at;
1126
- file.sessions[other] = otherRec;
1127
- }
1128
- writeBindingsFile(path, file);
1129
- if (domainStore?.available) for (const other of targets) {
1130
- const raw = domainStore.get(other);
1131
- if (!raw) continue;
1132
- domainStore.put(other, toDomainRecord({
1133
- ...fromDomainRecord(raw),
1134
- supersededAt: record.at
1135
- })).catch(() => {});
1136
- }
1137
- return true;
1138
- } catch {
1139
- return domainStore?.available === true;
1140
- }
959
+ function isSkiffSession(root, sessionId) {
960
+ return skiffProfileForSession(root, sessionId) !== null;
1141
961
  }
1142
962
  //#endregion
1143
- //#region src/message-source.ts
1144
- /** ACC 注入消息的 kind —— 唤醒 / 打回 / keeper 提醒 / 锚定轮 / rebuild 交接等一律同值。 */
1145
- const ACC_MESSAGE_KIND = "plugin:dsh-serenity-hooks";
1146
- /** ACC 注入消息的统一来源值(各缝共用;需要 `form` 时按 `ContextFormed` 另加字段)。 */
1147
- const PLUGIN_SOURCE = { kind: ACC_MESSAGE_KIND };
1148
- //#endregion
1149
- export { summarize as A, readTrajectoryUid as C, setActiveSessionInfo as D, sessionsRoot as E, localIdStamp as F, isoLocal as M, localFileStamp as N, setTrajectoryUid as O, localHuman as P, readActiveSessionMd as S, sessionEvents as T, getActiveSessionInfo as _, pruneMissingBindings as a, namingTitleFor as b, setBindingStore as c, UID_FILENAME as d, archiveSession as f, findSession as g, extractSessionMdPathFromText as h, listBoundSessionIds as i, useSession as j, showSession as k, COGNITIVE_TRAJECTORY_ACTIONS as l, createSession as m, PLUGIN_SOURCE as n, readLastBound as o, clearActiveSessionInfo as p, appendBound as r, resolveSessionCognitiveTrajectoryLabel as s, ACC_MESSAGE_KIND as t, DEFAULT_SESSION_SCOPE as u, listActiveTrajectories as v, resolveSessionByTitle as w, parseSessionContextFromEvents as x, listSessions as y };
963
+ export { sessionEvents as A, listSessions as C, readActiveSessionMd as D, parseTrajectoryCode as E, useSession as F, isoLocal as I, localFileStamp as L, setActiveSessionInfo as M, showSession as N, readTrajectoryUid as O, summarize as P, localHuman as R, listActiveTrajectories as S, parseSessionContextFromEvents as T, createSession as _, readSkiffProfileAt as a, getActiveSessionInfo as b, registerSkiffSession as c, unregisterSkiffSession as d, COGNITIVE_TRAJECTORY_ACTIONS as f, clearActiveSessionInfo as g, archiveSession as h, profile_exports as i, sessionsRoot as j, resolveSessionByTitle as k, skiffSessionInfo as l, UID_FILENAME as m, isSkiffSession as n, readSkiffProfileByCode as o, DEFAULT_SESSION_SCOPE as p, listSkiffProfiles as r, skiffProfileForSession as s, SKIFF_PROFILE_FILENAME as t, skiffTrajectoryDirNameFor as u, extractSessionMdPathFromText as v, namingTitleFor as w, getLastActiveSessionInfo as x, findSession as y, localIdStamp as z };