@shgroup/dsh-serenity-hooks 1.39.3 → 1.40.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.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.39.3",
3
+ "version": "1.40.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag;后两个按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2(deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -0,0 +1,115 @@
1
+ /**
2
+ * host/storage-domain.ts — 宿主存储域上的「会话 ↔ 轨迹」绑定表(§0L,S142 2026-09-19)
3
+ *
4
+ * 为什么存在(R↓):绑定的载体原本是 CCC 内的 `AGENT_SESSIONS/.bindings.json`。
5
+ * owner 裁定(「放 CCC 不合适」)⇒ 改存**宿主自己的存储域**
6
+ * (`~/.dsh/storages/`,与宿主 `workspace.json` 同处、同类结构、同一套校验)。
7
+ *
8
+ * 能力与边界:
9
+ * - `openBindingDomain(ctx)`:从插件 ctx 取 `storageDomain`(**经 `hostService` = `ctx.get`**,
10
+ * **不是**属性直读——dsp 的 `inject` 不含它,属性直读在真实 cordis 下抛
11
+ * `cannot get property "storageDomain" without inject`,同 v1.31.4 的 subagents 陷阱);
12
+ * 开域成功返回句柄,任何环节失败返回 `null`(**本模块永不抛错**:宿主对插件 apply 抛错
13
+ * = 整个 dsh 启动失败,见 `host/access.ts` 的同类契约)。
14
+ * - 句柄的**读是同步的**(`get` / `entries` / `keys` / `size` 走内存),**写才是异步的**
15
+ * (`put` / `delete` 等落盘后 resolve)——**这是 §0L 能"向前兼容"的技术前提**:
16
+ * 既有 10 个调用点全是同步签名,故调用方拿到句柄后读法完全不变。
17
+ *
18
+ * 域名约束(实测,`tests/host/storage-domain.test.ts` 钉住):
19
+ * `UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/` ⇒ **`serenity_bindings`(下划线)**;
20
+ * 连字符形如 `serenity-bindings` **非法**(会在 `defineDomain` 模块加载期抛错)。
21
+ *
22
+ * 记录形状:与旧 `.bindings.json` **同名字段**(`dirName` / `mdPath` / `sessionId` / `action`
23
+ * / `at` / `note` / `supersededAt`)——同形可显著降低迁移风险,也让"双读兜底"易于对账。
24
+ * 硬锚仍是 **`dirName`**(完整目录名,U4:绝不解析编号前缀)。
25
+ */
26
+ /** 域名称(下划线;连字符非法——见文件头) */
27
+ export declare const BINDING_DOMAIN_NAME = "serenity_bindings";
28
+ /** 表名 */
29
+ export declare const BINDING_TABLE_NAME = "bindings";
30
+ /** 域格式版本(`DomainSpec.version`;非负整数) */
31
+ export declare const BINDING_DOMAIN_VERSION = 1;
32
+ /**
33
+ * 一条绑定记录。字段与旧 `.bindings.json` 的 `SessionBoundRecord` **同名同义**
34
+ * (刻意不重命名:迁移对账不需要映射表,降低出错面)。
35
+ */
36
+ export interface BindingRecord {
37
+ /** 🔴 硬锚:完整 `AGENT_SESSIONS` 目录名(不解析编号格式,U4) */
38
+ dirName: string;
39
+ /** `SESSION.md` 绝对路径(展示/定位用;**不作锚**——绝对路径绑死机器) */
40
+ mdPath: string;
41
+ /** 展示码(`S###` 或任意 CCC 自定义格式;**仅展示**,绝不用于识别) */
42
+ sessionId?: string;
43
+ action: string;
44
+ at: number;
45
+ note?: string;
46
+ /** 已被取代(D69:同轨迹多会话只留一条;记录保留、退出"当前") */
47
+ supersededAt?: number;
48
+ }
49
+ /** 域内的表句柄(宿主 `KvTable` 的**结构子集**——只取本模块用到的成员,便于替身注入) */
50
+ export interface BindingTableLike {
51
+ /** 同步读一条(内存) */
52
+ get(key: string): BindingRecord | undefined;
53
+ /** 同步快照遍历 */
54
+ entries(): IterableIterator<[string, BindingRecord]>;
55
+ /** 当前条数 */
56
+ readonly size: number;
57
+ /** 异步落盘写一条 */
58
+ put(key: string, value: BindingRecord): Promise<void>;
59
+ /** 异步落盘删一条 */
60
+ delete(key: string): Promise<boolean>;
61
+ }
62
+ /** 域句柄(宿主 `Domain` 的结构子集) */
63
+ export interface BindingDomainLike {
64
+ /** 取表句柄(重复调用返回同一实例) */
65
+ table(name: string): BindingTableLike;
66
+ }
67
+ /** `storageDomain` 设施的结构子集(宿主 `DomainFacility`) */
68
+ export interface DomainFacilityLike {
69
+ open(spec: unknown): Promise<BindingDomainLike>;
70
+ }
71
+ /**
72
+ * 构造域规格(宿主 `defineDomain` 的入参形状)。
73
+ *
74
+ * 为什么不引 `@deepseek-ai/dsh-storage-domain` 的 `defineDomain`:
75
+ * 它是**宿主 peer 依赖**(本仓 peer-only 打包,不装它)⇒ 直接 import 会让插件在
76
+ * 未装该包的运行时**模块加载即失败**。域规格是**纯数据**,手写等价对象即可
77
+ * (`tests/host/storage-domain.test.ts` 用真实 `defineDomain` 验证本对象被接受)。
78
+ */
79
+ export declare function bindingDomainSpec(): unknown;
80
+ /**
81
+ * 从插件 ctx 打开绑定域。**永不抛错**;不可用一律返回 `null`(调用方决定降级)。
82
+ *
83
+ * 失败面(全部返回 `null`,调用方回落旧 `.bindings.json`):
84
+ * - 宿主未提供 `storageDomain`(未装载 storage-domain 插件 / 老宿主);
85
+ * - `open` 抛错(域名非法 / 后端缺失 `backend-not-found` / 版本不符 / 记录损坏);
86
+ * - 拿到的服务形状不对(无 `open` 函数)。
87
+ *
88
+ * @param ctx 插件上下文(真实 cordis Context;测试可传结构化替身)
89
+ * @returns 域句柄,或 `null`
90
+ */
91
+ export declare function openBindingDomain(ctx: unknown): Promise<BindingDomainLike | null>;
92
+ /**
93
+ * 绑定表的**同步读**封装(句柄缺失时全部返回空,语义 = "域里没有")。
94
+ *
95
+ * 为什么单独包一层(而不是让调用方直接拿 table):
96
+ * ① 句柄可能为 `null`(域不可用)⇒ 让 `null` 检查集中在一处;
97
+ * ② 读侧要统一做形状容忍(`asBindingRecord`)。
98
+ */
99
+ export declare class BindingStore {
100
+ private readonly domain;
101
+ constructor(domain: BindingDomainLike | null);
102
+ /** 域是否可用(`false` ⇒ 调用方应回落旧文件) */
103
+ get available(): boolean;
104
+ private table;
105
+ /** 同步读一条(域不可用 → `undefined`) */
106
+ get(sessionId: string): BindingRecord | undefined;
107
+ /** 同步遍历全部 `[会话id, 记录]`(域不可用 → 空) */
108
+ entries(): Array<[string, BindingRecord]>;
109
+ /** 异步写一条(域不可用 / 落盘失败 → `false`;**不抛**) */
110
+ put(sessionId: string, rec: BindingRecord): Promise<boolean>;
111
+ /** 异步删一条(域不可用 / 失败 → `false`;**不抛**) */
112
+ delete(sessionId: string): Promise<boolean>;
113
+ }
114
+ /** 由域句柄造 store(`null` 句柄 → 不可用 store,读全空、写全 false) */
115
+ export declare function bindingStore(domain: BindingDomainLike | null): BindingStore;
package/lib/index.js CHANGED
@@ -2,12 +2,12 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { a as hostSettings, c as hostWebServer, i as hostSessions, n as hostInjected, o as hostSubagents, r as hostService, s as hostWeb, t as hostAgents } from "./access-fiehjxV6.js";
3
3
  import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$1, f as readExclusiveTools, h as resolveInside, i as findGitRoot, l as pathInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readHandymanConfig, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-NlLr_sxy.js";
4
4
  import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCwd, t as NO_CCC_FROM_AGENT_CWD } from "./ccc-roots-Bd_SjEs7.js";
5
- import { C as useSession, S as summarize, T as localFileStamp, _ as resolveSessionByTitle, a as resolveSessionTrajectoryLabel, b as setActiveSessionInfo, c as TRAJECTORY_ACTIONS, d as extractSessionMdPathFromText, f as findSession, g as readActiveSessionMd, h as parseSessionContextFromEvents, i as readLastBound, l as clearActiveSessionInfo, m as listSessions, o as supersedeOtherBindings, p as getActiveSessionInfo, r as pruneMissingBindings, s as DEFAULT_SESSION_SCOPE, t as appendBound, u as createSession, v as sessionEvents$1, w as isoLocal, x as showSession, y as sessionsRoot } from "./trajectory-bound-B9o_4CMF.js";
5
+ import { C as showSession, D as localFileStamp, E as isoLocal, S as setActiveSessionInfo, T as useSession, _ as parseSessionContextFromEvents, a as readLastBound, b as sessionEvents$1, c as supersedeOtherBindings, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as pruneMissingBindings, l as DEFAULT_SESSION_SCOPE, m as findSession, o as resolveSessionTrajectoryLabel, p as extractSessionMdPathFromText, r as migrateBindingsToDomain, s as setBindingStore, t as appendBound, u as TRAJECTORY_ACTIONS, v as readActiveSessionMd, w as summarize, x as sessionsRoot, y as resolveSessionByTitle } from "./trajectory-bound-CQTh86vq.js";
6
6
  import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-B2ajFypI.js";
7
7
  import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-CEHL5cek.js";
8
8
  import { a as unregisterSkiffSession$1, i as skiffSessionSnapshot$1, n as skiffRoleFor$1, r as skiffSessionInfo$1, t as registerSkiffSession$1 } from "./skiff-registry-FTjoWQTQ.js";
9
9
  import { r as sessionsRootDir, t as hasSessionLogById } from "./session-cleanup-BtkSmry8.js";
10
- import { a as WAKE_CATCH_UP_MS, i as registerDisposer, n as sendToTrajectory, o as addWake, r as wakeSchedulerState, s as listWakes, t as registerWakeScheduler } from "./wake-scheduler-CCpCk-7r.js";
10
+ import { a as WAKE_CATCH_UP_MS, i as registerDisposer, n as sendToTrajectory, o as addWake, r as wakeSchedulerState, s as listWakes, t as registerWakeScheduler } from "./wake-scheduler-UTh4t14k.js";
11
11
  import { a as FACE_PORTS, c as MECHANISM_PORTS, i as ACP_HTTP_PORT, l as SKIFF_DEBUG_PORT, n as registerSettingsSection, o as GATEWAY_PORT, s as MAIN_WEB_PORT, t as readSimpleSettings, u as WEIXIN_SEND_PORT } from "./settings-section-DMDUoQum.js";
12
12
  import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-CtFYm53S.js";
13
13
  import z from "@deepseek-ai/schemastery";
@@ -7520,6 +7520,17 @@ const HOST_SERVICES = [
7520
7520
  }],
7521
7521
  impact: "双端口网关无法换取 dsh cookie → 外部反代 401",
7522
7522
  required: false
7523
+ },
7524
+ {
7525
+ id: "storageDomain",
7526
+ name: "storageDomain",
7527
+ access: "lazy",
7528
+ members: [{
7529
+ name: "open",
7530
+ kind: "function"
7531
+ }],
7532
+ impact: "轨迹绑定无处可存 → §0L 整体不可用(须退回\"写会话日志\"备选方案)",
7533
+ required: false
7523
7534
  }
7524
7535
  ];
7525
7536
  /** dsp 被验证过的宿主版本范围(与 package.json peerDependencies 同源,单一真相源) */
@@ -14444,6 +14455,161 @@ function createAccDiagTool(ctx) {
14444
14455
  });
14445
14456
  }
14446
14457
  //#endregion
14458
+ //#region src/host/storage-domain.ts
14459
+ /**
14460
+ * host/storage-domain.ts — 宿主存储域上的「会话 ↔ 轨迹」绑定表(§0L,S142 2026-09-19)
14461
+ *
14462
+ * 为什么存在(R↓):绑定的载体原本是 CCC 内的 `AGENT_SESSIONS/.bindings.json`。
14463
+ * owner 裁定(「放 CCC 不合适」)⇒ 改存**宿主自己的存储域**
14464
+ * (`~/.dsh/storages/`,与宿主 `workspace.json` 同处、同类结构、同一套校验)。
14465
+ *
14466
+ * 能力与边界:
14467
+ * - `openBindingDomain(ctx)`:从插件 ctx 取 `storageDomain`(**经 `hostService` = `ctx.get`**,
14468
+ * **不是**属性直读——dsp 的 `inject` 不含它,属性直读在真实 cordis 下抛
14469
+ * `cannot get property "storageDomain" without inject`,同 v1.31.4 的 subagents 陷阱);
14470
+ * 开域成功返回句柄,任何环节失败返回 `null`(**本模块永不抛错**:宿主对插件 apply 抛错
14471
+ * = 整个 dsh 启动失败,见 `host/access.ts` 的同类契约)。
14472
+ * - 句柄的**读是同步的**(`get` / `entries` / `keys` / `size` 走内存),**写才是异步的**
14473
+ * (`put` / `delete` 等落盘后 resolve)——**这是 §0L 能"向前兼容"的技术前提**:
14474
+ * 既有 10 个调用点全是同步签名,故调用方拿到句柄后读法完全不变。
14475
+ *
14476
+ * 域名约束(实测,`tests/host/storage-domain.test.ts` 钉住):
14477
+ * `UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/` ⇒ **`serenity_bindings`(下划线)**;
14478
+ * 连字符形如 `serenity-bindings` **非法**(会在 `defineDomain` 模块加载期抛错)。
14479
+ *
14480
+ * 记录形状:与旧 `.bindings.json` **同名字段**(`dirName` / `mdPath` / `sessionId` / `action`
14481
+ * / `at` / `note` / `supersededAt`)——同形可显著降低迁移风险,也让"双读兜底"易于对账。
14482
+ * 硬锚仍是 **`dirName`**(完整目录名,U4:绝不解析编号前缀)。
14483
+ */
14484
+ /** 域名称(下划线;连字符非法——见文件头) */
14485
+ const BINDING_DOMAIN_NAME = "serenity_bindings";
14486
+ /** 表名 */
14487
+ const BINDING_TABLE_NAME = "bindings";
14488
+ const passthroughSchema = {
14489
+ parse: (v) => v,
14490
+ safeParse: (v) => ({
14491
+ success: true,
14492
+ data: v
14493
+ })
14494
+ };
14495
+ /**
14496
+ * 构造域规格(宿主 `defineDomain` 的入参形状)。
14497
+ *
14498
+ * 为什么不引 `@deepseek-ai/dsh-storage-domain` 的 `defineDomain`:
14499
+ * 它是**宿主 peer 依赖**(本仓 peer-only 打包,不装它)⇒ 直接 import 会让插件在
14500
+ * 未装该包的运行时**模块加载即失败**。域规格是**纯数据**,手写等价对象即可
14501
+ * (`tests/host/storage-domain.test.ts` 用真实 `defineDomain` 验证本对象被接受)。
14502
+ */
14503
+ function bindingDomainSpec() {
14504
+ return {
14505
+ name: BINDING_DOMAIN_NAME,
14506
+ version: 1,
14507
+ tables: { [BINDING_TABLE_NAME]: { valueSchema: passthroughSchema } }
14508
+ };
14509
+ }
14510
+ /** 读侧形状容忍(手改 / 旧数据 / 未来字段):只要求 `dirName` 是字符串 */
14511
+ function asBindingRecord(value) {
14512
+ const r = value;
14513
+ if (!r || typeof r !== "object" || typeof r.dirName !== "string") return null;
14514
+ return r;
14515
+ }
14516
+ /**
14517
+ * 从插件 ctx 打开绑定域。**永不抛错**;不可用一律返回 `null`(调用方决定降级)。
14518
+ *
14519
+ * 失败面(全部返回 `null`,调用方回落旧 `.bindings.json`):
14520
+ * - 宿主未提供 `storageDomain`(未装载 storage-domain 插件 / 老宿主);
14521
+ * - `open` 抛错(域名非法 / 后端缺失 `backend-not-found` / 版本不符 / 记录损坏);
14522
+ * - 拿到的服务形状不对(无 `open` 函数)。
14523
+ *
14524
+ * @param ctx 插件上下文(真实 cordis Context;测试可传结构化替身)
14525
+ * @returns 域句柄,或 `null`
14526
+ */
14527
+ async function openBindingDomain(ctx) {
14528
+ const facility = hostService(ctx, "storageDomain");
14529
+ if (!facility || typeof facility.open !== "function") return null;
14530
+ try {
14531
+ const domain = await facility.open(bindingDomainSpec());
14532
+ if (!domain || typeof domain.table !== "function") return null;
14533
+ const table = domain.table(BINDING_TABLE_NAME);
14534
+ if (!table || typeof table.get !== "function") return null;
14535
+ return domain;
14536
+ } catch {
14537
+ return null;
14538
+ }
14539
+ }
14540
+ /**
14541
+ * 绑定表的**同步读**封装(句柄缺失时全部返回空,语义 = "域里没有")。
14542
+ *
14543
+ * 为什么单独包一层(而不是让调用方直接拿 table):
14544
+ * ① 句柄可能为 `null`(域不可用)⇒ 让 `null` 检查集中在一处;
14545
+ * ② 读侧要统一做形状容忍(`asBindingRecord`)。
14546
+ */
14547
+ var BindingStore = class {
14548
+ domain;
14549
+ constructor(domain) {
14550
+ this.domain = domain;
14551
+ }
14552
+ /** 域是否可用(`false` ⇒ 调用方应回落旧文件) */
14553
+ get available() {
14554
+ return this.domain !== null;
14555
+ }
14556
+ table() {
14557
+ if (!this.domain) return null;
14558
+ try {
14559
+ return this.domain.table(BINDING_TABLE_NAME);
14560
+ } catch {
14561
+ return null;
14562
+ }
14563
+ }
14564
+ /** 同步读一条(域不可用 → `undefined`) */
14565
+ get(sessionId) {
14566
+ try {
14567
+ const rec = this.table()?.get(sessionId);
14568
+ return rec ? asBindingRecord(rec) ?? void 0 : void 0;
14569
+ } catch {
14570
+ return;
14571
+ }
14572
+ }
14573
+ /** 同步遍历全部 `[会话id, 记录]`(域不可用 → 空) */
14574
+ entries() {
14575
+ const out = [];
14576
+ try {
14577
+ const t = this.table();
14578
+ if (!t) return out;
14579
+ for (const [k, v] of t.entries()) {
14580
+ const rec = asBindingRecord(v);
14581
+ if (rec) out.push([k, rec]);
14582
+ }
14583
+ } catch {}
14584
+ return out;
14585
+ }
14586
+ /** 异步写一条(域不可用 / 落盘失败 → `false`;**不抛**) */
14587
+ async put(sessionId, rec) {
14588
+ try {
14589
+ const t = this.table();
14590
+ if (!t) return false;
14591
+ await t.put(sessionId, rec);
14592
+ return true;
14593
+ } catch {
14594
+ return false;
14595
+ }
14596
+ }
14597
+ /** 异步删一条(域不可用 / 失败 → `false`;**不抛**) */
14598
+ async delete(sessionId) {
14599
+ try {
14600
+ const t = this.table();
14601
+ if (!t) return false;
14602
+ return await t.delete(sessionId);
14603
+ } catch {
14604
+ return false;
14605
+ }
14606
+ }
14607
+ };
14608
+ /** 由域句柄造 store(`null` 句柄 → 不可用 store,读全空、写全 false) */
14609
+ function bindingStore(domain) {
14610
+ return new BindingStore(domain);
14611
+ }
14612
+ //#endregion
14447
14613
  //#region src/index.ts
14448
14614
  const name = "dsh-serenity-hooks";
14449
14615
  /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
@@ -14495,6 +14661,19 @@ function apply(ctx, config) {
14495
14661
  if (report !== null && report.issues.length > 0) console.warn(`[serenity-hooks] ${summarizeHostContract(report)}`);
14496
14662
  } catch {}
14497
14663
  registerImChannel(weixinChannel);
14664
+ (async () => {
14665
+ try {
14666
+ const domain = await openBindingDomain(ctx);
14667
+ setBindingStore(bindingStore(domain));
14668
+ if (domain) {
14669
+ const root = process.cwd();
14670
+ const stats = migrateBindingsToDomain(root);
14671
+ if (stats.migrated > 0) console.log(`[serenity-hooks] §0L 绑定已迁移入宿主存储域:${stats.migrated} 条(跳过 ${stats.skipped})`);
14672
+ } else console.log("[serenity-hooks] §0L 存储域不可用 → 绑定继续走 CCC 内 .bindings.json(向前兼容回落)");
14673
+ } catch {
14674
+ setBindingStore(null);
14675
+ }
14676
+ })();
14498
14677
  if (config.tools) {
14499
14678
  ctx.tools.register(ccFsTool);
14500
14679
  ctx.tools.register(createTrajectoryTool(ctx));
@@ -526,6 +526,107 @@ const SESSION_BOUND_EVENT = "serenity/bound";
526
526
  /** 绑定文件相对 CCC 根的路径 */
527
527
  const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
528
528
  const BINDINGS_VERSION = 1;
529
+ let domainStore = null;
530
+ /**
531
+ * 注入域句柄(装载期调用一次)。传 `null` = 域不可用 ⇒ 全部退回旧文件行为。
532
+ * 幂等:重复调用以后者为准(HMR / 重载场景)。
533
+ */
534
+ function setBindingStore(store) {
535
+ domainStore = store;
536
+ }
537
+ /** 域记录 → 本模块记录形状(同名字段,无需映射表;见 host/storage-domain.ts 头注) */
538
+ function fromDomainRecord(rec) {
539
+ return {
540
+ dirName: rec.dirName,
541
+ mdPath: rec.mdPath,
542
+ ...rec.sessionId ? { sessionId: rec.sessionId } : {},
543
+ action: rec.action ?? "activate",
544
+ at: typeof rec.at === "number" ? rec.at : 0,
545
+ ...rec.note ? { note: rec.note } : {},
546
+ ...rec.supersededAt !== void 0 ? { supersededAt: rec.supersededAt } : {}
547
+ };
548
+ }
549
+ /** 本模块记录 → 域记录形状 */
550
+ function toDomainRecord(rec) {
551
+ return {
552
+ dirName: rec.dirName,
553
+ mdPath: rec.mdPath,
554
+ ...rec.sessionId ? { sessionId: rec.sessionId } : {},
555
+ action: rec.action,
556
+ at: rec.at,
557
+ ...rec.note ? { note: rec.note } : {},
558
+ ...rec.supersededAt !== void 0 ? { supersededAt: rec.supersededAt } : {}
559
+ };
560
+ }
561
+ /**
562
+ * §0L **一次性迁移**:把某个 CCC 的旧 `.bindings.json` 全量灌入宿主存储域。
563
+ *
564
+ * 设计要点(R↓):
565
+ * - **幂等**:只写域里**尚不存在**的键(`skip existing`)。⇒ 重复调用(每次启动都调)
566
+ * 不会覆盖域里的新状态,也不会因并发而互相踩踏。
567
+ * - **不删旧文件**:迁移只读不写旧表(owner「向前兼容」的第二道保险仍在)。
568
+ * - **不丢绑定**:逐条迁移,单条失败不影响其余(返回计数供诊断)。
569
+ * - **失败静默**:域写不可用 ⇒ 返回 `{migrated:0, skipped:0, failed:0}`,不抛错
570
+ * (装载期调用,不能成为启动单点)。
571
+ *
572
+ * @param root CCC 根(`AGENT_SESSIONS/.bindings.json` 所在)
573
+ * @returns 迁移计数(诊断用;`migrated` = 本次真正灌入的条数)
574
+ */
575
+ function migrateBindingsToDomain(root) {
576
+ const out = {
577
+ migrated: 0,
578
+ skipped: 0,
579
+ failed: 0
580
+ };
581
+ if (!domainStore?.available) return out;
582
+ let file;
583
+ try {
584
+ file = readBindingsFile(join(root, BINDINGS_REL_PATH));
585
+ } catch {
586
+ return out;
587
+ }
588
+ for (const [id, raw] of Object.entries(file.sessions)) {
589
+ const rec = asBoundRecord(raw);
590
+ if (!rec) continue;
591
+ if (domainStore.get(id)) {
592
+ out.skipped += 1;
593
+ continue;
594
+ }
595
+ try {
596
+ domainStore.put(id, toDomainRecord(rec)).catch(() => {});
597
+ out.migrated += 1;
598
+ } catch {
599
+ out.failed += 1;
600
+ }
601
+ }
602
+ return out;
603
+ }
604
+ /**
605
+ * 域内读一条(同步)。域不可用 → `undefined`(调用方据此回落文件)。
606
+ * @param sessionId dsh 会话 id
607
+ */
608
+ function readFromDomain(sessionId) {
609
+ if (!domainStore?.available) return void 0;
610
+ const rec = domainStore.get(sessionId);
611
+ return rec ? fromDomainRecord(rec) : void 0;
612
+ }
613
+ /**
614
+ * 域内取**某轨迹的全部绑定**(同步;已排除 superseded —— 与 `listBoundSessionIds` 同判据)。
615
+ * 域不可用 → `null`(**注意与"空结果"区分**:null = 回落文件,[] = 域里确实没有)。
616
+ */
617
+ function listFromDomain(dirName) {
618
+ if (!domainStore?.available) return null;
619
+ const out = [];
620
+ for (const [id, raw] of domainStore.entries()) {
621
+ const rec = fromDomainRecord(raw);
622
+ if (rec.dirName !== dirName) continue;
623
+ out.push({
624
+ id,
625
+ rec
626
+ });
627
+ }
628
+ return out;
629
+ }
529
630
  function sessionHeader(session) {
530
631
  const header = session?.header;
531
632
  if (!header || typeof header !== "object") return null;
@@ -586,12 +687,21 @@ function readLegacyBoundFromEvents(session) {
586
687
  return null;
587
688
  }
588
689
  /**
589
- * 读取会话当前绑定(权威,latest-wins):文件记录优先,无则回落旧事件形态。
690
+ * 读取会话当前绑定(权威,latest-wins)。
691
+ *
692
+ * 读取顺序(§0L,S142 2026-09-19):
693
+ * 1. **宿主存储域**(域可用时的权威来源);
694
+ * 2. **旧文件** `.bindings.json`(域不可用 ⇒ 零回归回落;域可用但该会话未迁移 ⇒ 兜底);
695
+ * 3. **旧事件形态** `serenity/bound`(v1.30.5 及更早的存量)。
590
696
  * 无绑定返回 null。
591
697
  */
592
698
  function readLastBound(session) {
593
- const path = bindingsPathFor(session);
594
699
  const id = sessionHeader(session)?.id;
700
+ if (typeof id === "string") {
701
+ const fromDomain = readFromDomain(id);
702
+ if (fromDomain) return fromDomain;
703
+ }
704
+ const path = bindingsPathFor(session);
595
705
  if (path && typeof id === "string") {
596
706
  const rec = asBoundRecord(readBindingsFile(path).sessions[id]);
597
707
  if (rec) return rec;
@@ -639,12 +749,24 @@ function resolveSessionTrajectoryLabel(session, scope) {
639
749
  * @returns 会话 id 列表(最新绑定在前);无绑定 → `[]`
640
750
  */
641
751
  function listBoundSessionIds(root, dirName) {
642
- const file = readBindingsFile(join(root, BINDINGS_REL_PATH));
643
752
  const hits = [];
753
+ const fromDomain = listFromDomain(dirName);
754
+ if (fromDomain !== null) {
755
+ for (const { id, rec } of fromDomain) {
756
+ if (rec.supersededAt !== void 0) continue;
757
+ hits.push({
758
+ id,
759
+ at: typeof rec.at === "number" ? rec.at : 0
760
+ });
761
+ }
762
+ if (hits.length > 0) return hits.sort((a, b) => b.at - a.at).map((h) => h.id);
763
+ }
764
+ const file = readBindingsFile(join(root, BINDINGS_REL_PATH));
644
765
  for (const [id, raw] of Object.entries(file.sessions)) {
645
766
  const rec = asBoundRecord(raw);
646
767
  if (!rec || rec.dirName !== dirName) continue;
647
768
  if (rec.supersededAt !== void 0) continue;
769
+ if (hits.some((h) => h.id === id)) continue;
648
770
  hits.push({
649
771
  id,
650
772
  at: typeof rec.at === "number" ? rec.at : 0
@@ -670,23 +792,49 @@ function listBoundSessionIds(root, dirName) {
670
792
  function supersedeOtherBindings(root, dirName, keepIds) {
671
793
  const path = join(root, BINDINGS_REL_PATH);
672
794
  const file = readBindingsFile(path);
673
- const marked = [];
674
795
  const at = Date.now();
796
+ /**
797
+ * 需要退休的会话 id 集合。
798
+ *
799
+ * 🔴 必须取**域 ∪ 文件的并集**(实测缺陷,2026-09-19):
800
+ * 首版实现只在**文件循环**里 `marked.push(id)`,于是"记录只在域里"时
801
+ * 返回值恒为 `[]` —— 而调用方(`tools/trajectory.ts` 的 `use`)**靠这个返回值**决定
802
+ * 是否回报 `supersededBindings`,并且早先用 `marked.length === 0` 短路**跳过了写盘**。
803
+ * ⇒ 症状是"D69 只留一条**静默不生效**"(记录没被标记,返回也看不出异常)。
804
+ * 由 `tests/trajectory-bound.test.ts` 的 D69 域路径用例抓到。
805
+ */
806
+ const toMark = /* @__PURE__ */ new Set();
807
+ if (domainStore?.available) for (const [id, raw] of domainStore.entries()) {
808
+ const rec = fromDomainRecord(raw);
809
+ if (rec.dirName !== dirName) continue;
810
+ if (keepIds.has(id) || rec.supersededAt !== void 0) continue;
811
+ toMark.add(id);
812
+ }
675
813
  for (const [id, raw] of Object.entries(file.sessions)) {
676
814
  const rec = asBoundRecord(raw);
677
815
  if (!rec || rec.dirName !== dirName) continue;
678
816
  if (keepIds.has(id) || rec.supersededAt !== void 0) continue;
817
+ toMark.add(id);
818
+ }
819
+ if (toMark.size === 0) return [];
820
+ if (domainStore?.available) for (const id of toMark) {
821
+ const raw = domainStore.get(id);
822
+ if (!raw) continue;
823
+ domainStore.put(id, toDomainRecord({
824
+ ...fromDomainRecord(raw),
825
+ supersededAt: at
826
+ })).catch(() => {});
827
+ }
828
+ for (const id of toMark) {
829
+ const rec = asBoundRecord(file.sessions[id]);
830
+ if (!rec) continue;
679
831
  rec.supersededAt = at;
680
832
  file.sessions[id] = rec;
681
- marked.push(id);
682
833
  }
683
- if (marked.length === 0) return [];
684
834
  try {
685
835
  writeBindingsFile(path, file);
686
- } catch {
687
- return [];
688
- }
689
- return marked.sort();
836
+ } catch {}
837
+ return [...toMark].sort();
690
838
  }
691
839
  /**
692
840
  * **(c) 绑定一致性兜底**(S142 §0y,所有者 2026-09-18 裁「c 也算个兜底」):
@@ -708,25 +856,25 @@ function supersedeOtherBindings(root, dirName, keepIds) {
708
856
  function pruneMissingBindings(root, isMissing) {
709
857
  const path = join(root, BINDINGS_REL_PATH);
710
858
  const file = readBindingsFile(path);
711
- const removed = [];
712
- for (const id of Object.keys(file.sessions)) {
713
- let missing;
859
+ const toRemove = /* @__PURE__ */ new Set();
860
+ const judge = (id) => {
714
861
  try {
715
- missing = isMissing(id);
862
+ return isMissing(id);
716
863
  } catch {
717
- continue;
864
+ return false;
718
865
  }
719
- if (!missing) continue;
720
- delete file.sessions[id];
721
- removed.push(id);
866
+ };
867
+ if (domainStore?.available) {
868
+ for (const [id] of domainStore.entries()) if (judge(id)) toRemove.add(id);
722
869
  }
723
- if (removed.length === 0) return [];
870
+ for (const id of Object.keys(file.sessions)) if (judge(id)) toRemove.add(id);
871
+ if (toRemove.size === 0) return [];
872
+ if (domainStore?.available) for (const id of toRemove) domainStore.delete(id).catch(() => {});
873
+ for (const id of toRemove) delete file.sessions[id];
724
874
  try {
725
875
  writeBindingsFile(path, file);
726
- } catch {
727
- return [];
728
- }
729
- return removed.sort();
876
+ } catch {}
877
+ return [...toRemove].sort();
730
878
  }
731
879
  /**
732
880
  * 写入绑定(latest-wins 覆盖该会话记录)。
@@ -739,22 +887,25 @@ function pruneMissingBindings(root, isMissing) {
739
887
  function appendBound(session, action, rec) {
740
888
  const path = bindingsPathFor(session);
741
889
  const id = sessionHeader(session)?.id;
742
- if (!path || typeof id !== "string") return false;
890
+ if (typeof id !== "string") return false;
891
+ const record = {
892
+ dirName: rec.dirName,
893
+ mdPath: rec.mdPath,
894
+ ...rec.sessionId ? { sessionId: rec.sessionId } : {},
895
+ action,
896
+ at: Date.now(),
897
+ ...rec.note ? { note: rec.note } : {}
898
+ };
899
+ if (domainStore?.available) domainStore.put(id, toDomainRecord(record)).catch(() => {});
900
+ if (!path) return domainStore?.available === true;
743
901
  try {
744
902
  const file = readBindingsFile(path);
745
- file.sessions[id] = {
746
- dirName: rec.dirName,
747
- mdPath: rec.mdPath,
748
- ...rec.sessionId ? { sessionId: rec.sessionId } : {},
749
- action,
750
- at: Date.now(),
751
- ...rec.note ? { note: rec.note } : {}
752
- };
903
+ file.sessions[id] = record;
753
904
  writeBindingsFile(path, file);
754
905
  return true;
755
906
  } catch {
756
- return false;
907
+ return domainStore?.available === true;
757
908
  }
758
909
  }
759
910
  //#endregion
760
- export { useSession as C, localIdStamp as D, localHuman as E, summarize as S, localFileStamp as T, resolveSessionByTitle as _, resolveSessionTrajectoryLabel as a, setActiveSessionInfo as b, TRAJECTORY_ACTIONS as c, extractSessionMdPathFromText as d, findSession as f, readActiveSessionMd as g, parseSessionContextFromEvents as h, readLastBound as i, clearActiveSessionInfo as l, listSessions as m, listBoundSessionIds as n, supersedeOtherBindings as o, getActiveSessionInfo as p, pruneMissingBindings as r, DEFAULT_SESSION_SCOPE as s, appendBound as t, createSession as u, sessionEvents as v, isoLocal as w, showSession as x, sessionsRoot as y };
911
+ export { showSession as C, localFileStamp as D, isoLocal as E, localHuman as O, setActiveSessionInfo as S, useSession as T, parseSessionContextFromEvents as _, readLastBound as a, sessionEvents as b, supersedeOtherBindings as c, clearActiveSessionInfo as d, createSession as f, listSessions as g, getActiveSessionInfo as h, pruneMissingBindings as i, localIdStamp as k, DEFAULT_SESSION_SCOPE as l, findSession as m, listBoundSessionIds as n, resolveSessionTrajectoryLabel as o, extractSessionMdPathFromText as p, migrateBindingsToDomain as r, setBindingStore as s, appendBound as t, TRAJECTORY_ACTIONS as u, readActiveSessionMd as v, summarize as w, sessionsRoot as x, resolveSessionByTitle as y };
@@ -23,6 +23,7 @@
23
23
  * 编码无关(U4):绑定锚 = **完整 AGENT_SESSIONS 目录名**(磁盘唯一存在),
24
24
  * sessionId(S###/apaas-xxx/自定义)仅是派生展示字段——绝不假设 S 前缀。
25
25
  */
26
+ import { type BindingStore } from './host/storage-domain.js';
26
27
  type SessionBoundAction = 'activate' | 'switch' | 'create' | 'rebuild' | 'reconcile' | 'release';
27
28
  interface SessionBoundRecord {
28
29
  dirName: string;
@@ -45,10 +46,41 @@ interface SessionBoundRecord {
45
46
  }
46
47
  /** 绑定文件相对 CCC 根的路径 */
47
48
  export declare const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
49
+ /**
50
+ * 注入域句柄(装载期调用一次)。传 `null` = 域不可用 ⇒ 全部退回旧文件行为。
51
+ * 幂等:重复调用以后者为准(HMR / 重载场景)。
52
+ */
53
+ export declare function setBindingStore(store: BindingStore | null): void;
54
+ /** 当前域句柄(诊断用;`null` = 未接线/不可用) */
55
+ export declare function getBindingStore(): BindingStore | null;
56
+ /**
57
+ * §0L **一次性迁移**:把某个 CCC 的旧 `.bindings.json` 全量灌入宿主存储域。
58
+ *
59
+ * 设计要点(R↓):
60
+ * - **幂等**:只写域里**尚不存在**的键(`skip existing`)。⇒ 重复调用(每次启动都调)
61
+ * 不会覆盖域里的新状态,也不会因并发而互相踩踏。
62
+ * - **不删旧文件**:迁移只读不写旧表(owner「向前兼容」的第二道保险仍在)。
63
+ * - **不丢绑定**:逐条迁移,单条失败不影响其余(返回计数供诊断)。
64
+ * - **失败静默**:域写不可用 ⇒ 返回 `{migrated:0, skipped:0, failed:0}`,不抛错
65
+ * (装载期调用,不能成为启动单点)。
66
+ *
67
+ * @param root CCC 根(`AGENT_SESSIONS/.bindings.json` 所在)
68
+ * @returns 迁移计数(诊断用;`migrated` = 本次真正灌入的条数)
69
+ */
70
+ export declare function migrateBindingsToDomain(root: string): {
71
+ migrated: number;
72
+ skipped: number;
73
+ failed: number;
74
+ };
48
75
  /** 绑定文件绝对路径(由会话 cwd 上溯 .serenity 定位 CCC 根);无法定位返回 null */
49
76
  export declare function bindingsPathFor(session: unknown): string | null;
50
77
  /**
51
- * 读取会话当前绑定(权威,latest-wins):文件记录优先,无则回落旧事件形态。
78
+ * 读取会话当前绑定(权威,latest-wins)。
79
+ *
80
+ * 读取顺序(§0L,S142 2026-09-19):
81
+ * 1. **宿主存储域**(域可用时的权威来源);
82
+ * 2. **旧文件** `.bindings.json`(域不可用 ⇒ 零回归回落;域可用但该会话未迁移 ⇒ 兜底);
83
+ * 3. **旧事件形态** `serenity/bound`(v1.30.5 及更早的存量)。
52
84
  * 无绑定返回 null。
53
85
  */
54
86
  export declare function readLastBound(session: unknown): SessionBoundRecord | null;
@@ -1,6 +1,6 @@
1
1
  import { r as hostService, t as hostAgents } from "./access-fiehjxV6.js";
2
2
  import { o as listCccs } from "./ccc-roots-Bd_SjEs7.js";
3
- import { D as localIdStamp, E as localHuman, f as findSession, n as listBoundSessionIds, w as isoLocal, y as sessionsRoot } from "./trajectory-bound-B9o_4CMF.js";
3
+ import { E as isoLocal, O as localHuman, k as localIdStamp, m as findSession, n as listBoundSessionIds, x as sessionsRoot } from "./trajectory-bound-CQTh86vq.js";
4
4
  import { t as readSimpleSettings } from "./settings-section-DMDUoQum.js";
5
5
  import { basename, dirname, join } from "node:path";
6
6
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.39.3",
3
+ "version": "1.40.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2。",
5
5
  "license": "MIT",
6
6
  "repository": {