@shgroup/dsh-serenity-hooks 1.104.0 → 1.107.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/README.md +4 -3
- package/dsh.plugin.json +2 -3
- package/lib/{profile-Dpg3wYO5.js → bind-file-B0TuOP89.js} +1 -333
- package/lib/{ccc-B-h_A7uc.js → ccc-BmpE3cQz.js} +64 -12
- package/lib/{ccc-roots-BIdg0EyM.js → ccc-roots-DPMwBywc.js} +15 -3
- package/lib/{channels-BItMJUO8.js → channels-Dty_NIl_.js} +2 -146
- package/lib/contract-BiNzzd77.js +2197 -0
- package/lib/host/access.d.ts +30 -0
- package/lib/index.js +176 -6355
- package/lib/invariant.js +1 -2
- package/lib/kernel/ccc.d.ts +60 -13
- package/lib/kernel/invariant.d.ts +2 -2
- package/lib/message-source-BTIe2cVO.js +254 -0
- package/lib/observation-BWpkA865.js +210 -0
- package/lib/profile-BjBn_-be.js +336 -0
- package/lib/{route-CmEoKp9w.js → route-reyv8qcB.js} +2 -2
- package/lib/rules-BLkYTPUy.js +261 -0
- package/lib/scheduler-B0pB9mxI.js +3603 -0
- package/lib/seams/guards.d.ts +0 -4
- package/lib/{service-Cndtf7q4.js → service-Z3aSR1cn.js} +1 -1
- package/lib/service-xvjwsX3p.js +249 -0
- package/lib/{session-CaNqSHVB.js → session-BlgUdKH4.js} +55 -6
- package/lib/settings-DY4_DYYY.js +145 -0
- package/lib/subscriptions-CyrfNpcG.js +273 -0
- package/lib/tools/diag/service.d.ts +74 -24
- package/lib/tools/kit/service.d.ts +1 -1
- package/lib/tools/msm/service.d.ts +2 -2
- package/lib/trajectory/subscriptions.d.ts +170 -0
- package/package.json +2 -2
- package/lib/message-source-CKJPJCzc.js +0 -513
- package/lib/tools/diag/index.d.ts +0 -25
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
import { r as cccRootForCwd } from "./ccc-roots-BIdg0EyM.js";
|
|
2
|
-
import { g as resolveInside, h as readUtf8 } from "./ccc-B-h_A7uc.js";
|
|
3
|
-
import { D as getLastActiveSessionInfo, E as getActiveSessionInfo, I as sessionEvents, L as sessionsRoot, M as parseTrajectoryCode, _ as readBindDeclaration, g as listBindDeclarations, h as isBindUsable, m as claimBinding } from "./profile-Dpg3wYO5.js";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
//#region src/kernel/constants.ts
|
|
8
|
-
/** 常量(纯模块,零 DSH 依赖) */
|
|
9
|
-
/** 本包名 —— 上溯定位包根时的**身份核对**(同名目录家的邻居不会误命中) */
|
|
10
|
-
const PACKAGE_NAME = "@shgroup/dsh-serenity-hooks";
|
|
11
|
-
/** 上溯层数上限(源码态最深 `src/<域>/<子域>/` / 发布态 `lib/` ⇒ 8 层足够,且**保证有界**) */
|
|
12
|
-
const MAX_ROOT_ASCENT = 8;
|
|
13
|
-
/**
|
|
14
|
-
* 包根 = 含**本包** `package.json` 的目录。
|
|
15
|
-
*
|
|
16
|
-
* 🔴 为什么不写 `join(here, '..')`:本模块自身的位置**随目录重构变化**
|
|
17
|
-
* (曾 `src/`,现 `src/kernel/`),而发布态是从打包入口 `lib/index.js` 反推 ——
|
|
18
|
-
* 两种布局的深度不同,写死层数**必然在其中一个布局下静默错位**。
|
|
19
|
-
* 实测代价:`src/` → `src/kernel/` 之后,`join(here,'..')` 指到 `src/`(无 package.json)
|
|
20
|
-
* ⇒ `ACC_VERSION` 落进 catch 分支变 `0.0.0`,**没有任何门禁报警**。
|
|
21
|
-
*
|
|
22
|
-
* ⇒ 判据改成**内容**(找带本包名的 package.json)而不是**层数**。
|
|
23
|
-
*
|
|
24
|
-
* @param start 起点目录(缺省 = 本模块所在目录;测试可注入)
|
|
25
|
-
*/
|
|
26
|
-
function findPackageRoot(start) {
|
|
27
|
-
let dir = start ?? dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
for (let i = 0; i < MAX_ROOT_ASCENT; i += 1) {
|
|
29
|
-
try {
|
|
30
|
-
if (JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")).name === PACKAGE_NAME) return dir;
|
|
31
|
-
} catch {}
|
|
32
|
-
const parent = dirname(dir);
|
|
33
|
-
if (parent === dir) return null;
|
|
34
|
-
dir = parent;
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* ACC 版本:自动从 package.json 读取(单一真相源,消除与 CHANGELOG 的漂移)。
|
|
40
|
-
* 发布时只需改 package.json 的 version。
|
|
41
|
-
*/
|
|
42
|
-
const ACC_VERSION = (() => {
|
|
43
|
-
try {
|
|
44
|
-
const root = findPackageRoot();
|
|
45
|
-
if (root === null) return "0.0.0";
|
|
46
|
-
return JSON.parse(readFileSync(join(root, "package.json"), "utf-8")).version ?? "0.0.0";
|
|
47
|
-
} catch {
|
|
48
|
-
return "0.0.0";
|
|
49
|
-
}
|
|
50
|
-
})();
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/trajectory/policy/rules.ts
|
|
53
|
-
/**
|
|
54
|
-
* trajectory/policy/rules.ts — 轨迹约束面的**纯逻辑助手**(零 DSH 依赖,可独立单测)
|
|
55
|
-
*
|
|
56
|
-
* ## 2026-10-19 重做后的职责边界(R↓,别再混起来)
|
|
57
|
-
*
|
|
58
|
-
* | 模块 | 管什么 |
|
|
59
|
-
* |---|---|
|
|
60
|
-
* | `./profile.ts` | **判据 + 读盘**("这是不是 轨迹约束"、"约束面是什么") |
|
|
61
|
-
* | `./rules.ts`(本文件) | **从约束面派生出的纯函数**(白名单集合、提示词读取器、基础提示词文本) |
|
|
62
|
-
* | `../session-create.ts` | 会话创建/复用、提问、守门查询 |
|
|
63
|
-
*
|
|
64
|
-
* ⇒ 本文件**不再读 `serenity.json`**:`轨迹约束.roles` 整段已退役(owner 2026-10-19 重做)。
|
|
65
|
-
* 它只接收一个已解析好的 `TrajectoryConstraint`,因此**不涉及任何 IO**(除提示词文件,那是刻意懒读的)。
|
|
66
|
-
*
|
|
67
|
-
* ## S12(2026-10-19,轨迹中心重构)
|
|
68
|
-
*
|
|
69
|
-
* 原名 `role/轨迹约束/config.ts`。改名理由(框架 §6.3「角色不是一个概念,是一段历史」):
|
|
70
|
-
* 它派生的是**某条轨迹的约束面**,与"配置读取"无关(本文件零配置读取,见上一段)
|
|
71
|
-
* ⇒ `config` 这个名字描述的是**它曾经的样子**,`rules` 描述它**现在做的事**。
|
|
72
|
-
*/
|
|
73
|
-
/** 白名单推导 ────────────────────────────────────────────────────────────── */
|
|
74
|
-
/** 提示词来源提示串(`apply` 回执与面板共用;避免两处措辞漂) */
|
|
75
|
-
const TRAJECTORY_CONSTRAINT_HINT = "systemPromptFile hot-reloads by mtime; the rest of trajectory-constraint.json is re-read every turn";
|
|
76
|
-
/**
|
|
77
|
-
* 该轨迹会话**可见**的工具面(tools ∪ `msm`)。
|
|
78
|
-
*
|
|
79
|
-
* 🔴 **返回值 `null` 有专门含义(D181,owner 2026-10-20 裁「不写当然不收窄」)**:
|
|
80
|
-
*
|
|
81
|
-
* | `tools` | `msms` | 返回 | 语义 |
|
|
82
|
-
* |---|---|---|---|
|
|
83
|
-
* | 缺省 | 缺省 | **`null`** | **不收窄任何工具面**(这条轨迹要的是别的设定,如只写 `resident`) |
|
|
84
|
-
* | `[]` | 缺省 | `{'msm'?}` 空/仅 msm | **全隐藏**(除恒可用的 `skill`) |
|
|
85
|
-
* | `['a']` | … | `{'a', …}` | 只给这些 |
|
|
86
|
-
*
|
|
87
|
-
* ⛔ **调用方不许把 `null` 当空集处理** —— 那正是 S196 被锁死的形态(D179(b))。
|
|
88
|
-
* 判据是**字段在不在**,不是数组空不空。
|
|
89
|
-
*
|
|
90
|
-
* 🔴 `msm` 为什么自动加入:`msms` 非空意味着这个轨迹 **有 MSM 通道**,
|
|
91
|
-
* 而 MSM 的唯一合法调用入口是 `msm` 工具(单入口纪律)——不给它 `msm` 就等于
|
|
92
|
-
* 声明了一堆调不动的能力。⚠️ 这是**并集**,不是覆盖:`tools` 里显式写的照旧生效。
|
|
93
|
-
*
|
|
94
|
-
* @returns 白名单集合;**`null` = 不收窄**(调用方须显式判 `null`)
|
|
95
|
-
*/
|
|
96
|
-
function profileToolWhitelist(profile) {
|
|
97
|
-
if (!profile) return null;
|
|
98
|
-
if (profile.tools === void 0 && profile.msms === void 0) return null;
|
|
99
|
-
const out = /* @__PURE__ */ new Set();
|
|
100
|
-
for (const t of profile.tools ?? []) out.add(t);
|
|
101
|
-
if ((profile.msms?.length ?? 0) > 0) out.add("msm");
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
/** 该轨迹会话可 exec 的 MSM 白名单(`msm` 工具内部再过滤;独立于工具白名单) */
|
|
105
|
-
function profileMsmWhitelist(profile) {
|
|
106
|
-
return new Set(profile?.msms ?? []);
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* 该轨迹可**写**的路径前缀(owner 2026-10-20 令:「轨迹设定自己的写白名单」)。
|
|
110
|
-
*
|
|
111
|
-
* 🔴 与 `profileToolWhitelist` 共用同一条判据(D181/D183):**字段在不在,不是数组空不空**。
|
|
112
|
-
*
|
|
113
|
-
* | `writes` | 返回 | 语义 |
|
|
114
|
-
* |---|---|---|
|
|
115
|
-
* | 缺省 | **`null`** | 不收窄(写权限 = 路径守卫那几条,与今天一致) |
|
|
116
|
-
* | `[]` | `[]`(空数组) | **一个写都落不下**(显式意图) |
|
|
117
|
-
* | `['a/']` | `['a/']` | 写目标必须落在 `a/` 之内 |
|
|
118
|
-
*
|
|
119
|
-
* ⛔ **调用方不许把 `null` 当 `[]`** —— 那会把"我没想到这件事"读成"我不许写任何东西",
|
|
120
|
-
* 正是 S196 被静默锁死的形态(D179(b))。
|
|
121
|
-
*
|
|
122
|
-
* ⚠️ 返回的是**原始条目**(相对 CCC 根或绝对路径都可),解析成绝对路径由调用方按自己的 root 做 ——
|
|
123
|
-
* 本模块不持有 root(判据在 `seams/guards.ts`)。
|
|
124
|
-
*/
|
|
125
|
-
function profileWriteWhitelist(profile) {
|
|
126
|
-
if (!profile) return null;
|
|
127
|
-
if (profile.writes === void 0) return null;
|
|
128
|
-
return profile.writes;
|
|
129
|
-
}
|
|
130
|
-
/** 轨迹纪律参与项(`profile` 缺省已在 `readTrajectoryConstraintAt` 归一为全 true) */
|
|
131
|
-
function constraintCognitiveTrajectory(profile) {
|
|
132
|
-
return {
|
|
133
|
-
session: profile?.cognitiveTrajectory.session === true,
|
|
134
|
-
keeper: profile?.cognitiveTrajectory.keeper === true,
|
|
135
|
-
rebuild: profile?.cognitiveTrajectory.rebuild === true
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* 解析约束面声明的**系统提示词全文**(一次性读取;`create` / `validate` / `list` 用)。
|
|
140
|
-
*
|
|
141
|
-
* 顺序:`systemPromptFile`(**推荐**:超长提示词在 JSON 里内嵌不可读)→ 内嵌 `systemPrompt` → 空串。
|
|
142
|
-
* 文件缺失/逃逸 ⇒ **抛错**(调用方 catch 降级;`validate` 据此报 issue)。
|
|
143
|
-
*
|
|
144
|
-
* ⚠️ 热路径**不用**它(那会用每次工具调用去读盘)——热路径用 `createProfilePromptReader`。
|
|
145
|
-
*/
|
|
146
|
-
function resolveConstraintSystemPrompt(root, profile) {
|
|
147
|
-
if (!profile) return "";
|
|
148
|
-
const file = profile.systemPromptFile?.trim();
|
|
149
|
-
if (file) {
|
|
150
|
-
const abs = resolveInside(root, file);
|
|
151
|
-
if (!existsSync(abs)) throw new Error(`轨迹约束 "${profile.code}": systemPromptFile "${file}" not found (resolved: ${abs})`);
|
|
152
|
-
return readUtf8(abs).trim();
|
|
153
|
-
}
|
|
154
|
-
return profile.systemPrompt ?? "";
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* 提示词来源(`list`/`validate` 展示用)。
|
|
158
|
-
*
|
|
159
|
-
* 🔴 **2026-10-19 修(T4 的漏网面)**:v1.72.0 起提示词有两代来源——**新形态**
|
|
160
|
-
* 是轨迹目录里的 `PROMPT.md`(`trajectory/prompt.ts`,**优先**),旧形态才是
|
|
161
|
-
* `systemPromptFile` / `systemPrompt`。本函数原先只认旧形态 ⇒ 两条只写了 `PROMPT.md`
|
|
162
|
-
* 的轨迹在 `container_admin config list` 里恒显示 `promptSource: 'none'`。
|
|
163
|
-
*
|
|
164
|
-
* 那是**读数器过时**,不是配置缺失:`validate` 侧早已按两条来源判(见
|
|
165
|
-
* `trajectory-constraint-admin.ts` 的 `hasTrajectoryPromptOrLegacy`),而 `list` 侧漏了。
|
|
166
|
-
* ⇒ 返 **四种**值,把"有章程但来源是新形态"这一格单独说出来。
|
|
167
|
-
*
|
|
168
|
-
* @param hasPromptFile 该轨迹目录下**是否真有** `PROMPT.md`(调用方查,本函数不做 IO)
|
|
169
|
-
*/
|
|
170
|
-
function constraintSystemPromptSource(profile, hasPromptFile = false) {
|
|
171
|
-
if (hasPromptFile) return "trajectory-file";
|
|
172
|
-
if (profile?.systemPromptFile?.trim()) return "file";
|
|
173
|
-
if (profile?.systemPrompt?.trim()) return "inline";
|
|
174
|
-
return "none";
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* 提示词**读取器**(长期热更新;改文件即生效,无需重启)。
|
|
178
|
-
*
|
|
179
|
-
* 为什么返回函数而不是字符串(R↓,实证代价):旧实现在创建时把提示词**快照**进系统提示词段闭包
|
|
180
|
-
* ⇒ CCC 改了提示词文件必须重启 dsh web 才生效(实测:改完 18KB 提示词,live agent 仍用旧文本)。
|
|
181
|
-
* 现形态由系统提示词段的 `text()` **每轮调用**本读取器:
|
|
182
|
-
* - `systemPromptFile`:按 **mtime + size** 缓存重读(命中缓存零 IO;未变不解析)
|
|
183
|
-
* - 内嵌 `systemPrompt`:静态(改 JSON 仍需重载配置,这与文件热更的差别是**必然**的 —— 文件有 mtime,JSON 没有)
|
|
184
|
-
* - 读取失败:返回**上次成功内容**(⛔ 绝不返回空——空提示词会让角色失去人格与纪律),并按标签去重告警一次
|
|
185
|
-
*
|
|
186
|
-
* @param root CCC 根
|
|
187
|
-
* @param profile 约束面
|
|
188
|
-
*/
|
|
189
|
-
function createProfilePromptReader(root, profile) {
|
|
190
|
-
const file = profile?.systemPromptFile?.trim();
|
|
191
|
-
if (!file) {
|
|
192
|
-
const inline = profile?.systemPrompt ?? "";
|
|
193
|
-
return () => inline;
|
|
194
|
-
}
|
|
195
|
-
let abs = null;
|
|
196
|
-
try {
|
|
197
|
-
abs = resolveInside(root, file);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
console.warn(`[serenity-hooks] ✗ 轨迹约束提示词路径非法(${file}): ${String(err?.message ?? err)}`);
|
|
200
|
-
}
|
|
201
|
-
if (abs === null) return () => "";
|
|
202
|
-
const absPath = abs;
|
|
203
|
-
let cachedMtime = -1;
|
|
204
|
-
let cachedSize = -1;
|
|
205
|
-
let cachedText = "";
|
|
206
|
-
let warned = false;
|
|
207
|
-
return () => {
|
|
208
|
-
try {
|
|
209
|
-
const st = statSync(absPath);
|
|
210
|
-
if (st.mtimeMs === cachedMtime && st.size === cachedSize) return cachedText;
|
|
211
|
-
const text = readUtf8(absPath).trim();
|
|
212
|
-
const isReload = cachedMtime !== -1;
|
|
213
|
-
cachedMtime = st.mtimeMs;
|
|
214
|
-
cachedSize = st.size;
|
|
215
|
-
cachedText = text;
|
|
216
|
-
warned = false;
|
|
217
|
-
if (isReload) console.info(`[serenity-hooks] ↻ 轨迹约束提示词已热重载(${file},${text.length} 字符)`);
|
|
218
|
-
return text;
|
|
219
|
-
} catch (err) {
|
|
220
|
-
if (!warned) {
|
|
221
|
-
warned = true;
|
|
222
|
-
console.warn(`[serenity-hooks] ✗ 轨迹约束提示词读取失败(${file})——沿用上次成功内容: ${String(err?.message ?? err)}`);
|
|
223
|
-
}
|
|
224
|
-
return cachedText;
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* 轨迹约束 基础提示词(dsp 只给这部分;CCC 的 `systemPrompt` 段由调用方拼接)。
|
|
230
|
-
*
|
|
231
|
-
* ## 🔴 为什么**不再枚举**清单(owner 2026-10-19 令)
|
|
232
|
-
*
|
|
233
|
-
* 旧版在这里抄了两份清单(`MSMs: a, b, c` / `Tools: read, grep, …`)。那是**第二真相源**,
|
|
234
|
-
* 而真相源本来就在模型自己手里:
|
|
235
|
-
* · **工具清单** = 这一轮实际注册给它的 tool schema —— 模型已经看得见,抄一遍只会漂
|
|
236
|
-
* · **MSM 清单** = `msm()` 无参调用返回的目录 —— 已按白名单过滤,且**每次实时读配置**
|
|
237
|
-
*
|
|
238
|
-
* ⚠️ **漂移不是理论风险,是已发生的缺陷**(2026-10-19):角色配置早就长到 8 个工具,
|
|
239
|
-
* 而这个函数是从**创建时冻结的快照**求值的 ⇒ 提示词里一直写着旧的 4 个
|
|
240
|
-
* ⇒ 模型照着提示词办事,**它自己以为没有的能力就不去调** ⇒ 症状是"工具用不了",
|
|
241
|
-
* 而守卫那边(每调用实时读配置)其实是放行的。两个真相源打架,输的总是提示词。
|
|
242
|
-
*
|
|
243
|
-
* ⇒ 现在只声明**边界与发现路径**,不抄内容:清单在哪,怎么取,越界会怎样。
|
|
244
|
-
*
|
|
245
|
-
* @param code 轨迹编码(轨迹约束的身份 —— 不是"角色名",那个概念已退役)
|
|
246
|
-
*/
|
|
247
|
-
function buildTrajectoryBasePrompt(code) {
|
|
248
|
-
return [
|
|
249
|
-
"=== Serenity 轨迹约束 ===",
|
|
250
|
-
`Cognitive trajectory: ${code} (this is the trajectory you ARE, not a role you play)`,
|
|
251
|
-
"Your capability boundary is enforced mechanically by this CCC — anything outside it is rejected at the call site, not by your judgment.",
|
|
252
|
-
"Discover your surface, do not memorize it (both are generated from the live config on every turn):",
|
|
253
|
-
" · Your tools are exactly the tool schemas you see in this session — if a tool is not in your schema, it is not available.",
|
|
254
|
-
" · Your MSMs are listed by calling msm() with no arguments — that directory is already filtered to this trajectory.",
|
|
255
|
-
" · Usage for any MSM: msm(\"<name>\", [], inspect=true) or pass \"--help\" as the first argument.",
|
|
256
|
-
"Do not infer unavailability from anything you remember or from an earlier list: this surface changes when the CCC configuration changes, and it takes effect without restarting.",
|
|
257
|
-
"",
|
|
258
|
-
"---",
|
|
259
|
-
""
|
|
260
|
-
].join("\n");
|
|
261
|
-
}
|
|
262
|
-
//#endregion
|
|
263
|
-
//#region src/trajectory/bound.ts
|
|
264
|
-
/**
|
|
265
|
-
* trajectory/bound.ts — 「会话 ↔ 认知轨迹」绑定(**2026-10-20 重做:归属 = 轨迹自己的交代**)
|
|
266
|
-
*
|
|
267
|
-
* ## 判决 D172(owner 2026-10-20 原话,这是整个文件的设计依据)
|
|
268
|
-
*
|
|
269
|
-
* > 「**认知轨迹自己交代永远是最好的交代**」
|
|
270
|
-
* > 「绑定表是没有意义的,绑定关系必须**运行时按照规则探测**,绑定表本质就搞成**第二真相源**了」
|
|
271
|
-
* > 「依赖文件系统…每个认知轨迹目录内放一个 `current.dsh_session_bind`,**以这个为绑定真相源**」
|
|
272
|
-
* > 「文件里写的 sessionId **可能已经不存在了**,这个情况下就**视同没绑定**,就正确了」
|
|
273
|
-
*
|
|
274
|
-
* ## 旧形态为什么错(不是"性能不够",是"真相源放错地方")
|
|
275
|
-
*
|
|
276
|
-
* 旧实现用 **`.bindings.json` + 宿主存储域** 两张旁路索引记录"谁承载谁"。
|
|
277
|
-
* 它们是**旁路**:描述的对象(轨迹目录)与自己分离 ⇒ 一旦不一致就要写代码对账,
|
|
278
|
-
* 而**对账失败没有任何机械信号**(本轨反复踩的形状:症状与真因在读数上完全不像)。
|
|
279
|
-
*
|
|
280
|
-
* 新形态:归属写在**轨迹目录自己里面**(`current.dsh_session_bind`)。
|
|
281
|
-
* 判据不再是"表里有没有一条记录",而是"**这一刻谁真的在承载它**"。
|
|
282
|
-
*
|
|
283
|
-
* ## 🔴 三条不变量(前两条由**形态**保证,第三条由**探针**保证)
|
|
284
|
-
*
|
|
285
|
-
* | # | 不变量 | 保证方式 |
|
|
286
|
-
* |---|---|---|
|
|
287
|
-
* | 1 | 一条轨迹**至多一个**当前载体 | 它是**一个文件**,写入即整体原子覆盖 |
|
|
288
|
-
* | 2 | 一个载体**至多一条**轨迹 | 写入前扫全部轨迹的交代,该 id 已在别处 ⇒ **拒绝** |
|
|
289
|
-
* | 3 | **会话不存在 ⇒ 视同没绑定** | `isBindUsable(decl, alive)`(探针由调用方给) |
|
|
290
|
-
*
|
|
291
|
-
* ⚠️ 旧的 `supersededAt`("只留一条"靠标记退掉)**整条退役**:标记可以忘,覆盖不会忘。
|
|
292
|
-
* 存量里带 `supersededAt` 的记录在迁移时**直接跳过**(它们本就不是"当前")。
|
|
293
|
-
*
|
|
294
|
-
* ## 零依赖与不抛
|
|
295
|
-
* 读函数**永不抛**;写函数返回布尔 / 结构化结果,由调用方决定如何呈现。
|
|
296
|
-
*/
|
|
297
|
-
/**
|
|
298
|
-
* 从 dsh 会话对象取 `header.id` / `header.cwd`。
|
|
299
|
-
* @returns 取不到 ⇒ `null`
|
|
300
|
-
*/
|
|
301
|
-
function sessionHeader(session) {
|
|
302
|
-
const header = session?.header;
|
|
303
|
-
if (!header || typeof header !== "object") return null;
|
|
304
|
-
return header;
|
|
305
|
-
}
|
|
306
|
-
/** 由会话对象定位 CCC 根(上溯 `.serenity`);定位不到 ⇒ `null` */
|
|
307
|
-
function rootOf(session) {
|
|
308
|
-
const cwd = sessionHeader(session)?.cwd;
|
|
309
|
-
if (typeof cwd !== "string") return null;
|
|
310
|
-
return cccRootForCwd(cwd);
|
|
311
|
-
}
|
|
312
|
-
/** 目录名 → 轨迹标签(固定位置编码;非本工具建的目录回退整名——编码无关 U4) */
|
|
313
|
-
function cognitiveTrajectoryLabel(dirName) {
|
|
314
|
-
return parseTrajectoryCode(dirName) ?? dirName;
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* 把一条「轨迹自我交代」投影成对外的 `SessionBoundRecord`。
|
|
318
|
-
*
|
|
319
|
-
* 🔴 **`sessionId` 的语义在这里对齐**:旧记录里 `sessionId` 是**展示码**(`S###`),
|
|
320
|
-
* 新绑定文件里是**dsh 会话 id**(它是硬锚)。历史调用方有两类读法,故两个字段都给:
|
|
321
|
-
* · `sessionId` → **dsh 会话 id**(新语义,硬锚)
|
|
322
|
-
* · `sessionId2` → 展示码(由轨迹目录名派生;旧语义)
|
|
323
|
-
* 这样"我要这个会话的 id"与"我要显示成 S###"各自有确定字段,不再靠猜。
|
|
324
|
-
*/
|
|
325
|
-
function toRecord(dirName, decl, root) {
|
|
326
|
-
return {
|
|
327
|
-
dirName,
|
|
328
|
-
mdPath: join(root, "AGENT_SESSIONS", dirName, "SESSION.md"),
|
|
329
|
-
sessionId: decl.sessionId,
|
|
330
|
-
sessionId2: cognitiveTrajectoryLabel(dirName),
|
|
331
|
-
at: decl.at,
|
|
332
|
-
action: decl.action,
|
|
333
|
-
...decl.note !== void 0 ? { note: decl.note } : {},
|
|
334
|
-
...decl.createdAsUser === true ? { createdAsUser: true } : {}
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
/** @deprecated D172:存储域已退役;保留为空实现,装载期调用点无需改动 */
|
|
338
|
-
function setBindingStore(_store) {}
|
|
339
|
-
/**
|
|
340
|
-
* 读**这个会话**当前承载哪条轨迹(正向:会话 → 轨迹)。
|
|
341
|
-
*
|
|
342
|
-
* 判据(D172):扫全部轨迹的自我交代,找 `sessionId === 本会话 id` 且**仍然 usable** 的那条。
|
|
343
|
-
*
|
|
344
|
-
* 🔴 为什么这里是"扫"而不是"查表":**归属是算出来的,不是记下来的**。
|
|
345
|
-
* 本 CCC 的轨迹数是**数十**量级,每次读盘只读每个目录里的一个小文件;比维护一份会漂的索引便宜。
|
|
346
|
-
*
|
|
347
|
-
* ⚠️ **不验"会话是否还活着"**:本函数回答的是"这个**活着的**会话属于谁"——
|
|
348
|
-
* 调用方手里就拿着那个会话对象,它显然存在。`alive` 探针只在**反向**(轨迹 → 会话)才需要。
|
|
349
|
-
*
|
|
350
|
-
* @returns 记录(含 `dirName` / `mdPath` / dsh `sessionId`);无 ⇒ `null`
|
|
351
|
-
*/
|
|
352
|
-
function readLastBound(session) {
|
|
353
|
-
const id = sessionHeader(session)?.id;
|
|
354
|
-
const root = rootOf(session);
|
|
355
|
-
if (typeof id !== "string" || id === "" || root === null) return readLegacyBoundFromEvents(session);
|
|
356
|
-
for (const { dirName, decl } of listBindDeclarations(sessionsRoot(root))) if (decl.sessionId === id) return toRecord(dirName, decl, root);
|
|
357
|
-
return readLegacyBoundFromEvents(session);
|
|
358
|
-
}
|
|
359
|
-
/** 旧形态事件类型(v1.29.1~v1.30.5 写入会话日志;只读兼容,**永不写**) */
|
|
360
|
-
const SESSION_BOUND_EVENT = "serenity/bound";
|
|
361
|
-
/** 从会话日志扫最后一条 `serenity/bound`(兼容存量;坏记录跳过) */
|
|
362
|
-
function readLegacyBoundFromEvents(session) {
|
|
363
|
-
const events = sessionEvents(session);
|
|
364
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
365
|
-
const ev = events[i];
|
|
366
|
-
if (!ev || ev.type !== SESSION_BOUND_EVENT) continue;
|
|
367
|
-
const d = ev.data;
|
|
368
|
-
if (!d || typeof d.dirName !== "string" || typeof d.mdPath !== "string") continue;
|
|
369
|
-
return {
|
|
370
|
-
dirName: d.dirName,
|
|
371
|
-
mdPath: d.mdPath,
|
|
372
|
-
sessionId: typeof d.sessionId === "string" ? d.sessionId : "",
|
|
373
|
-
at: typeof d.at === "number" ? d.at : 0,
|
|
374
|
-
action: typeof d.action === "string" ? d.action : "activate"
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* **调用方自身**所属轨迹的标签(审计字段 `createdBy` 的唯一正解)。
|
|
381
|
-
*
|
|
382
|
-
* 三级来源(逐级回退,理由见旧实现注释与 S142 §30.12.1 实测):
|
|
383
|
-
* 1. `readLastBound(session)` —— 持久归属(重启存活)
|
|
384
|
-
* 2. `getActiveSessionInfo(scope)` —— 本会话的内存激活(按 scope 隔离)
|
|
385
|
-
* 3. `getLastActiveSessionInfo()` —— 全局最近激活指针(最后兜底)
|
|
386
|
-
*/
|
|
387
|
-
function resolveSessionCognitiveTrajectoryLabel(session, scope) {
|
|
388
|
-
const bound = session ? readLastBound(session) : null;
|
|
389
|
-
if (bound?.dirName) return cognitiveTrajectoryLabel(bound.dirName);
|
|
390
|
-
const scoped = scope !== "" ? getActiveSessionInfo(scope) : null;
|
|
391
|
-
if (scoped) return scoped.sessionId;
|
|
392
|
-
return getLastActiveSessionInfo()?.sessionId ?? "";
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* 反向查:**此刻**承载某条轨迹的 dsh 会话 id(按 `at` 倒序,最新在前)。
|
|
396
|
-
*
|
|
397
|
-
* D172 起判据 = **轨迹自己的交代 + "那个会话还在"**:
|
|
398
|
-
* · 文件不存在 ⇒ 没绑定
|
|
399
|
-
* · 文件在但 `alive` 说不存在 ⇒ **视同没绑定**(owner 原话),**不算候选**
|
|
400
|
-
*
|
|
401
|
-
* ⚠️ **`alive` 缺省时怎么判**:调用方没给探针 ⇒ 只能信文件(`usable`)。
|
|
402
|
-
* 那是"读不到宿主事实"的降级,**不是**"会话一定在"。需要严格判的一方请显式传探针
|
|
403
|
-
* (唤醒选人是这种:见 `wake/scheduler.ts`)。
|
|
404
|
-
*
|
|
405
|
-
* 🔴 **2026-10-20(owner 裁决):生产调用点必须传探针,两个调用点都已接上。**
|
|
406
|
-
* 缺省分支**保留**只为两件事:① 既有单测的向后兼容;② 诊断面"只读文件"的显式用法。
|
|
407
|
-
* ⛔ 不要新增不传探针的生产调用点 —— 那等于把"归档会话算活"的漏洞再开一次
|
|
408
|
-
* (本仓同族已踩:`carrierState` + CRO 快照两处都漏传过)。
|
|
409
|
-
* 生产一律用 `trajectory/alive.ts` 的 `hostAliveProbe(ctx)`(判据 = owner 定的
|
|
410
|
-
* 「① 进程内 loaded ∨ ② 日志在且未被归档」)。
|
|
411
|
-
*
|
|
412
|
-
* @param root CCC 根
|
|
413
|
-
* @param dirName 轨迹目录名
|
|
414
|
-
* @param alive 探针:该 dsh 会话 id 此刻是否真实存在
|
|
415
|
-
* @returns 会话 id 列表(本形态**至多一条**——一个文件只装一个 id);无 ⇒ `[]`
|
|
416
|
-
*/
|
|
417
|
-
function listBoundSessionIds(root, dirName, alive) {
|
|
418
|
-
const decl = readBindDeclaration(join(sessionsRoot(root), dirName));
|
|
419
|
-
if (decl === null) return [];
|
|
420
|
-
return (alive ? isBindUsable(decl, alive).state === "usable" : isBindUsable(decl, () => true).state === "usable") ? [decl.sessionId] : [];
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* 把一条轨迹的当前载体写成 `sessionId`(**唯一写入口**,内部走 `claimBinding` 的两条不变量)。
|
|
424
|
-
*
|
|
425
|
-
* 旧签名 `appendBound(session, action, rec)` 保持不变:`session` 提供 `header.id` / `header.cwd`,
|
|
426
|
-
* `rec.dirName` 指明写哪条轨迹的交代。
|
|
427
|
-
*
|
|
428
|
-
* @returns 写入成功 ⇒ `{ ok: true, replaced?, released? }`(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**);
|
|
429
|
-
* 失败 ⇒ `{ ok: false, code, detail }`,**`code` 指名是哪一条守卫拒的**
|
|
430
|
-
*
|
|
431
|
-
* 🔴 **为什么从 `boolean` 改成分级结果(2026-10-20,一条 skiff 轨迹实测逼出)**:
|
|
432
|
-
* 本函数有**两条完全不同的失败路径** —— ① header 守卫(id / root 取不到)② `claimBinding` 拒绝
|
|
433
|
-
* (`SESSION_TAKEN` / `TRAJECTORY_OCCUPIED` / `WRITE_FAILED`)。日志侧早已分开(D186),
|
|
434
|
-
* **但回执侧只有一句 header 文案** ⇒ 调用方被指向错误的方向(那位实测者第一轮就照文案去找 header,
|
|
435
|
-
* 真因是槽位被占)。⇒ 结果必须**带码**,让回执能说出是哪一半。
|
|
436
|
-
*/
|
|
437
|
-
function appendBound(session, action, rec) {
|
|
438
|
-
const id = sessionHeader(session)?.id;
|
|
439
|
-
const root = rootOf(session);
|
|
440
|
-
if (typeof id !== "string" || id === "" || root === null) {
|
|
441
|
-
const detail = `会话对象取不到可用的 header(id=${typeof id === "string" && id !== "" ? id : "(空/非字符串)"},root=${root ?? "null(header.cwd 缺失或上溯不到 .serenity)"}) ⇒ 轨迹「${rec.dirName}」的 current.dsh_session_bind **未被改写**。 排查:该会话的 header.cwd 是否指向某个含 .serenity 的目录; 无 cwd 的会话(如某些程序性创建的载体)**不会**写绑定。`;
|
|
442
|
-
console.warn(`[serenity-hooks] ✗ 绑定未写入(SESSION_HEADER_UNRESOLVED):${detail}`);
|
|
443
|
-
return {
|
|
444
|
-
ok: false,
|
|
445
|
-
code: "SESSION_HEADER_UNRESOLVED",
|
|
446
|
-
detail
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
const header = sessionHeader(session);
|
|
450
|
-
if (header?.origin === "subagent") {
|
|
451
|
-
const detail = `会话 ${id} 是 **subagent 子会话**(header.origin="subagent"${header.parentSession ? `,parentSession=${header.parentSession}` : ""}),不能作为轨迹「${rec.dirName}」的载体 ⇒ current.dsh_session_bind **未被改写**。\n 原因:子会话的消息投递权归宿主 subagent routing,ACC 唤醒/投递会被宿主以 \`session/agent-busy\`(is owned by subagent routing)拒绝 ⇒ 该轨迹**每一条**到点唤醒都送不出去。
|
|
452
|
-
正确做法:在**根会话**(不是子会话)里执行 cognitive_trajectory use。`;
|
|
453
|
-
console.warn(`[serenity-hooks] ✗ 绑定未写入(SESSION_IS_SUBAGENT):${detail}`);
|
|
454
|
-
return {
|
|
455
|
-
ok: false,
|
|
456
|
-
code: "SESSION_IS_SUBAGENT",
|
|
457
|
-
detail
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
const result = claimBinding(sessionsRoot(root), rec.dirName, {
|
|
461
|
-
sessionId: id,
|
|
462
|
-
at: Date.now(),
|
|
463
|
-
action,
|
|
464
|
-
...rec.note !== void 0 ? { note: rec.note } : {},
|
|
465
|
-
...rec.createdAsUser === true ? { createdAsUser: true } : {}
|
|
466
|
-
}, {
|
|
467
|
-
takeover: rec.takeover === true,
|
|
468
|
-
...rec.alive !== void 0 ? { alive: rec.alive } : {}
|
|
469
|
-
});
|
|
470
|
-
if (!result.ok) {
|
|
471
|
-
console.warn(`[serenity-hooks] ✗ 绑定未写入(${result.code}):${result.detail}`);
|
|
472
|
-
return {
|
|
473
|
-
ok: false,
|
|
474
|
-
code: result.code,
|
|
475
|
-
detail: result.detail
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
return {
|
|
479
|
-
ok: true,
|
|
480
|
-
...result.replaced === void 0 ? {} : { replaced: result.replaced },
|
|
481
|
-
...result.released === void 0 ? {} : { released: result.released }
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* 这条轨迹**是否曾经有过载体**(判"要不要允许自建")。
|
|
486
|
-
*
|
|
487
|
-
* 🔴 这是 owner 第 2 条的判据来源:**"绑定表空"≠"从未有过会话"**。
|
|
488
|
-
* 旧实现拿"未退的绑定条目为空"当"没绑定",于是一条轨迹的载体被退掉之后就
|
|
489
|
-
* 又长出一个新的(实测:S196 在同一分钟出现第二个载体)。
|
|
490
|
-
*
|
|
491
|
-
* 新判据 = **轨迹目录里有没有交代文件**(哪怕它指向的会话已不存在 ⇒ 有过,就不再自动建)。
|
|
492
|
-
*
|
|
493
|
-
* @returns `'never'`(从未有过 ⇒ 允许自建)/`'occupied'`(有过 ⇒ 禁止自建)/`'live'`(此刻有效)
|
|
494
|
-
*/
|
|
495
|
-
function carrierState(root, dirName, alive) {
|
|
496
|
-
const decl = readBindDeclaration(join(sessionsRoot(root), dirName));
|
|
497
|
-
if (decl === null) return { state: "never" };
|
|
498
|
-
return isBindUsable(decl, alive).state === "usable" ? {
|
|
499
|
-
state: "live",
|
|
500
|
-
sessionId: decl.sessionId
|
|
501
|
-
} : {
|
|
502
|
-
state: "occupied",
|
|
503
|
-
sessionId: decl.sessionId
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
//#endregion
|
|
507
|
-
//#region src/kernel/message-source.ts
|
|
508
|
-
/** ACC 注入消息的 kind —— 唤醒 / 打回 / keeper 提醒 / 锚定轮 / rebuild 交接等一律同值。 */
|
|
509
|
-
const ACC_MESSAGE_KIND = "plugin:dsh-serenity-hooks";
|
|
510
|
-
/** ACC 注入消息的统一来源值(各缝共用;需要 `form` 时按 `ContextFormed` 另加字段)。 */
|
|
511
|
-
const PLUGIN_SOURCE = { kind: ACC_MESSAGE_KIND };
|
|
512
|
-
//#endregion
|
|
513
|
-
export { resolveConstraintSystemPrompt as _, listBoundSessionIds as a, setBindingStore as c, constraintCognitiveTrajectory as d, constraintSystemPromptSource as f, profileWriteWhitelist as g, profileToolWhitelist as h, carrierState as i, TRAJECTORY_CONSTRAINT_HINT as l, profileMsmWhitelist as m, PLUGIN_SOURCE as n, readLastBound as o, createProfilePromptReader as p, appendBound as r, resolveSessionCognitiveTrajectoryLabel as s, ACC_MESSAGE_KIND as t, buildTrajectoryBasePrompt as u, ACC_VERSION as v, findPackageRoot as y };
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tools/diag/index.ts(工具面)— ACC 负责人专用的运行态诊断(v1.33,S142 §32.11)
|
|
3
|
-
*
|
|
4
|
-
* 用户裁决(2026-09-14):「不要 diag-live 了;或者变成一个隐藏 tool **只对 home-serenity 可见**,
|
|
5
|
-
* 做成我们专属的运行时诊断工具」。⇒ 合并原 `diag`(脚本侧条件链)+ `diag-live`(进程内运行态),
|
|
6
|
-
* 一次调用出全报告,并**从 CCC 工具面默认移除**。
|
|
7
|
-
*
|
|
8
|
-
* 可见性不在本文件判定(与 im-bridge 同一套机制):`seams/guards.ts`
|
|
9
|
-
* `syncExclusiveToolsVisibility` 在会话就绪 / pre-step 时按 CCC 配置
|
|
10
|
-
* `exclusiveTools` 决定是否 `agent.ctx.tools.restrict({ deny: ['acc-diag'] })`——
|
|
11
|
-
* **未声明的 CCC 看不到它**(不是"看得到但被拒")。
|
|
12
|
-
*
|
|
13
|
-
* 为什么必须是独立工具而不是 `container_admin` 的一个域(§32.11 ②):
|
|
14
|
-
* `tools.restrict` 是**工具级** deny,没有域级粒度——放进 container_admin 就等于
|
|
15
|
-
* 要隐藏它必须连 role/msm/config 一起隐藏。
|
|
16
|
-
*/
|
|
17
|
-
import type { Context } from 'cordis';
|
|
18
|
-
/**
|
|
19
|
-
* 运行态诊断工具(`acc-diag`)。**无动作参数**:一次调用即全报告
|
|
20
|
-
* (① live 运行态 / ①b 唤醒时钟 / ③ 唤醒注册表)。
|
|
21
|
-
*
|
|
22
|
-
* ⚠️ 2026-09-15(ACC 侧 autopilot 退场,S142):原四段降为三段——原 ②「面板解析」与
|
|
23
|
-
* ④「唤起条件链」的主语随机制消失,已删(见 `tools/diag/service.ts` 文件头;④ 的缺口已登记)。
|
|
24
|
-
*/
|
|
25
|
-
export declare function createAccDiagTool(ctx: Context): import("@deepseek-ai/dsh-tools").ToolDefinition;
|