@shgroup/dsh-serenity-hooks 1.76.0 → 1.77.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.76.0",
3
+ "version": "1.77.1",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -0,0 +1,213 @@
1
+ import { n as __exportAll } from "./rolldown-runtime-B4iAMlE-.js";
2
+ import { join } from "node:path";
3
+ import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
4
+ //#region src/trajectory/cleanup.ts
5
+ /**
6
+ * trajectory/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 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 { hasSessionLogById as n, sessionsRootDir as r, cleanup_exports as t };