@shgroup/dsh-serenity-hooks 1.39.3 → 1.40.1
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 +1 -1
- package/lib/host/storage-domain.d.ts +115 -0
- package/lib/index.js +177 -2
- package/lib/{trajectory-bound-B9o_4CMF.js → trajectory-bound-4xkZDfmO.js} +210 -34
- package/lib/trajectory-bound.d.ts +48 -1
- package/lib/{wake-scheduler-CCpCk-7r.js → wake-scheduler-BRnPUnER.js} +1 -1
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.1",
|
|
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
|
|
5
|
+
import { C as summarize, E as localFileStamp, S as showSession, T as isoLocal, _ as readActiveSessionMd, a as resolveSessionTrajectoryLabel, b as sessionsRoot, c as DEFAULT_SESSION_SCOPE, d as createSession, f as extractSessionMdPathFromText, g as parseSessionContextFromEvents, h as listSessions, i as readLastBound, l as TRAJECTORY_ACTIONS, m as getActiveSessionInfo, o as setBindingStore, p as findSession, r as pruneMissingBindings, s as supersedeOtherBindings, t as appendBound, u as clearActiveSessionInfo, v as resolveSessionByTitle, w as useSession, x as setActiveSessionInfo, y as sessionEvents$1 } from "./trajectory-bound-4xkZDfmO.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-
|
|
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-BRnPUnER.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,15 @@ 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) console.log("[serenity-hooks] §0L 存储域不可用 → 绑定继续走 CCC 内 .bindings.json(向前兼容回落)");
|
|
14669
|
+
} catch {
|
|
14670
|
+
setBindingStore(null);
|
|
14671
|
+
}
|
|
14672
|
+
})();
|
|
14498
14673
|
if (config.tools) {
|
|
14499
14674
|
ctx.tools.register(ccFsTool);
|
|
14500
14675
|
ctx.tools.register(createTrajectoryTool(ctx));
|
|
@@ -526,6 +526,131 @@ 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
|
+
if (out.migrated > 0) migratedRoots.add(root);
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
/** 已补过迁移的 CCC 根(进程内去重;跨进程各补一次是可接受的——迁移幂等) */
|
|
606
|
+
const migratedRoots = /* @__PURE__ */ new Set();
|
|
607
|
+
/**
|
|
608
|
+
* **按 CCC 惰性补迁移**(§0L 修正,S142 2026-09-19)。
|
|
609
|
+
*
|
|
610
|
+
* 🔴 为什么需要它(实测缺陷,R↓):
|
|
611
|
+
* 装载期那次迁移用的是 `process.cwd()` —— 而 dsh **服务进程的 cwd 是 `/home/yh`**,
|
|
612
|
+
* **不是 CCC 根**(实测 `acc-diag`:`进程 cwd: /home/yh | 进程 CCC: (无)`)。
|
|
613
|
+
* ⇒ 迁移读的是一个**不存在的文件**,静默迁移 0 条
|
|
614
|
+
* (症状:发布后 `~/.dsh/storages/serenity_bindings.json` **没出现**)。
|
|
615
|
+
*
|
|
616
|
+
* 现在改为:**任何一次能定位到 CCC 根的路径**都顺手补一次迁移(幂等 + 进程内去重)。
|
|
617
|
+
* 这样无论服务从哪个 cwd 启动,只要那条 CCC 有会话活动,它的绑定就会被带上。
|
|
618
|
+
*
|
|
619
|
+
* @param root CCC 根
|
|
620
|
+
*/
|
|
621
|
+
function ensureBindingsMigrated(root) {
|
|
622
|
+
if (!domainStore?.available) return;
|
|
623
|
+
if (migratedRoots.has(root)) return;
|
|
624
|
+
try {
|
|
625
|
+
migrateBindingsToDomain(root);
|
|
626
|
+
} catch {}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* 域内读一条(同步)。域不可用 → `undefined`(调用方据此回落文件)。
|
|
630
|
+
* @param sessionId dsh 会话 id
|
|
631
|
+
*/
|
|
632
|
+
function readFromDomain(sessionId) {
|
|
633
|
+
if (!domainStore?.available) return void 0;
|
|
634
|
+
const rec = domainStore.get(sessionId);
|
|
635
|
+
return rec ? fromDomainRecord(rec) : void 0;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* 域内取**某轨迹的全部绑定**(同步;已排除 superseded —— 与 `listBoundSessionIds` 同判据)。
|
|
639
|
+
* 域不可用 → `null`(**注意与"空结果"区分**:null = 回落文件,[] = 域里确实没有)。
|
|
640
|
+
*/
|
|
641
|
+
function listFromDomain(dirName) {
|
|
642
|
+
if (!domainStore?.available) return null;
|
|
643
|
+
const out = [];
|
|
644
|
+
for (const [id, raw] of domainStore.entries()) {
|
|
645
|
+
const rec = fromDomainRecord(raw);
|
|
646
|
+
if (rec.dirName !== dirName) continue;
|
|
647
|
+
out.push({
|
|
648
|
+
id,
|
|
649
|
+
rec
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return out;
|
|
653
|
+
}
|
|
529
654
|
function sessionHeader(session) {
|
|
530
655
|
const header = session?.header;
|
|
531
656
|
if (!header || typeof header !== "object") return null;
|
|
@@ -537,6 +662,7 @@ function bindingsPathFor(session) {
|
|
|
537
662
|
if (typeof header?.cwd !== "string") return null;
|
|
538
663
|
const root = cccRootForCwd(header.cwd);
|
|
539
664
|
if (!root) return null;
|
|
665
|
+
ensureBindingsMigrated(root);
|
|
540
666
|
return join(root, BINDINGS_REL_PATH);
|
|
541
667
|
}
|
|
542
668
|
function readBindingsFile(path) {
|
|
@@ -586,12 +712,21 @@ function readLegacyBoundFromEvents(session) {
|
|
|
586
712
|
return null;
|
|
587
713
|
}
|
|
588
714
|
/**
|
|
589
|
-
* 读取会话当前绑定(权威,latest-wins
|
|
715
|
+
* 读取会话当前绑定(权威,latest-wins)。
|
|
716
|
+
*
|
|
717
|
+
* 读取顺序(§0L,S142 2026-09-19):
|
|
718
|
+
* 1. **宿主存储域**(域可用时的权威来源);
|
|
719
|
+
* 2. **旧文件** `.bindings.json`(域不可用 ⇒ 零回归回落;域可用但该会话未迁移 ⇒ 兜底);
|
|
720
|
+
* 3. **旧事件形态** `serenity/bound`(v1.30.5 及更早的存量)。
|
|
590
721
|
* 无绑定返回 null。
|
|
591
722
|
*/
|
|
592
723
|
function readLastBound(session) {
|
|
593
|
-
const path = bindingsPathFor(session);
|
|
594
724
|
const id = sessionHeader(session)?.id;
|
|
725
|
+
if (typeof id === "string") {
|
|
726
|
+
const fromDomain = readFromDomain(id);
|
|
727
|
+
if (fromDomain) return fromDomain;
|
|
728
|
+
}
|
|
729
|
+
const path = bindingsPathFor(session);
|
|
595
730
|
if (path && typeof id === "string") {
|
|
596
731
|
const rec = asBoundRecord(readBindingsFile(path).sessions[id]);
|
|
597
732
|
if (rec) return rec;
|
|
@@ -639,12 +774,24 @@ function resolveSessionTrajectoryLabel(session, scope) {
|
|
|
639
774
|
* @returns 会话 id 列表(最新绑定在前);无绑定 → `[]`
|
|
640
775
|
*/
|
|
641
776
|
function listBoundSessionIds(root, dirName) {
|
|
642
|
-
const file = readBindingsFile(join(root, BINDINGS_REL_PATH));
|
|
643
777
|
const hits = [];
|
|
778
|
+
const fromDomain = listFromDomain(dirName);
|
|
779
|
+
if (fromDomain !== null) {
|
|
780
|
+
for (const { id, rec } of fromDomain) {
|
|
781
|
+
if (rec.supersededAt !== void 0) continue;
|
|
782
|
+
hits.push({
|
|
783
|
+
id,
|
|
784
|
+
at: typeof rec.at === "number" ? rec.at : 0
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
if (hits.length > 0) return hits.sort((a, b) => b.at - a.at).map((h) => h.id);
|
|
788
|
+
}
|
|
789
|
+
const file = readBindingsFile(join(root, BINDINGS_REL_PATH));
|
|
644
790
|
for (const [id, raw] of Object.entries(file.sessions)) {
|
|
645
791
|
const rec = asBoundRecord(raw);
|
|
646
792
|
if (!rec || rec.dirName !== dirName) continue;
|
|
647
793
|
if (rec.supersededAt !== void 0) continue;
|
|
794
|
+
if (hits.some((h) => h.id === id)) continue;
|
|
648
795
|
hits.push({
|
|
649
796
|
id,
|
|
650
797
|
at: typeof rec.at === "number" ? rec.at : 0
|
|
@@ -670,23 +817,49 @@ function listBoundSessionIds(root, dirName) {
|
|
|
670
817
|
function supersedeOtherBindings(root, dirName, keepIds) {
|
|
671
818
|
const path = join(root, BINDINGS_REL_PATH);
|
|
672
819
|
const file = readBindingsFile(path);
|
|
673
|
-
const marked = [];
|
|
674
820
|
const at = Date.now();
|
|
821
|
+
/**
|
|
822
|
+
* 需要退休的会话 id 集合。
|
|
823
|
+
*
|
|
824
|
+
* 🔴 必须取**域 ∪ 文件的并集**(实测缺陷,2026-09-19):
|
|
825
|
+
* 首版实现只在**文件循环**里 `marked.push(id)`,于是"记录只在域里"时
|
|
826
|
+
* 返回值恒为 `[]` —— 而调用方(`tools/trajectory.ts` 的 `use`)**靠这个返回值**决定
|
|
827
|
+
* 是否回报 `supersededBindings`,并且早先用 `marked.length === 0` 短路**跳过了写盘**。
|
|
828
|
+
* ⇒ 症状是"D69 只留一条**静默不生效**"(记录没被标记,返回也看不出异常)。
|
|
829
|
+
* 由 `tests/trajectory-bound.test.ts` 的 D69 域路径用例抓到。
|
|
830
|
+
*/
|
|
831
|
+
const toMark = /* @__PURE__ */ new Set();
|
|
832
|
+
if (domainStore?.available) for (const [id, raw] of domainStore.entries()) {
|
|
833
|
+
const rec = fromDomainRecord(raw);
|
|
834
|
+
if (rec.dirName !== dirName) continue;
|
|
835
|
+
if (keepIds.has(id) || rec.supersededAt !== void 0) continue;
|
|
836
|
+
toMark.add(id);
|
|
837
|
+
}
|
|
675
838
|
for (const [id, raw] of Object.entries(file.sessions)) {
|
|
676
839
|
const rec = asBoundRecord(raw);
|
|
677
840
|
if (!rec || rec.dirName !== dirName) continue;
|
|
678
841
|
if (keepIds.has(id) || rec.supersededAt !== void 0) continue;
|
|
842
|
+
toMark.add(id);
|
|
843
|
+
}
|
|
844
|
+
if (toMark.size === 0) return [];
|
|
845
|
+
if (domainStore?.available) for (const id of toMark) {
|
|
846
|
+
const raw = domainStore.get(id);
|
|
847
|
+
if (!raw) continue;
|
|
848
|
+
domainStore.put(id, toDomainRecord({
|
|
849
|
+
...fromDomainRecord(raw),
|
|
850
|
+
supersededAt: at
|
|
851
|
+
})).catch(() => {});
|
|
852
|
+
}
|
|
853
|
+
for (const id of toMark) {
|
|
854
|
+
const rec = asBoundRecord(file.sessions[id]);
|
|
855
|
+
if (!rec) continue;
|
|
679
856
|
rec.supersededAt = at;
|
|
680
857
|
file.sessions[id] = rec;
|
|
681
|
-
marked.push(id);
|
|
682
858
|
}
|
|
683
|
-
if (marked.length === 0) return [];
|
|
684
859
|
try {
|
|
685
860
|
writeBindingsFile(path, file);
|
|
686
|
-
} catch {
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
return marked.sort();
|
|
861
|
+
} catch {}
|
|
862
|
+
return [...toMark].sort();
|
|
690
863
|
}
|
|
691
864
|
/**
|
|
692
865
|
* **(c) 绑定一致性兜底**(S142 §0y,所有者 2026-09-18 裁「c 也算个兜底」):
|
|
@@ -708,25 +881,25 @@ function supersedeOtherBindings(root, dirName, keepIds) {
|
|
|
708
881
|
function pruneMissingBindings(root, isMissing) {
|
|
709
882
|
const path = join(root, BINDINGS_REL_PATH);
|
|
710
883
|
const file = readBindingsFile(path);
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
let missing;
|
|
884
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
885
|
+
const judge = (id) => {
|
|
714
886
|
try {
|
|
715
|
-
|
|
887
|
+
return isMissing(id);
|
|
716
888
|
} catch {
|
|
717
|
-
|
|
889
|
+
return false;
|
|
718
890
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
891
|
+
};
|
|
892
|
+
if (domainStore?.available) {
|
|
893
|
+
for (const [id] of domainStore.entries()) if (judge(id)) toRemove.add(id);
|
|
722
894
|
}
|
|
723
|
-
|
|
895
|
+
for (const id of Object.keys(file.sessions)) if (judge(id)) toRemove.add(id);
|
|
896
|
+
if (toRemove.size === 0) return [];
|
|
897
|
+
if (domainStore?.available) for (const id of toRemove) domainStore.delete(id).catch(() => {});
|
|
898
|
+
for (const id of toRemove) delete file.sessions[id];
|
|
724
899
|
try {
|
|
725
900
|
writeBindingsFile(path, file);
|
|
726
|
-
} catch {
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
return removed.sort();
|
|
901
|
+
} catch {}
|
|
902
|
+
return [...toRemove].sort();
|
|
730
903
|
}
|
|
731
904
|
/**
|
|
732
905
|
* 写入绑定(latest-wins 覆盖该会话记录)。
|
|
@@ -739,22 +912,25 @@ function pruneMissingBindings(root, isMissing) {
|
|
|
739
912
|
function appendBound(session, action, rec) {
|
|
740
913
|
const path = bindingsPathFor(session);
|
|
741
914
|
const id = sessionHeader(session)?.id;
|
|
742
|
-
if (
|
|
915
|
+
if (typeof id !== "string") return false;
|
|
916
|
+
const record = {
|
|
917
|
+
dirName: rec.dirName,
|
|
918
|
+
mdPath: rec.mdPath,
|
|
919
|
+
...rec.sessionId ? { sessionId: rec.sessionId } : {},
|
|
920
|
+
action,
|
|
921
|
+
at: Date.now(),
|
|
922
|
+
...rec.note ? { note: rec.note } : {}
|
|
923
|
+
};
|
|
924
|
+
if (domainStore?.available) domainStore.put(id, toDomainRecord(record)).catch(() => {});
|
|
925
|
+
if (!path) return domainStore?.available === true;
|
|
743
926
|
try {
|
|
744
927
|
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
|
-
};
|
|
928
|
+
file.sessions[id] = record;
|
|
753
929
|
writeBindingsFile(path, file);
|
|
754
930
|
return true;
|
|
755
931
|
} catch {
|
|
756
|
-
return
|
|
932
|
+
return domainStore?.available === true;
|
|
757
933
|
}
|
|
758
934
|
}
|
|
759
935
|
//#endregion
|
|
760
|
-
export {
|
|
936
|
+
export { summarize as C, localHuman as D, localFileStamp as E, localIdStamp as O, showSession as S, isoLocal as T, readActiveSessionMd as _, resolveSessionTrajectoryLabel as a, sessionsRoot as b, DEFAULT_SESSION_SCOPE as c, createSession as d, extractSessionMdPathFromText as f, parseSessionContextFromEvents as g, listSessions as h, readLastBound as i, TRAJECTORY_ACTIONS as l, getActiveSessionInfo as m, listBoundSessionIds as n, setBindingStore as o, findSession as p, pruneMissingBindings as r, supersedeOtherBindings as s, appendBound as t, clearActiveSessionInfo as u, resolveSessionByTitle as v, useSession as w, setActiveSessionInfo as x, sessionEvents 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,56 @@ 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
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* **按 CCC 惰性补迁移**(§0L 修正,S142 2026-09-19)。
|
|
77
|
+
*
|
|
78
|
+
* 🔴 为什么需要它(实测缺陷,R↓):
|
|
79
|
+
* 装载期那次迁移用的是 `process.cwd()` —— 而 dsh **服务进程的 cwd 是 `/home/yh`**,
|
|
80
|
+
* **不是 CCC 根**(实测 `acc-diag`:`进程 cwd: /home/yh | 进程 CCC: (无)`)。
|
|
81
|
+
* ⇒ 迁移读的是一个**不存在的文件**,静默迁移 0 条
|
|
82
|
+
* (症状:发布后 `~/.dsh/storages/serenity_bindings.json` **没出现**)。
|
|
83
|
+
*
|
|
84
|
+
* 现在改为:**任何一次能定位到 CCC 根的路径**都顺手补一次迁移(幂等 + 进程内去重)。
|
|
85
|
+
* 这样无论服务从哪个 cwd 启动,只要那条 CCC 有会话活动,它的绑定就会被带上。
|
|
86
|
+
*
|
|
87
|
+
* @param root CCC 根
|
|
88
|
+
*/
|
|
89
|
+
export declare function ensureBindingsMigrated(root: string): void;
|
|
48
90
|
/** 绑定文件绝对路径(由会话 cwd 上溯 .serenity 定位 CCC 根);无法定位返回 null */
|
|
49
91
|
export declare function bindingsPathFor(session: unknown): string | null;
|
|
50
92
|
/**
|
|
51
|
-
* 读取会话当前绑定(权威,latest-wins
|
|
93
|
+
* 读取会话当前绑定(权威,latest-wins)。
|
|
94
|
+
*
|
|
95
|
+
* 读取顺序(§0L,S142 2026-09-19):
|
|
96
|
+
* 1. **宿主存储域**(域可用时的权威来源);
|
|
97
|
+
* 2. **旧文件** `.bindings.json`(域不可用 ⇒ 零回归回落;域可用但该会话未迁移 ⇒ 兜底);
|
|
98
|
+
* 3. **旧事件形态** `serenity/bound`(v1.30.5 及更早的存量)。
|
|
52
99
|
* 无绑定返回 null。
|
|
53
100
|
*/
|
|
54
101
|
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
|
|
3
|
+
import { D as localHuman, O as localIdStamp, T as isoLocal, b as sessionsRoot, n as listBoundSessionIds, p as findSession } from "./trajectory-bound-4xkZDfmO.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.
|
|
3
|
+
"version": "1.40.1",
|
|
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": {
|