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