@shgroup/dsh-serenity-hooks 1.47.3 → 1.47.4
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/api.d.ts +0 -10
- package/lib/clock-runtime.d.ts +7 -5
- package/lib/cro-turns.d.ts +0 -3
- package/lib/cro.d.ts +0 -2
- package/lib/index.js +2118 -50
- package/lib/{message-source-aR-FJ38L.js → message-source-yuCgrbU6.js} +45 -4
- package/lib/msm-ops.d.ts +3 -1
- package/lib/tools/trajectory.d.ts +0 -22
- package/lib/trajectory-assistant.d.ts +3 -8
- package/lib/trajectory-bound.d.ts +0 -17
- package/lib/trajectory-ops.d.ts +29 -2
- package/lib/trajectory-skills.d.ts +0 -2
- package/lib/wake-scheduler.d.ts +1 -1
- package/package.json +1 -1
- package/lib/session-cleanup-BtkSmry8.js +0 -213
- package/lib/wake-scheduler-BR4hD8Vj.js +0 -1888
|
@@ -227,10 +227,17 @@ function showSession(root, key) {
|
|
|
227
227
|
const content = readFileSync(mdPath, "utf-8");
|
|
228
228
|
return `# ${session.dirName}\n\n${content}`;
|
|
229
229
|
}
|
|
230
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* 生成 SESSION.md 模板(固定格式,owner 2026-09-25 裁决;时间戳 YYYY-MM-DD HH:mm)。
|
|
232
|
+
*
|
|
233
|
+
* 🔴 **状态行是被解析的机器契约**:`completed` 的判据 = SESSION.md 里出现 `[x]`(见 `parseSessionMd`)
|
|
234
|
+
* ⇒ 状态行必须带标记 token:未完成 `- 状态: [ ] <label>`/已完成 `- 状态: [x] <label>`。
|
|
235
|
+
* ⚠️ 改模板时**两件事必须同时成立**:① 状态行保留 `[ ]`(且全模板不得出现任何 `[x]`——否则新会话
|
|
236
|
+
* 一出生就被判 completed)② 七个固定段的名字与顺序不得动(契约钉在 `tests/trajectory-ops.test.ts`)。
|
|
237
|
+
*/
|
|
231
238
|
function sessionMdTemplate(title, id, goal, now) {
|
|
232
239
|
const ts = localDateTimeMinutes(now);
|
|
233
|
-
return `# SESSION: ${title}\n- ID: ${id}\n\n##
|
|
240
|
+
return `# SESSION: ${title}\n- ID: ${id}\n- 状态: [ ] 进行中\n\n## 身份与边界\n${goal ?? "(待补充)"}\n\n## 权威锚点\n- (指针:仓库 / 工具 / 单一真相源位置)\n\n## 工作纪律\n- (只放本轨迹特有的;通用项外推到 skill)\n\n## 决策\n| # | 决策 | 理由 / 否决项 |\n|---|------|------|\n| 1 | | |\n\n## 日志\n- ${ts} — 创建(里程碑 + 证据指针;执行细节归 commit message)\n\n## 当前状态\n- 在办:\n- 断点:\n\n## 待 owner 裁决\n- \n`;
|
|
234
241
|
}
|
|
235
242
|
/** create 子命令(对齐 osp createSession:--desc/--issue 二选一 + dry-run + 长度限制) */
|
|
236
243
|
/** 目录名脱敏(Windows 审计问题 10):非法字符 → '-', 去尾点/空格, 保留名(CON/NUL 等)加前缀 */
|
|
@@ -306,6 +313,39 @@ function createSession(opts) {
|
|
|
306
313
|
const DEFAULT_SESSION_SCOPE = "default";
|
|
307
314
|
/** [SESSION CONTEXT] 恢复标记(use 时注入 events 历史;进程重启后从 events 解析) */
|
|
308
315
|
const SESSION_CONTEXT_MARKER = "[SESSION CONTEXT] Activated:";
|
|
316
|
+
/**
|
|
317
|
+
* 概括清洗(标题用;需求② S142 用户拍板:编号日期后加 ≤20 字内容概括)。
|
|
318
|
+
* 规则(服务端统一,不信任 LLM 输入):
|
|
319
|
+
* - 去控制字符/换行/回车/制表(防标题注入/多行污染)
|
|
320
|
+
* - trim(去首尾空白)
|
|
321
|
+
* - 截断 ≤20 字符(按 Unicode 码点——中英混排统一;emoji 等代理对按码点保留)
|
|
322
|
+
* - 去 `/`(防标题被误读为路径分隔)
|
|
323
|
+
* @returns 清洗后的概括(空输入 → 空串;调用方决定是否允许空)
|
|
324
|
+
*/
|
|
325
|
+
function sanitizeSessionSummary(summary) {
|
|
326
|
+
return [...summary.replace(/[\u0000-\u001f\u007f]/g, "").replace(/\//g, "").trim()].slice(0, 20).join("");
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 从激活会话派生命名标题(v1.22.9 格式修正 + 需求② 概括):
|
|
330
|
+
* F3 原始需求是 **`S###-日期`**(如 `S143-2026-08-26`)——从 `sessionId` 派生,
|
|
331
|
+
* 而非完整目录名(`2026-08-24--S142--...` 超长 + 中文,不符合用户拍板格式)。
|
|
332
|
+
* 需求②(S142 用户拍板):编号日期后加 ≤20 字内容概括 → `S###-YYYY-MM-DD-<概括>`
|
|
333
|
+
* ——概括来自显式 summary 参数(服务端截断/清洗,可靠不靠猜);编号日期仍固定派生。
|
|
334
|
+
* 无 S### 编号(issue 会话等)→ 回退目录名。
|
|
335
|
+
* @param active 激活会话信息(sessionId + dirName)
|
|
336
|
+
* @param summary 内容概括(≤20 字,服务端清洗截断;空 → 不带概括的 `S###-日期`)
|
|
337
|
+
* @returns `S143-2026-08-26-概括` / `S143-2026-08-26` / 原目录名
|
|
338
|
+
*/
|
|
339
|
+
function namingTitleFor(active, summary) {
|
|
340
|
+
const sid = active.sessionId;
|
|
341
|
+
if (typeof sid === "string" && /^S\d+$/.test(sid)) {
|
|
342
|
+
const date = active.dirName.match(/^(\d{4}-\d{2}-\d{2})--/)?.[1] ?? "";
|
|
343
|
+
const cleaned = summary ? sanitizeSessionSummary(summary) : "";
|
|
344
|
+
const base = date ? `${sid}-${date}` : sid;
|
|
345
|
+
return cleaned ? `${base}-${cleaned}` : base;
|
|
346
|
+
}
|
|
347
|
+
return active.dirName;
|
|
348
|
+
}
|
|
309
349
|
/** 内存活跃会话:scope(dsh 会话 id)→ 会话信息(不落盘;并行多会话各自 key 隔离) */
|
|
310
350
|
const activeStore = /* @__PURE__ */ new Map();
|
|
311
351
|
/** 全局最近活跃(对齐 osp lastActive;供无 scope 上下文使用) */
|
|
@@ -358,7 +398,8 @@ function useSession(root, key, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
358
398
|
``,
|
|
359
399
|
`→ All subsequent work should refer back to this session.`,
|
|
360
400
|
` Use "session show ${sessionId}" to check current progress.`,
|
|
361
|
-
`
|
|
401
|
+
` When a milestone lands or a decision is made: overwrite "当前状态" and append ONE`,
|
|
402
|
+
` milestone line to "日志" (one line + an evidence pointer — details go to the commit message).`,
|
|
362
403
|
``,
|
|
363
404
|
`→ BEFORE responding to the user, you MUST call todowrite immediately`,
|
|
364
405
|
` with the session todo list. The first item MUST be:`,
|
|
@@ -924,4 +965,4 @@ const ACC_MESSAGE_KIND = "plugin:dsh-serenity-hooks";
|
|
|
924
965
|
/** ACC 注入消息的统一来源值(各缝共用;需要 `form` 时按 `ContextFormed` 另加字段)。 */
|
|
925
966
|
const PLUGIN_SOURCE = { kind: ACC_MESSAGE_KIND };
|
|
926
967
|
//#endregion
|
|
927
|
-
export {
|
|
968
|
+
export { localIdStamp as A, setActiveSessionInfo as C, isoLocal as D, useSession as E, localFileStamp as O, sessionsRoot as S, summarize as T, namingTitleFor as _, pruneMissingBindings as a, resolveSessionByTitle as b, setBindingStore as c, clearActiveSessionInfo as d, createSession as f, listSessions as g, getActiveSessionInfo as h, listBoundSessionIds as i, localHuman as k, DEFAULT_SESSION_SCOPE as l, findSession as m, PLUGIN_SOURCE as n, readLastBound as o, extractSessionMdPathFromText as p, appendBound as r, resolveSessionTrajectoryLabel as s, ACC_MESSAGE_KIND as t, TRAJECTORY_ACTIONS as u, parseSessionContextFromEvents as v, showSession as w, sessionEvents as x, readActiveSessionMd as y };
|
package/lib/msm-ops.d.ts
CHANGED
|
@@ -24,7 +24,9 @@ export interface MsmEntry {
|
|
|
24
24
|
usage?: string;
|
|
25
25
|
flags?: MsmFlag[];
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
/** 🔴 2026-09-25(S142 第 1 批):本接口**必须导出** —— `runMsm`/`runMsmAsync` 的**公开签名**引用它
|
|
28
|
+
* (此前未导出 ⇒ 外部调用方看不到该类型,只能传结构等价字面量 = 契约不完整)。 */
|
|
29
|
+
export interface MsmArgs {
|
|
28
30
|
action: MsmAction;
|
|
29
31
|
name?: string;
|
|
30
32
|
args?: string[];
|
|
@@ -44,28 +44,6 @@ interface RenameOnUseDeps {
|
|
|
44
44
|
/** sessionTitle 服务可用性 */
|
|
45
45
|
sessionTitleAvailable: boolean;
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
* 清洗 + 截断会话概括(需求② S142 用户拍板:编号日期后加 ≤20 字内容概括)。
|
|
49
|
-
* 规则(服务端统一,不信任 LLM 输入):
|
|
50
|
-
* - 去控制字符/换行/回车/制表(防标题注入/多行污染)
|
|
51
|
-
* - trim(去首尾空白)
|
|
52
|
-
* - 截断 ≤20 字符(按 Unicode 码点——中英混排统一;emoji 等代理对按码点保留)
|
|
53
|
-
* - 去 `/`(防标题被误读为路径分隔)
|
|
54
|
-
* @returns 清洗后的概括(空输入 → 空串;调用方决定是否允许空)
|
|
55
|
-
*/
|
|
56
|
-
export declare function sanitizeSessionSummary(summary: string): string;
|
|
57
|
-
/**
|
|
58
|
-
* 从激活会话派生命名标题(v1.22.9 格式修正 + 需求② 概括):
|
|
59
|
-
* F3 原始需求是 **`S###-日期`**(如 `S143-2026-08-26`)——从 `sessionId` 派生,
|
|
60
|
-
* 而非完整目录名(`2026-08-24--S142--...` 超长 + 中文,不符合用户拍板格式)。
|
|
61
|
-
* 需求②(S142 用户拍板):编号日期后加 ≤20 字内容概括 → `S###-YYYY-MM-DD-<概括>`
|
|
62
|
-
* ——概括来自显式 summary 参数(服务端截断/清洗,可靠不靠猜);编号日期仍固定派生。
|
|
63
|
-
* 无 S### 编号(issue 会话等)→ 回退目录名。
|
|
64
|
-
* @param active 激活会话信息(sessionId + dirName)
|
|
65
|
-
* @param summary 内容概括(≤20 字,服务端清洗截断;空 → 不带概括的 `S###-日期`)
|
|
66
|
-
* @returns `S143-2026-08-26-概括` / `S143-2026-08-26` / 原目录名
|
|
67
|
-
*/
|
|
68
|
-
export declare function namingTitleFor(active: ActiveSessionInfo, summary?: string): string;
|
|
69
47
|
/**
|
|
70
48
|
* use 激活宁静号会话后,把当前 dsh 会话重命名为命名标题(`S###-日期[-概括]`)。
|
|
71
49
|
* v1.27.1:**永远开启**(不再有 naming.enabled 门控)——仅 sessionTitle 服务
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* L6 结算)仅在本模块注释与内部文档出现,永不浮现在提示词文本。
|
|
16
16
|
*
|
|
17
17
|
* 结算(settlement)挂点:D6 用户拍板——若 CCC 有标准 SESSION 复盘仪式,
|
|
18
|
-
* 结算视图归 trajectory-assistant。当前 close/archive 无复盘摘要
|
|
19
|
-
*
|
|
18
|
+
* 结算视图归 trajectory-assistant。当前 close/archive 无复盘摘要 ⇒ 机制延后。
|
|
19
|
+
* 🔴 2026-09-25(S142 分批清死代码·第 1 批):原先那个 `onSettlement(cb)` **空 seam 已删**
|
|
20
|
+
* (零调用者、无触发器、空实现)——仪式真落地时按当时的信号重新设计,**不再靠空占位**。
|
|
20
21
|
*/
|
|
21
22
|
/** 可见提示词前缀用词(跨领域自然词,D8:无游戏黑话) */
|
|
22
23
|
export declare const EVENT_LABEL: {
|
|
@@ -59,10 +60,4 @@ type TrajectoryStyle = 'plain' | 'metaphor';
|
|
|
59
60
|
* "context limit/deck check" 类,不强制替换;扩展点留给未来实验)。
|
|
60
61
|
*/
|
|
61
62
|
export declare function styledToken(event: keyof typeof EVENT_LABEL, style?: TrajectoryStyle): string;
|
|
62
|
-
/**
|
|
63
|
-
* 结算 seam(OP-1/D6):未来标准 SESSION 复盘仪式接入点。当前无调用者,
|
|
64
|
-
* 仅导出契约:onSettlement(cb) 在"工作完成且被用户认可"时触发——该信号
|
|
65
|
-
* 尚无可靠自动检测(D2 用户拍板记录为未解问题),实现留待仪式落地。
|
|
66
|
-
*/
|
|
67
|
-
export declare function onSettlement(_cb: (sessionId: string) => void): void;
|
|
68
63
|
export {};
|
|
@@ -51,8 +51,6 @@ export declare const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
|
|
|
51
51
|
* 幂等:重复调用以后者为准(HMR / 重载场景)。
|
|
52
52
|
*/
|
|
53
53
|
export declare function setBindingStore(store: BindingStore | null): void;
|
|
54
|
-
/** 当前域句柄(诊断用;`null` = 未接线/不可用) */
|
|
55
|
-
export declare function getBindingStore(): BindingStore | null;
|
|
56
54
|
/**
|
|
57
55
|
* §0L **一次性迁移**:把某个 CCC 的旧 `.bindings.json` 全量灌入宿主存储域。
|
|
58
56
|
*
|
|
@@ -72,21 +70,6 @@ export declare function migrateBindingsToDomain(root: string): {
|
|
|
72
70
|
skipped: number;
|
|
73
71
|
failed: number;
|
|
74
72
|
};
|
|
75
|
-
/**
|
|
76
|
-
* **按 CCC 惰性补迁移**(§0L 修正,S142 2026-09-19)。
|
|
77
|
-
*
|
|
78
|
-
* 🔴 为什么需要它(实测缺陷,R↓):
|
|
79
|
-
* 装载期那次迁移用的是 `process.cwd()` —— 而 dsh **服务进程的 cwd 是 `/home/yh`**,
|
|
80
|
-
* **不是 CCC 根**(实测 `acc-diag`:`进程 cwd: /home/yh | 进程 CCC: (无)`)。
|
|
81
|
-
* ⇒ 迁移读的是一个**不存在的文件**,静默迁移 0 条
|
|
82
|
-
* (症状:发布后 `~/.dsh/storages/serenity_bindings.json` **没出现**)。
|
|
83
|
-
*
|
|
84
|
-
* 现在改为:**任何一次能定位到 CCC 根的路径**都顺手补一次迁移(幂等 + 进程内去重)。
|
|
85
|
-
* 这样无论服务从哪个 cwd 启动,只要那条 CCC 有会话活动,它的绑定就会被带上。
|
|
86
|
-
*
|
|
87
|
-
* @param root CCC 根
|
|
88
|
-
*/
|
|
89
|
-
export declare function ensureBindingsMigrated(root: string): void;
|
|
90
73
|
/** 绑定文件绝对路径(由会话 cwd 上溯 .serenity 定位 CCC 根);无法定位返回 null */
|
|
91
74
|
export declare function bindingsPathFor(session: unknown): string | null;
|
|
92
75
|
/**
|
package/lib/trajectory-ops.d.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* `container_trajectory` 工具的动作集合(v1.33,S142 §32 用户裁决后的收敛结果)。
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* 为什么收敛到这 8 个(R↓;v1.33 收敛时为 **6 个** —— 其后追加 `send-now`(09-16)
|
|
18
|
+
* 与 `cro-guide`(09-20),🔴 2026-09-25 更正计数,勿再写成"6 个"):旧面是 `logbook`(11)+ `trajectory`(12)两个工具,用户裁决
|
|
18
19
|
* 「都合并成 trajectory,废除 logbook 这个词」并逐条删并 ——
|
|
19
20
|
* summary 并进 list|health/qa 淘汰(判据=旧模板,EAP 已取代;仅"空壳/长期无活动"并入 use)|
|
|
20
21
|
* close/archive 删(completed 由 SESSION.md 的 `[x]` 推导;归档走 container_fs mv)|
|
|
@@ -52,7 +53,11 @@ export declare function sessionEvents<T = unknown>(session: unknown): readonly T
|
|
|
52
53
|
export declare function sessionsRoot(root: string): string;
|
|
53
54
|
interface SessionStatus {
|
|
54
55
|
hasSessionMd: boolean;
|
|
55
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* 「完成」的判据 = SESSION.md 里**出现 `[x]`**(v1.33 起不再由 close 动作写入)。
|
|
58
|
+
* 🔴 这是**全文匹配**、两代格式通用:新格式的状态行 `- 状态: [x] …` 与旧格式的
|
|
59
|
+
* `## 状态` 段内 `- [x] 已完成` 都能被识别 ⇒ **不可改成锚定某一行的正则**(会让旧会话永远"未完成")。
|
|
60
|
+
*/
|
|
56
61
|
completed: boolean;
|
|
57
62
|
}
|
|
58
63
|
interface SessionEntry {
|
|
@@ -101,6 +106,28 @@ export interface ActiveSessionInfo {
|
|
|
101
106
|
dirName: string;
|
|
102
107
|
mdPath: string;
|
|
103
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* 概括清洗(标题用;需求② S142 用户拍板:编号日期后加 ≤20 字内容概括)。
|
|
111
|
+
* 规则(服务端统一,不信任 LLM 输入):
|
|
112
|
+
* - 去控制字符/换行/回车/制表(防标题注入/多行污染)
|
|
113
|
+
* - trim(去首尾空白)
|
|
114
|
+
* - 截断 ≤20 字符(按 Unicode 码点——中英混排统一;emoji 等代理对按码点保留)
|
|
115
|
+
* - 去 `/`(防标题被误读为路径分隔)
|
|
116
|
+
* @returns 清洗后的概括(空输入 → 空串;调用方决定是否允许空)
|
|
117
|
+
*/
|
|
118
|
+
export declare function sanitizeSessionSummary(summary: string): string;
|
|
119
|
+
/**
|
|
120
|
+
* 从激活会话派生命名标题(v1.22.9 格式修正 + 需求② 概括):
|
|
121
|
+
* F3 原始需求是 **`S###-日期`**(如 `S143-2026-08-26`)——从 `sessionId` 派生,
|
|
122
|
+
* 而非完整目录名(`2026-08-24--S142--...` 超长 + 中文,不符合用户拍板格式)。
|
|
123
|
+
* 需求②(S142 用户拍板):编号日期后加 ≤20 字内容概括 → `S###-YYYY-MM-DD-<概括>`
|
|
124
|
+
* ——概括来自显式 summary 参数(服务端截断/清洗,可靠不靠猜);编号日期仍固定派生。
|
|
125
|
+
* 无 S### 编号(issue 会话等)→ 回退目录名。
|
|
126
|
+
* @param active 激活会话信息(sessionId + dirName)
|
|
127
|
+
* @param summary 内容概括(≤20 字,服务端清洗截断;空 → 不带概括的 `S###-日期`)
|
|
128
|
+
* @returns `S143-2026-08-26-概括` / `S143-2026-08-26` / 原目录名
|
|
129
|
+
*/
|
|
130
|
+
export declare function namingTitleFor(active: ActiveSessionInfo, summary?: string): string;
|
|
104
131
|
export declare function getActiveSessionInfo(scope: string): ActiveSessionInfo | null;
|
|
105
132
|
export declare function getLastActiveSessionInfo(): ActiveSessionInfo | null;
|
|
106
133
|
export declare function setActiveSessionInfo(scope: string, info: ActiveSessionInfo): void;
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
/** 全文注入总长上限(字符数;规格 §3「缺省 32 KB」——防上下文爆炸) */
|
|
17
17
|
export declare const TRAJECTORY_SKILLS_MAX_CHARS: number;
|
|
18
|
-
/** 缺失标记(规格 §3/§6-3 的验收锚点:任何"拿不到全文"的情况都必须带它,绝不静默) */
|
|
19
|
-
export declare const SKILL_MISSING_MARK = "[\u7F3A\u5931]";
|
|
20
18
|
/**
|
|
21
19
|
* 抽取文件顶部 YAML frontmatter 区(规格 §2.1)。
|
|
22
20
|
*
|
package/lib/wake-scheduler.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ interface WakeDeliveryResult {
|
|
|
54
54
|
* C6b(P2):字段由 {@link ClockRuntime} 提供(**工厂那份**,字段名与语义一个字未改);
|
|
55
55
|
* 此处只把 `lastTickLog` 收为**必需**(本钟独有,autopilot 没有它——现状稿 §2.4)。
|
|
56
56
|
*/
|
|
57
|
-
|
|
57
|
+
type WakeSchedulerRuntime = ClockRuntime & {
|
|
58
58
|
lastTickLog: string[];
|
|
59
59
|
};
|
|
60
60
|
/** 目标定位:目录名 + SESSION.md 绝对路径 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.47.
|
|
3
|
+
"version": "1.47.4",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
4
|
-
//#region src/session-cleanup.ts
|
|
5
|
-
/**
|
|
6
|
-
* session-cleanup.ts — DSH 平台旧会话自动清理(v1.29,S142 需求③)
|
|
7
|
-
*
|
|
8
|
-
* 背景(实证):DSH 会话物理存储 = `$DSH_HOME/sessions/`(缺省 ~/.dsh/sessions;
|
|
9
|
-
* base bundle cordis.patch.yml root: dshHomePath('sessions'))。磁盘布局:
|
|
10
|
-
* <root>/--<project-slug>--/<encoded-session-id>/session[.vN].jsonl(.zstd)
|
|
11
|
-
* **世代(v1.31.13 补)**:宿主自 DSH 0.1.5 起按世代写文件,且迁移**保留源文件**
|
|
12
|
-
* ⇒ 一个会话目录常见 v0 与 v3 两个文件并存。本模块用 `sessionLogArtifacts()` 统一枚举世代。
|
|
13
|
-
* DSH **无删除会话 API**(PersistenceCoordinator 仅 create/append/load/inspect/borrow,
|
|
14
|
-
* 无 delete/purge;workspace archiveSession 只加归档集不删文件)——物理删除只能直接
|
|
15
|
-
* 删文件。删除后一致性(利好):sessionPersistence.list() = readdir 扫描磁盘现存 →
|
|
16
|
-
* 物理删后 list 不再返回;WorkspaceRegistry 下次启动重建 header index 自动消失。
|
|
17
|
-
*
|
|
18
|
-
* 方案(S142 用户拍板):lastActive 基准 + 手动触发 + 不过滤不归档直接物理删;
|
|
19
|
-
* 安全底线 = live 会话保护跳过(删正在运行的会话 = 灾难)。
|
|
20
|
-
*
|
|
21
|
-
* 本模块纯逻辑(零 ctx 依赖):传入 sessionsRoot + liveIds,可单测。
|
|
22
|
-
*/
|
|
23
|
-
var session_cleanup_exports = /* @__PURE__ */ __exportAll({
|
|
24
|
-
collectEligibleSessions: () => collectEligibleSessions,
|
|
25
|
-
cutoffDaysAgo: () => cutoffDaysAgo,
|
|
26
|
-
findSessionLog: () => findSessionLog,
|
|
27
|
-
hasSessionLogById: () => hasSessionLogById,
|
|
28
|
-
performCleanup: () => performCleanup,
|
|
29
|
-
sessionLogArtifacts: () => sessionLogArtifacts,
|
|
30
|
-
sessionLogBytesById: () => sessionLogBytesById,
|
|
31
|
-
sessionsRootDir: () => sessionsRootDir
|
|
32
|
-
});
|
|
33
|
-
/** DSH 会话 root:env DSH_HOME → ~/.dsh(config-ops globalConfigPath 同款推导) */
|
|
34
|
-
function sessionsRootDir() {
|
|
35
|
-
const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
|
|
36
|
-
return join(dshHome, "sessions");
|
|
37
|
-
}
|
|
38
|
-
/** 一天的毫秒数 */
|
|
39
|
-
const DAY_MS = 864e5;
|
|
40
|
-
/**
|
|
41
|
-
* 扫描 sessions root,列出可清理候选:
|
|
42
|
-
* - 递归 `<root>/<project>/<session-id>/session.jsonl(.zstd)`(任意深度下找 *session* 文件)
|
|
43
|
-
* - 过滤 live(liveIds 集合)——安全底线
|
|
44
|
-
* - 过滤 lastActive >= cutoffMs(最后活动未达阈值 → 保留)
|
|
45
|
-
* root 不存在/为空 → 返回 [](非错误)。
|
|
46
|
-
*/
|
|
47
|
-
function collectEligibleSessions(root, cutoffMs, liveIds = /* @__PURE__ */ new Set()) {
|
|
48
|
-
if (!existsSync(root)) return [];
|
|
49
|
-
const out = [];
|
|
50
|
-
for (const project of readdirSync(root, { withFileTypes: true })) {
|
|
51
|
-
if (!project.isDirectory()) continue;
|
|
52
|
-
const projectPath = join(root, project.name);
|
|
53
|
-
for (const entry of readdirSync(projectPath, { withFileTypes: true })) {
|
|
54
|
-
if (!entry.isDirectory()) continue;
|
|
55
|
-
const dirPath = join(projectPath, entry.name);
|
|
56
|
-
const artifacts = sessionLogArtifacts(dirPath);
|
|
57
|
-
if (artifacts.length === 0) continue;
|
|
58
|
-
const log = findSessionLog(dirPath);
|
|
59
|
-
if (log === null) continue;
|
|
60
|
-
if (liveIds.has(entry.name)) continue;
|
|
61
|
-
let mtime = Number.NEGATIVE_INFINITY;
|
|
62
|
-
for (const artifact of artifacts) try {
|
|
63
|
-
mtime = Math.max(mtime, statSync(artifact.path).mtimeMs);
|
|
64
|
-
} catch {}
|
|
65
|
-
if (!Number.isFinite(mtime)) continue;
|
|
66
|
-
if (mtime >= cutoffMs) continue;
|
|
67
|
-
out.push({
|
|
68
|
-
id: entry.name,
|
|
69
|
-
project: project.name,
|
|
70
|
-
logPath: log,
|
|
71
|
-
lastActiveMs: mtime
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* 会话日志文件名识别:`session.jsonl` / `session.jsonl.zstd`(= **v0 世代**)/
|
|
79
|
-
* `session.v<N>.jsonl[.zstd]`(= 第 N 世代,DSH 0.1.5 起按世代写文件)。
|
|
80
|
-
* 出处:`@deepseek-ai/dsh-session-persistence-jsonl` 的 `parseSessionFormatLogFilename` 同款形态。
|
|
81
|
-
*/
|
|
82
|
-
const SESSION_LOG_RE = /^session(?:\.v(\d+))?\.jsonl(\.zstd)?$/;
|
|
83
|
-
/**
|
|
84
|
-
* 列出会话目录下的**全部日志世代**,按代递增排序(同代内 `.jsonl` 在前、`.zstd` 在后 = 后者更代表"当前书写格式")。
|
|
85
|
-
*
|
|
86
|
-
* 为什么需要(R↓,v1.31.13 修复):宿主自 0.1.5 起改成**按世代写文件**,且迁移**保留源文件**
|
|
87
|
-
* ⇒ 同一会话常见 `session.jsonl.zstd`(v0)与 `session.v3.jsonl.zstd`(v3)**并存**。
|
|
88
|
-
* 旧实现只认前两个名字 → 新格式会话对本模块**完全不可见**(静默 no-op:清理永远不生效、磁盘只涨)。
|
|
89
|
-
*/
|
|
90
|
-
function sessionLogArtifacts(sessionDirPath) {
|
|
91
|
-
let names;
|
|
92
|
-
try {
|
|
93
|
-
names = readdirSync(sessionDirPath);
|
|
94
|
-
} catch {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
const out = [];
|
|
98
|
-
for (const name of names) {
|
|
99
|
-
const m = SESSION_LOG_RE.exec(name);
|
|
100
|
-
if (m === null) continue;
|
|
101
|
-
out.push({
|
|
102
|
-
name,
|
|
103
|
-
generation: m[1] === void 0 ? 0 : Number(m[1]),
|
|
104
|
-
path: join(sessionDirPath, name)
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
return out.sort((a, b) => a.generation - b.generation || (a.name.endsWith(".zstd") ? 1 : 0) - (b.name.endsWith(".zstd") ? 1 : 0));
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* 在会话目录下找 session 日志文件;无 → null。
|
|
111
|
-
* 多世代并存时返回**最高世代**(= 宿主 `findLog` 选中的当前世代;旧行为对单文件目录完全一致)。
|
|
112
|
-
*/
|
|
113
|
-
function findSessionLog(sessionDirPath) {
|
|
114
|
-
const artifacts = sessionLogArtifacts(sessionDirPath);
|
|
115
|
-
const latest = artifacts[artifacts.length - 1];
|
|
116
|
-
return latest === void 0 ? null : latest.path;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* 执行清理(用户拍板:直接物理删,不过滤不归档;dryRun 预览不删)。
|
|
120
|
-
* 删除 = rmSync 整个会话目录(recursive + force)。失败逐个记 errors 不中断。
|
|
121
|
-
*/
|
|
122
|
-
function performCleanup(root, cutoffMs, liveIds = /* @__PURE__ */ new Set(), opts = {}) {
|
|
123
|
-
const candidates = collectEligibleSessions(root, cutoffMs, liveIds);
|
|
124
|
-
if (opts.dryRun) return {
|
|
125
|
-
candidates,
|
|
126
|
-
result: null
|
|
127
|
-
};
|
|
128
|
-
const result = {
|
|
129
|
-
deleted: [],
|
|
130
|
-
errors: []
|
|
131
|
-
};
|
|
132
|
-
for (const c of candidates) {
|
|
133
|
-
const dir = dirOf(c.logPath);
|
|
134
|
-
try {
|
|
135
|
-
rmSync(dir, {
|
|
136
|
-
recursive: true,
|
|
137
|
-
force: true
|
|
138
|
-
});
|
|
139
|
-
result.deleted.push(c.id);
|
|
140
|
-
} catch (e) {
|
|
141
|
-
result.errors.push({
|
|
142
|
-
id: c.id,
|
|
143
|
-
reason: e.message
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
candidates,
|
|
149
|
-
result
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
/** 从日志路径取会话目录(父目录) */
|
|
153
|
-
function dirOf(logPath) {
|
|
154
|
-
return logPath.slice(0, logPath.lastIndexOf("/")) || logPath;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* 某一 dsh 会话(目录名 / 会话 id)的**当前世代日志体积**(字节)。
|
|
158
|
-
*
|
|
159
|
-
* 为什么要它(S142 §0y「旧会话累积 ⇒ dsh 内存崩溃」,所有者令):
|
|
160
|
-
* · 宿主恢复/继续一条会话必须**重放全部事件**(`scanLog` → `materializeAppendBatch`)
|
|
161
|
-
* ⇒ **代价 ∝ 日志体积**;
|
|
162
|
-
* · 实测(`_tmp/measure-session-load.mjs`,照宿主 `scanZstdFrames` 复刻):
|
|
163
|
-
* 一条 **72.2 MB**(zstd)的日志 = 35 559 个 frame / 252.6 MB 文本 / 62 680 条事件,
|
|
164
|
-
* **光是读一遍峰值 RSS +1 175 MB**(探针什么都不保留);
|
|
165
|
-
* · 而宿主的解析缓存 `COLD_LOG_MEMO_MAX_ENTRIES = 2` 是**按条数限、不按字节限**的
|
|
166
|
-
* (`dsh-session-persistence-jsonl/lib/index.js:2175`)⇒ 两条巨日志 = 上限内。
|
|
167
|
-
* 🔴 **在本函数之前,ACC 侧没有任何地方知道"绑定的这条会话日志多大"——这个信号根本不存在。**
|
|
168
|
-
* 这是"靠 agent 自觉 rebuild 不够"的机制层原因(keeper 的 K 量的是**模型上下文**,压缩后回落;
|
|
169
|
-
* 而**日志文件只增不减** ⇒ rebuild 提醒永远不会因为"日志 72 MB"触发)。
|
|
170
|
-
*
|
|
171
|
-
* 只认会话目录(含任意世代日志);返回**最高世代**的日志(= 宿主 `findLog` 选中的当前世代)。
|
|
172
|
-
* 未命中 / 不可读 → null(非错误,调用方按"未知"处理)。
|
|
173
|
-
* @param root - DSH 会话 root({@link sessionsRootDir})
|
|
174
|
-
* @param sessionId - 会话目录名(`.bindings.json` 的键,含 `session-` 前缀)或裸 id
|
|
175
|
-
* @returns 字节数 + 日志绝对路径;未命中 → null
|
|
176
|
-
*/
|
|
177
|
-
function sessionLogBytesById(root, sessionId) {
|
|
178
|
-
if (sessionId === "" || !existsSync(root)) return null;
|
|
179
|
-
const dirName = sessionId.startsWith("session-") ? sessionId : `session-${sessionId}`;
|
|
180
|
-
for (const project of readdirSync(root, { withFileTypes: true })) {
|
|
181
|
-
if (!project.isDirectory()) continue;
|
|
182
|
-
const log = findSessionLog(join(root, project.name, dirName));
|
|
183
|
-
if (log === null) continue;
|
|
184
|
-
try {
|
|
185
|
-
return {
|
|
186
|
-
bytes: statSync(log).size,
|
|
187
|
-
path: log
|
|
188
|
-
};
|
|
189
|
-
} catch {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* **(c) 绑定一致性兜底**的判据:某会话的日志**是否仍在磁盘上**。
|
|
197
|
-
*
|
|
198
|
-
* 🔴 **fail-closed**:sessions root 本身不存在时**一律返回 true(当作"在")**——
|
|
199
|
-
* 否则换 `DSH_HOME` / 换机器会让"全部绑定看起来都悬空",进而误清整张绑定表。
|
|
200
|
-
* 只有"root 在、而这个会话目录确实不在"才算悬空。
|
|
201
|
-
* @param sessionRoot - DSH 会话 root({@link sessionsRootDir})
|
|
202
|
-
* @param sessionId - 会话目录名(`.bindings.json` 的键)或裸 id
|
|
203
|
-
*/
|
|
204
|
-
function hasSessionLogById(sessionRoot, sessionId) {
|
|
205
|
-
if (!existsSync(sessionRoot)) return true;
|
|
206
|
-
return sessionLogBytesById(sessionRoot, sessionId) !== null;
|
|
207
|
-
}
|
|
208
|
-
/** 便捷:默认 N 天前为 cutoff(供 API/调用方) */
|
|
209
|
-
function cutoffDaysAgo(days, nowMs = Date.now()) {
|
|
210
|
-
return nowMs - days * DAY_MS;
|
|
211
|
-
}
|
|
212
|
-
//#endregion
|
|
213
|
-
export { session_cleanup_exports as n, sessionsRootDir as r, hasSessionLogById as t };
|