@shgroup/dsh-serenity-hooks 1.76.0 → 1.77.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 +1 -1
- package/lib/cleanup-C-92qnDs.js +213 -0
- package/lib/index.js +26 -232
- package/lib/message-source-CYa8LbbM.js +488 -0
- package/lib/trajectory/bind-file.d.ts +228 -0
- package/lib/trajectory/bound.d.ts +117 -132
- package/package.json +1 -1
- package/lib/message-source-C_Zd2vJh.js +0 -435
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trajectory/bind-file.ts — **当前载体绑定 = 认知轨迹目录里的自我交代**(判决 D172,owner 2026-10-20)
|
|
3
|
+
*
|
|
4
|
+
* ## owner 的原话(这是本模块的全部设计依据)
|
|
5
|
+
*
|
|
6
|
+
* > 「**认知轨迹自己交代永远是最好的交代**」
|
|
7
|
+
* > 「绑定表是没有意义的,绑定关系必须**运行时按照规则探测**,绑定表本质就搞成**第二真相源**了」
|
|
8
|
+
* > 「依赖文件系统,定义一个 `.dsh_session_bind` 后缀(加入 gitignore);每个认知轨迹目录内放一个
|
|
9
|
+
* > `current.dsh_session_bind`,**以这个为绑定真相源**」
|
|
10
|
+
*
|
|
11
|
+
* ## 为什么这是对的(而不是又一张表)
|
|
12
|
+
*
|
|
13
|
+
* 旧的 `.bindings.json` + 宿主存储域是**旁路索引**:它描述的对象(轨迹目录)与它自己**分离**,
|
|
14
|
+
* ⇒ 一旦两者不一致就必须写代码去对账,而**对账失败没有任何机械信号**(本轨反复踩的形状)。
|
|
15
|
+
*
|
|
16
|
+
* 新形态把这条信息**放回它描述的对象的内部**:
|
|
17
|
+
*
|
|
18
|
+
* ```text
|
|
19
|
+
* <轨迹目录>/
|
|
20
|
+
* SESSION.md ← 身体
|
|
21
|
+
* PROMPT.md ← 章程
|
|
22
|
+
* trajectory.json ← 声明(约束面 / uid / cro.host)
|
|
23
|
+
* current.dsh_session_bind ← 🆕 **此刻谁在承载它**(单个文件 ⇒ 结构上不可能有两个)
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* ## 🔴 两条不变量由**形态本身**保证,不靠记账
|
|
27
|
+
*
|
|
28
|
+
* | 不变量 | 保证方式 |
|
|
29
|
+
* |---|---|
|
|
30
|
+
* | 一条轨迹**至多一个**当前载体 | 它是**一个文件**,写入即整体覆盖(原子 rename) |
|
|
31
|
+
* | 一个载体**至多一条**轨迹 | 写入前扫全部轨迹的 bind 文件,**该会话 id 已出现在别处 ⇒ 拒绝** |
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ 旧形态靠 `supersededAt` 标记"退掉",而**标记是可以忘的**;覆盖不会忘。
|
|
34
|
+
*
|
|
35
|
+
* ## 零依赖纪律(同 `policy/files.ts`)
|
|
36
|
+
* 只用 `node:fs` / `node:path` ⇒ 谁 import 它都不会成环。每个读函数**永不抛**。
|
|
37
|
+
*
|
|
38
|
+
* ## 🔴 第三条不变量(owner 2026-10-20 追加):「文件里写的 sessionId 可能已经不存在了
|
|
39
|
+
* ——这个情况下就**视同没绑定**,就正确了」
|
|
40
|
+
*
|
|
41
|
+
* 这一条让整个设计**自洽**:**绑定的有效性由「那个会话真实存在」决定,不由「文件存在」决定。**
|
|
42
|
+
*
|
|
43
|
+
* ```text
|
|
44
|
+
* 文件在 + 会话在 ⇒ 有效绑定
|
|
45
|
+
* 文件在 + 会话没了 ⇒ **视同没绑定**(过期文件自动失效,不需要任何人来清理)
|
|
46
|
+
* 文件不在 ⇒ 没绑定
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* ⇒ 因此**不需要**任何"清理陈旧绑定"的机制,也不需要 `reconcile` 去对账 —— 对账这件事本身消失。
|
|
50
|
+
* 代价是**每次取用都要验一次会话是否还在**(`ctx.agents.get` 或 `sessionController.resolveAgent`
|
|
51
|
+
* 或宿主会话清单),**这是运行时探测**,正是 owner 要的那种判据。
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ 本模块**只负责读写文件**;"会话是否还在"必须由**调用方**(持有宿主上下文)来验 ——
|
|
54
|
+
* 把那条判断放在这里会迫使本模块拿到 `ctx`,从而破坏零依赖与可单测性。
|
|
55
|
+
* 调用方正确用法见 {@link isBindUsable}。
|
|
56
|
+
*
|
|
57
|
+
* ## 🔴 第四条:**完整生命周期**(owner 2026-10-20 追加:「调用 use 会覆盖掉这个文件,
|
|
58
|
+
* 完整生命周期要实现,请注意」)
|
|
59
|
+
*
|
|
60
|
+
* 文件是**一个槽位**,不是追加日志。⇒ 每次改变归属都是一次**状态迁移**,
|
|
61
|
+
* 四个动作必须**成对**出现,否则槽位会留下无法解释的残留:
|
|
62
|
+
*
|
|
63
|
+
* | 迁移 | 何时 | 必须做什么 |
|
|
64
|
+
* |---|---|---|
|
|
65
|
+
* | **acquire** | 会话首次认领轨迹 | 写槽位(该会话此前不持有任何轨迹) |
|
|
66
|
+
* | **switch** | 会话从轨迹 A **改到** B | 🔴 **先清 A 的槽位,再写 B** —— 只写 B 会让 A 永远指着一个不再属于它的会话 |
|
|
67
|
+
* | **release** | 会话主动解绑 / 会话将死 | **清掉槽位**(不是写空串,是删文件) |
|
|
68
|
+
* | **takeover** | 载体换代(rebuild / 崩溃后重建) | 覆盖写:旧会话退位,新会话入位(**必须显式声明**) |
|
|
69
|
+
*
|
|
70
|
+
* 🔴 **最容易漏的是 switch 的"先清 A"**:旧实现(`.bindings.json`)天然支持"一个会话多条记录",
|
|
71
|
+
* 所以从来没暴露过这个问题;新形态是**单槽位**,不清 A 就会得到
|
|
72
|
+
* 「A 与 B 都说这个会话属于自己」—— 正是 owner 要禁的"一个会话绑两条轨迹",
|
|
73
|
+
* 只不过这次是**残留**而不是**主动绑定**,更难发现。
|
|
74
|
+
*
|
|
75
|
+
* ⇒ 因此**不要**直接调 {@link writeBindDeclaration} 来改归属;一律走
|
|
76
|
+
* {@link transitionBinding}(它把四条迁移的配对关系写死在一处)。
|
|
77
|
+
*/
|
|
78
|
+
/** 绑定文件的固定名(**owner 指定**:`current.dsh_session_bind`) */
|
|
79
|
+
export declare const BIND_FILENAME = "current.dsh_session_bind";
|
|
80
|
+
/**
|
|
81
|
+
* 绑定文件的**族**(认族不认名,D167 通用纪律)。
|
|
82
|
+
*
|
|
83
|
+
* 目前只有一代。留成数组是**为了下一次改名不重演历史**:本仓已在
|
|
84
|
+
* `skiff.json → trajectory-constraint.json → trajectory.json` 上栽过两次
|
|
85
|
+
* (上游改名、下游写死旧名 ⇒ 症状与真因在读数上完全不像)。
|
|
86
|
+
*/
|
|
87
|
+
export declare const BIND_FILENAME_FAMILY: readonly ["current.dsh_session_bind"];
|
|
88
|
+
/**
|
|
89
|
+
* 绑定文件的内容(**自我交代**:不只是"谁",还有"何时、为何、谁建的")。
|
|
90
|
+
*
|
|
91
|
+
* 字段与旧的 `SessionBoundRecord` 同名 —— 迁移不需要映射表(降低出错面)。
|
|
92
|
+
*/
|
|
93
|
+
export interface BindDeclaration {
|
|
94
|
+
/** 承载它的 dsh 会话 id(**硬锚**) */
|
|
95
|
+
sessionId: string;
|
|
96
|
+
/** 绑定发生的时刻(epoch ms) */
|
|
97
|
+
at: number;
|
|
98
|
+
/** 绑定动作:activate / switch / create / rebuild / reconcile / release */
|
|
99
|
+
action: string;
|
|
100
|
+
/** 人类可读的一句话(**不是判据**,判据见 `createdAsUser`) */
|
|
101
|
+
note?: string;
|
|
102
|
+
/** 这条载体是 ACC 以"用户开场白"形态建的(D170)。**布尔事实**,不从 note 文案推 */
|
|
103
|
+
createdAsUser?: boolean;
|
|
104
|
+
}
|
|
105
|
+
/** 某轨迹目录里绑定文件的绝对路径 */
|
|
106
|
+
export declare function bindFilePath(trajectoryDir: string): string;
|
|
107
|
+
/**
|
|
108
|
+
* 读一条轨迹的**当前载体声明**。
|
|
109
|
+
*
|
|
110
|
+
* @param trajectoryDir 轨迹目录绝对路径
|
|
111
|
+
* @returns 声明;文件不存在 / 坏 JSON / 形状不符 ⇒ `null`(**永不抛**)
|
|
112
|
+
*/
|
|
113
|
+
export declare function readBindDeclaration(trajectoryDir: string): BindDeclaration | null;
|
|
114
|
+
/**
|
|
115
|
+
* 写一条轨迹的**当前载体声明**(原子:同目录 tmp + rename)。
|
|
116
|
+
*
|
|
117
|
+
* 🔴 **不做唯一性检查** —— 那是 `claimBinding` 的职责。本函数只负责"这份文件长成什么样"。
|
|
118
|
+
* 分开的理由:唯一性检查需要**扫全部轨迹**,而写只需要一个目录 ⇒ 让不扫盘的地方能便宜地写。
|
|
119
|
+
*/
|
|
120
|
+
export declare function writeBindDeclaration(trajectoryDir: string, decl: BindDeclaration): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* 清掉一条轨迹的当前载体声明(解绑)。
|
|
123
|
+
* @returns 文件原本存在且已删除 ⇒ true
|
|
124
|
+
*/
|
|
125
|
+
export declare function clearBindDeclaration(trajectoryDir: string): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* 列出某 CCC 下**全部轨迹的当前载体声明**(`dirName → 声明`)。
|
|
128
|
+
*
|
|
129
|
+
* 这是**反向查询**(会话 → 轨迹 / 轨迹 → 会话)的唯一入口,取代旧的旁路表扫描。
|
|
130
|
+
* 跳过 `_` / `.` 前缀目录(归档区与运行时区不是轨迹,判据同 `activeTrajectoryDirNames`)。
|
|
131
|
+
*
|
|
132
|
+
* @param sessionsRoot `<CCC>/AGENT_SESSIONS`
|
|
133
|
+
* @returns 有交代的轨迹;**永不抛**(目录读不到 ⇒ 空表)
|
|
134
|
+
*/
|
|
135
|
+
export declare function listBindDeclarations(sessionsRoot: string): Array<{
|
|
136
|
+
dirName: string;
|
|
137
|
+
decl: BindDeclaration;
|
|
138
|
+
}>;
|
|
139
|
+
/**
|
|
140
|
+
* **这条绑定还作数吗**(第三条不变量的机械形态)。
|
|
141
|
+
*
|
|
142
|
+
* 判据(owner 原话:「文件里写的 sessionId 可能已经不存在了,这个情况下就视同没绑定」):
|
|
143
|
+
*
|
|
144
|
+
* | 文件 | 会话存在? | 结论 |
|
|
145
|
+
* |---|---|---|
|
|
146
|
+
* | 无 | — | `absent` |
|
|
147
|
+
* | 有 | 是 | **`usable`** |
|
|
148
|
+
* | 有 | 否 | **`stale`** ⇒ **视同没绑定** |
|
|
149
|
+
*
|
|
150
|
+
* 🔴 **为什么"会话存在"必须由调用方给**:判断它要问宿主(`ctx.agents.get` /
|
|
151
|
+
* `sessionController.resolveAgent` / 会话清单),而本模块是零依赖层。
|
|
152
|
+
* ⇒ 这里只收一个 **`alive: (sessionId) => boolean`** 探针,把判断留在外面,本函数仍是纯逻辑、可单测。
|
|
153
|
+
*
|
|
154
|
+
* ⚠️ **`stale` 不删文件**:删除是写操作,而"视同没绑定"已经足够。
|
|
155
|
+
* 留着过期文件反而有用 —— 它记录了"这条轨迹上一次由谁承载"(沿革不丢,同旧 `supersededAt` 的意图)。
|
|
156
|
+
*
|
|
157
|
+
* @param decl {@link readBindDeclaration} 的结果
|
|
158
|
+
* @param alive 探针:该会话 id 此刻是否真实存在(**由调用方用宿主事实实现**)
|
|
159
|
+
* @returns `absent` / `usable` / `stale`,以及(usable 时的)声明
|
|
160
|
+
*/
|
|
161
|
+
export declare function isBindUsable(decl: BindDeclaration | null, alive: (sessionId: string) => boolean): {
|
|
162
|
+
state: 'absent';
|
|
163
|
+
} | {
|
|
164
|
+
state: 'usable';
|
|
165
|
+
decl: BindDeclaration;
|
|
166
|
+
} | {
|
|
167
|
+
state: 'stale';
|
|
168
|
+
decl: BindDeclaration;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* **解绑**(生命周期的第四步):把某条轨迹的槽位清空。
|
|
172
|
+
*
|
|
173
|
+
* ## 什么时候该调它(判据,不是"看情况")
|
|
174
|
+
*
|
|
175
|
+
* | 场景 | 该不该 release |
|
|
176
|
+
* |---|---|
|
|
177
|
+
* | 会话主动离开这条轨迹(`use` 换到别的轨迹) | ✅ **必须**(否则旧轨迹永远指着一个不再属于它的会话) |
|
|
178
|
+
* | 会话即将销毁 / 被用户关闭 | ✅ 该(但**不强制**:`isBindUsable` 会把它判成 `stale`,视同没绑定) |
|
|
179
|
+
* | 想"删掉绑定让下一条消息重新建" | ⛔ **不该** —— 那会掩盖归属问题;用 `takeover` 换代 |
|
|
180
|
+
*
|
|
181
|
+
* 🔴 **为什么"会话死了可以不 release"仍然安全**:第四条不变量已经把这件事兜住了
|
|
182
|
+
* (`stale` ⇒ 视同没绑定)。⇒ release 的价值**不是"让绑定失效"**(那有探针兜底),
|
|
183
|
+
* 而是**"让沿革干净"** —— 不留一个指向已无关会话的槽位。
|
|
184
|
+
*
|
|
185
|
+
* @param sessionsRoot `<CCC>/AGENT_SESSIONS`
|
|
186
|
+
* @param dirName 轨迹目录名
|
|
187
|
+
* @param expectSessionId 只在这个槽位当前确实是该会话时才清(防误清别人的)
|
|
188
|
+
* @returns 真的清掉了 ⇒ true(槽位本来空 / 不是那个会话 ⇒ false)
|
|
189
|
+
*/
|
|
190
|
+
export declare function releaseBinding(sessionsRoot: string, dirName: string, expectSessionId?: string): boolean;
|
|
191
|
+
/**
|
|
192
|
+
* 找出**已经占用**某个 dsh 会话 id 的那条轨迹(唯一性判据)。
|
|
193
|
+
*
|
|
194
|
+
* @param sessionsRoot `<CCC>/AGENT_SESSIONS`
|
|
195
|
+
* @param sessionId 待检查的会话 id
|
|
196
|
+
* @returns 占用它的轨迹目录名;无人占用 ⇒ `null`
|
|
197
|
+
*/
|
|
198
|
+
export declare function findTrajectoryHolding(sessionsRoot: string, sessionId: string): string | null;
|
|
199
|
+
/**
|
|
200
|
+
* **认领**:把某条轨迹的当前载体设成 `sessionId`,并**当场执行两条不变量**。
|
|
201
|
+
*
|
|
202
|
+
* 这是全仓**唯一**允许改变绑定关系的写入口:`appendBound` 与迁移脚本都走这里。
|
|
203
|
+
* 集中成一处的理由 = 不变量只能有一个实现,否则迟早出现"某条路径忘了检查"(本仓已多次踩)。
|
|
204
|
+
*
|
|
205
|
+
* ## 判据(任一不成立即**拒绝**,不写)
|
|
206
|
+
*
|
|
207
|
+
* 1. **载体唯一**:`sessionId` 已安家在**另一条**轨迹 ⇒ 拒绝(一个会话不能绑两条轨迹)。
|
|
208
|
+
* ⚠️ 若它已安家在**本条**轨迹 ⇒ 幂等通过(重复 `use` 同一条轨迹是合法重入)。
|
|
209
|
+
* 2. **轨迹唯一**:本条轨迹已由**另一个** `sessionId` 承载 ⇒ **拒绝**,除非 `takeover`。
|
|
210
|
+
* `takeover` = 载体换代(`rebuild` / 崩溃后新建),由调用方**显式**声明;
|
|
211
|
+
* 绝不默认允许 —— 静默换代正是"一条轨迹长出两个载体"的成因。
|
|
212
|
+
*
|
|
213
|
+
* @param sessionsRoot `<CCC>/AGENT_SESSIONS`
|
|
214
|
+
* @param dirName 目标轨迹目录名
|
|
215
|
+
* @param decl 要写入的声明
|
|
216
|
+
* @param opts.takeover 允许顶掉本条轨迹现有的另一个载体
|
|
217
|
+
* @returns `{ ok: true }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
|
|
218
|
+
*/
|
|
219
|
+
export declare function claimBinding(sessionsRoot: string, dirName: string, decl: BindDeclaration, opts?: {
|
|
220
|
+
takeover?: boolean;
|
|
221
|
+
}): {
|
|
222
|
+
ok: true;
|
|
223
|
+
released?: string;
|
|
224
|
+
} | {
|
|
225
|
+
ok: false;
|
|
226
|
+
code: 'SESSION_TAKEN' | 'TRAJECTORY_OCCUPIED' | 'WRITE_FAILED';
|
|
227
|
+
detail: string;
|
|
228
|
+
};
|
|
@@ -1,167 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* trajectory/bound.ts —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
2
|
+
* trajectory/bound.ts — 「会话 ↔ 认知轨迹」绑定(**2026-10-20 重做:归属 = 轨迹自己的交代**)
|
|
3
|
+
*
|
|
4
|
+
* ## 判决 D172(owner 2026-10-20 原话,这是整个文件的设计依据)
|
|
5
|
+
*
|
|
6
|
+
* > 「**认知轨迹自己交代永远是最好的交代**」
|
|
7
|
+
* > 「绑定表是没有意义的,绑定关系必须**运行时按照规则探测**,绑定表本质就搞成**第二真相源**了」
|
|
8
|
+
* > 「依赖文件系统…每个认知轨迹目录内放一个 `current.dsh_session_bind`,**以这个为绑定真相源**」
|
|
9
|
+
* > 「文件里写的 sessionId **可能已经不存在了**,这个情况下就**视同没绑定**,就正确了」
|
|
10
|
+
*
|
|
11
|
+
* ## 旧形态为什么错(不是"性能不够",是"真相源放错地方")
|
|
12
|
+
*
|
|
13
|
+
* 旧实现用 **`.bindings.json` + 宿主存储域** 两张旁路索引记录"谁承载谁"。
|
|
14
|
+
* 它们是**旁路**:描述的对象(轨迹目录)与自己分离 ⇒ 一旦不一致就要写代码对账,
|
|
15
|
+
* 而**对账失败没有任何机械信号**(本轨反复踩的形状:症状与真因在读数上完全不像)。
|
|
16
|
+
*
|
|
17
|
+
* 新形态:归属写在**轨迹目录自己里面**(`current.dsh_session_bind`)。
|
|
18
|
+
* 判据不再是"表里有没有一条记录",而是"**这一刻谁真的在承载它**"。
|
|
19
|
+
*
|
|
20
|
+
* ## 🔴 三条不变量(前两条由**形态**保证,第三条由**探针**保证)
|
|
21
|
+
*
|
|
22
|
+
* | # | 不变量 | 保证方式 |
|
|
23
|
+
* |---|---|---|
|
|
24
|
+
* | 1 | 一条轨迹**至多一个**当前载体 | 它是**一个文件**,写入即整体原子覆盖 |
|
|
25
|
+
* | 2 | 一个载体**至多一条**轨迹 | 写入前扫全部轨迹的交代,该 id 已在别处 ⇒ **拒绝** |
|
|
26
|
+
* | 3 | **会话不存在 ⇒ 视同没绑定** | `isBindUsable(decl, alive)`(探针由调用方给) |
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ 旧的 `supersededAt`("只留一条"靠标记退掉)**整条退役**:标记可以忘,覆盖不会忘。
|
|
29
|
+
* 存量里带 `supersededAt` 的记录在迁移时**直接跳过**(它们本就不是"当前")。
|
|
30
|
+
*
|
|
31
|
+
* ## 零依赖与不抛
|
|
32
|
+
* 读函数**永不抛**;写函数返回布尔 / 结构化结果,由调用方决定如何呈现。
|
|
25
33
|
*/
|
|
26
|
-
import { type
|
|
27
|
-
|
|
28
|
-
interface SessionBoundRecord {
|
|
34
|
+
import { type BindDeclaration, writeBindDeclaration, BIND_FILENAME } from './bind-file.js';
|
|
35
|
+
/** 绑定记录形状(对外保持旧名,避免 10 个调用点全部改名) */
|
|
36
|
+
export interface SessionBoundRecord extends BindDeclaration {
|
|
37
|
+
/** 轨迹目录名(**锚**;不是 `S###` —— 编码只是展示) */
|
|
29
38
|
dirName: string;
|
|
39
|
+
/** `SESSION.md` 绝对路径(展示 / 定位用;**不作锚**) */
|
|
30
40
|
mdPath: string;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
at: number;
|
|
34
|
-
note?: string;
|
|
35
|
-
/** 🆕 ACC 以"用户开场白"形态建的载体(判决 D171)。**布尔事实**,不从 `note` 文案推。 */
|
|
36
|
-
createdAsUser?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* **(b) 已被取代标记**(S142 §0y,所有者 2026-09-18 裁「b 做」)。
|
|
39
|
-
*
|
|
40
|
-
* 语义:这条绑定**仍然存在**(沿革不丢、正向 `readLastBound` 仍能查到"我曾属于哪条轨迹"),
|
|
41
|
-
* 但**不再作为唤醒候选**({@link listBoundSessionIds} 过滤掉)——这正是"解绑"的机械形态。
|
|
42
|
-
*
|
|
43
|
-
* 为什么需要(实证):`.bindings.json` 只增不清——实测 S142 名下挂 **4 代**、S185 挂 **2 代**,
|
|
44
|
-
* 同一 cognitive trajectory 因此可以有**两条 live 会话**:各持一份完整上下文(内存翻倍)
|
|
45
|
-
* 且**都在写同一批文件**(双写者)。
|
|
46
|
-
*/
|
|
47
|
-
supersededAt?: number;
|
|
41
|
+
/** 展示码(`S###` 或自定义),**仅展示** */
|
|
42
|
+
sessionId2?: string;
|
|
48
43
|
}
|
|
49
|
-
/**
|
|
44
|
+
/** ⚠️ 兼容层:旧代码里 `rec.sessionId` 曾指"展示码",现在指 dsh 会话 id。见 `readLastBound` 的返回。 */
|
|
45
|
+
export type SessionBoundAction = 'activate' | 'switch' | 'create' | 'rebuild' | 'reconcile' | 'release';
|
|
46
|
+
/** @deprecated D172:绑定已迁到轨迹目录内;保留导出只为不打断外部 import */
|
|
50
47
|
export declare const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
export declare function setBindingStore(store: BindingStore | null): void;
|
|
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): {
|
|
48
|
+
/** @deprecated D172:存储域已退役;保留为空实现,装载期调用点无需改动 */
|
|
49
|
+
export declare function setBindingStore(_store: unknown): void;
|
|
50
|
+
/** @deprecated D172:迁移已由一次性脚本完成;保留恒零返回 */
|
|
51
|
+
export declare function migrateBindingsToDomain(_root: string): {
|
|
71
52
|
migrated: number;
|
|
72
53
|
skipped: number;
|
|
73
54
|
failed: number;
|
|
74
55
|
};
|
|
75
|
-
/**
|
|
76
|
-
export declare function bindingsPathFor(
|
|
56
|
+
/** @deprecated D172:`.bindings.json` 已退役;保留返回 null */
|
|
57
|
+
export declare function bindingsPathFor(_session: unknown): string | null;
|
|
77
58
|
/**
|
|
78
|
-
*
|
|
59
|
+
* 读**这个会话**当前承载哪条轨迹(正向:会话 → 轨迹)。
|
|
60
|
+
*
|
|
61
|
+
* 判据(D172):扫全部轨迹的自我交代,找 `sessionId === 本会话 id` 且**仍然 usable** 的那条。
|
|
62
|
+
*
|
|
63
|
+
* 🔴 为什么这里是"扫"而不是"查表":**归属是算出来的,不是记下来的**。
|
|
64
|
+
* 本 CCC 的轨迹数是**数十**量级,每次读盘只读每个目录里的一个小文件;比维护一份会漂的索引便宜。
|
|
79
65
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* 无绑定返回 null。
|
|
66
|
+
* ⚠️ **不验"会话是否还活着"**:本函数回答的是"这个**活着的**会话属于谁"——
|
|
67
|
+
* 调用方手里就拿着那个会话对象,它显然存在。`alive` 探针只在**反向**(轨迹 → 会话)才需要。
|
|
68
|
+
*
|
|
69
|
+
* @returns 记录(含 `dirName` / `mdPath` / dsh `sessionId`);无 ⇒ `null`
|
|
85
70
|
*/
|
|
86
71
|
export declare function readLastBound(session: unknown): SessionBoundRecord | null;
|
|
87
|
-
/**
|
|
72
|
+
/** 判定会话是否已有归属(reconcile 用:无归属才允许用标题升级) */
|
|
88
73
|
export declare function hasAnyBound(session: unknown): boolean;
|
|
89
74
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* 三级来源,逐级回退(R↓:为什么不用单一来源):
|
|
93
|
-
* 1. `readLastBound(session)` —— **持久 bound**,权威(重启存活、不受别的会话激活污染)
|
|
94
|
-
* 2. `getActiveSessionInfo(scope)` —— 本 dsh 会话的**内存**激活(按 scope 隔离)
|
|
95
|
-
* 3. `getLastActiveSessionInfo()` —— **全局**最近激活指针(无绑定的会话兼容回退)
|
|
96
|
-
*
|
|
97
|
-
* ⚠️ 为何单列成本函数(S142 §30.12.1 实测缺陷):唤醒注册表的 `createdBy` 原先直接取第 3 级
|
|
98
|
-
* 全局指针,而它记录的是"**进程内最近一次被激活的 cognitive trajectory**"——多会话并发时必然张冠李戴
|
|
99
|
-
* (实测:S142 登记唤醒被写成 `by=S060`)。`createdBy` 是 D60「以可审计替代入口栅」的**唯一凭证**,
|
|
100
|
-
* 失真即替代控制失效 ⇒ 必须以"调用方自己是谁"为准,全局指针只作最后兜底。
|
|
75
|
+
* **调用方自身**所属轨迹的标签(审计字段 `createdBy` 的唯一正解)。
|
|
101
76
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
77
|
+
* 三级来源(逐级回退,理由见旧实现注释与 S142 §30.12.1 实测):
|
|
78
|
+
* 1. `readLastBound(session)` —— 持久归属(重启存活)
|
|
79
|
+
* 2. `getActiveSessionInfo(scope)` —— 本会话的内存激活(按 scope 隔离)
|
|
80
|
+
* 3. `getLastActiveSessionInfo()` —— 全局最近激活指针(最后兜底)
|
|
105
81
|
*/
|
|
106
82
|
export declare function resolveSessionCognitiveTrajectoryLabel(session: unknown, scope: string): string;
|
|
107
83
|
/**
|
|
108
|
-
*
|
|
84
|
+
* 反向查:**此刻**承载某条轨迹的 dsh 会话 id(按 `at` 倒序,最新在前)。
|
|
109
85
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
86
|
+
* D172 起判据 = **轨迹自己的交代 + "那个会话还在"**:
|
|
87
|
+
* · 文件不存在 ⇒ 没绑定
|
|
88
|
+
* · 文件在但 `alive` 说不存在 ⇒ **视同没绑定**(owner 原话),**不算候选**
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ **`alive` 缺省时怎么判**:调用方没给探针 ⇒ 只能信文件(`usable`)。
|
|
91
|
+
* 那是"读不到宿主事实"的降级,**不是**"会话一定在"。需要严格判的一方请显式传探针
|
|
92
|
+
* (唤醒选人是这种:见 `wake/scheduler.ts`)。
|
|
113
93
|
*
|
|
114
94
|
* @param root CCC 根
|
|
115
|
-
* @param dirName
|
|
116
|
-
* @
|
|
95
|
+
* @param dirName 轨迹目录名
|
|
96
|
+
* @param alive 探针:该 dsh 会话 id 此刻是否真实存在
|
|
97
|
+
* @returns 会话 id 列表(本形态**至多一条**——一个文件只装一个 id);无 ⇒ `[]`
|
|
117
98
|
*/
|
|
118
|
-
export declare function listBoundSessionIds(root: string, dirName: string): string[];
|
|
99
|
+
export declare function listBoundSessionIds(root: string, dirName: string, alive?: (sessionId: string) => boolean): string[];
|
|
119
100
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* 把绑定到 `dirName` 的**其它**会话 id 标记为 `supersededAt` —— **保留记录**(沿革/正向查询不丢),
|
|
123
|
-
* 只切断"**唤醒还会找到它**"这条路。语义上即"解绑",且**不动那个会话本身**(它仍可被人工使用)。
|
|
101
|
+
* @deprecated D172:`supersededAt` 机制整条退役。
|
|
124
102
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* @param root CCC 根
|
|
130
|
-
* @param dirName 目标 cognitive trajectory 目录名
|
|
131
|
-
* @param keepIds 保留(不标记)的会话 id 集合——通常是"当前正在 use 的这条"
|
|
132
|
-
* @returns **实际被标记**的会话 id(升序稳定;供调用方如实报告,未标记任何一条 → `[]`)
|
|
103
|
+
* 旧形态里"同轨迹只留一条"靠在别的记录上**盖戳**;新形态里它是**一个文件**,
|
|
104
|
+
* 写入即覆盖 ⇒ **没有"其它条"可以盖戳**。保留本函数只为不打断既有调用点,
|
|
105
|
+
* 恒返回空数组("没有需要退休的")。
|
|
133
106
|
*/
|
|
134
|
-
export declare function supersedeOtherBindings(
|
|
107
|
+
export declare function supersedeOtherBindings(_root: string, _dirName: string, _keepIds: ReadonlySet<string>): string[];
|
|
135
108
|
/**
|
|
136
|
-
*
|
|
137
|
-
* 删除指向**已不存在**的 dsh 会话的绑定记录。
|
|
138
|
-
*
|
|
139
|
-
* 为什么需要:`.bindings.json` **只增不清**,而 DSH 会话目录会被
|
|
140
|
-
* `session-cleanup`(本仓既有机制)物理删除 ⇒ 表里必然残留**悬空 id**。
|
|
141
|
-
* 悬空记录本身无害,但它会**污染反向索引**(`listBoundSessionIds` 是唤醒选人的唯一入口),
|
|
142
|
-
* 让"这条轨迹到底还有没有可用会话"变得不可判。
|
|
143
|
-
*
|
|
144
|
-
* 🔴 **安全性由调用方的判据负责**(本函数是纯函数,不认识文件系统):
|
|
145
|
-
* `isMissing` 必须是 **fail-closed** 的(读不到 ⇒ 返回 false = "不当作缺失"),
|
|
146
|
-
* 见 `hasSessionLogById`。判据写错会**清掉整张表**,故此处刻意不提供默认实现。
|
|
109
|
+
* @deprecated D172:**悬空清理不再需要**。
|
|
147
110
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
111
|
+
* owner 原话:「文件里写的 sessionId 可能已经不存在了,这个情况下就**视同没绑定**,就正确了」。
|
|
112
|
+
* ⇒ 过期文件**自动失效**,不需要任何人去删它;保留它反而记录了"上一次由谁承载"。
|
|
113
|
+
* 保留本函数只为不打断既有调用点,恒返回空数组。
|
|
151
114
|
*/
|
|
152
|
-
export declare function pruneMissingBindings(
|
|
115
|
+
export declare function pruneMissingBindings(_root: string, _isMissing: (sessionId: string) => boolean): string[];
|
|
153
116
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* 绑定持久化尽力而为:无法定位 CCC 根 / 无会话 id / 写盘失败 → 返回 false,不阻断主流程。
|
|
117
|
+
* 把一条轨迹的当前载体写成 `sessionId`(**唯一写入口**,内部走 `claimBinding` 的两条不变量)。
|
|
157
118
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* `rebuild`/`reconcile`)**都自动满足"只留一条"**,调用方**不需要**(也**不应当**)自己算 selfId。
|
|
119
|
+
* 旧签名 `appendBound(session, action, rec)` 保持不变:`session` 提供 `header.id` / `header.cwd`,
|
|
120
|
+
* `rec.dirName` 指明写哪条轨迹的交代。
|
|
161
121
|
*
|
|
162
|
-
* @
|
|
163
|
-
* @param action 绑定动作
|
|
164
|
-
* @param rec 绑定记录(dirName + mdPath 必填;sessionId 可选展示码)
|
|
122
|
+
* @returns 写入成功 ⇒ true(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**)
|
|
165
123
|
*/
|
|
166
124
|
export declare function appendBound(session: unknown, action: SessionBoundAction, rec: {
|
|
167
125
|
dirName: string;
|
|
@@ -169,5 +127,32 @@ export declare function appendBound(session: unknown, action: SessionBoundAction
|
|
|
169
127
|
sessionId?: string;
|
|
170
128
|
note?: string;
|
|
171
129
|
createdAsUser?: boolean;
|
|
130
|
+
/** 允许顶掉该轨迹现有的另一个载体(rebuild / 崩溃后重建载体时**必须显式**声明) */
|
|
131
|
+
takeover?: boolean;
|
|
172
132
|
}): boolean;
|
|
173
|
-
|
|
133
|
+
/**
|
|
134
|
+
* 这条轨迹**是否曾经有过载体**(判"要不要允许自建")。
|
|
135
|
+
*
|
|
136
|
+
* 🔴 这是 owner 第 2 条的判据来源:**"绑定表空"≠"从未有过会话"**。
|
|
137
|
+
* 旧实现拿"未退的绑定条目为空"当"没绑定",于是一条轨迹的载体被退掉之后就
|
|
138
|
+
* 又长出一个新的(实测:S196 在同一分钟出现第二个载体)。
|
|
139
|
+
*
|
|
140
|
+
* 新判据 = **轨迹目录里有没有交代文件**(哪怕它指向的会话已不存在 ⇒ 有过,就不再自动建)。
|
|
141
|
+
*
|
|
142
|
+
* @returns `'never'`(从未有过 ⇒ 允许自建)/`'occupied'`(有过 ⇒ 禁止自建)/`'live'`(此刻有效)
|
|
143
|
+
*/
|
|
144
|
+
export declare function carrierState(root: string, dirName: string, alive: (sessionId: string) => boolean): {
|
|
145
|
+
state: 'live';
|
|
146
|
+
sessionId: string;
|
|
147
|
+
} | {
|
|
148
|
+
state: 'occupied';
|
|
149
|
+
sessionId: string;
|
|
150
|
+
} | {
|
|
151
|
+
state: 'never';
|
|
152
|
+
};
|
|
153
|
+
/** 某会话 id 现在安家在哪个轨迹(唯一性查询的对外门面) */
|
|
154
|
+
export declare function findTrajectoryOfSession(root: string, sessionId: string): string | null;
|
|
155
|
+
/** 可写声明(迁移脚本 / 测试直用) */
|
|
156
|
+
export { writeBindDeclaration };
|
|
157
|
+
export { BIND_FILENAME };
|
|
158
|
+
export type { BindDeclaration };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.77.0",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|