@shgroup/dsh-serenity-hooks 1.76.0 → 1.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dsh.plugin.json +1 -1
- package/lib/cleanup-C-92qnDs.js +213 -0
- package/lib/index.js +26 -232
- package/lib/message-source-CYa8LbbM.js +488 -0
- package/lib/trajectory/bind-file.d.ts +228 -0
- package/lib/trajectory/bound.d.ts +117 -132
- package/package.json +1 -1
- package/lib/message-source-C_Zd2vJh.js +0 -435
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.77.0",
|
|
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 };
|
package/lib/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as
|
|
|
4
4
|
import { A as sessionsRoot, B as localFileStamp, C as namingTitleFor, D as readTrajectoryUid, E as readActiveSessionMd, F as useSession, H as localIdStamp, I as TRAJECTORY_FILENAME, L as TRAJECTORY_LEGACY_FILENAMES, M as showSession, N as summarize, O as resolveSessionByTitle, P as trajectoryDescOf, R as readTrajectoryFile, S as listSessions, T as parseTrajectoryCode, V as localHuman, _ as extractSessionMdPathFromText, a as readTrajectoryConstraintAt, c as registerTrajectorySession$1, d as COGNITIVE_TRAJECTORY_ACTIONS, f as DEFAULT_SESSION_SCOPE, g as createSession, h as clearActiveSessionInfo, j as setActiveSessionInfo, k as sessionEvents$1, l as trajectoryDirNameForSession, m as archiveSession, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, p as UID_FILENAME, r as listTrajectoryConstraints, s as trajectoryConstraintForSession, t as TRAJECTORY_CONSTRAINT_FILENAME, u as unregisterTrajectorySession$1, v as findSession, w as parseSessionContextFromEvents, x as listActiveTrajectories, y as getActiveSessionInfo, z as isoLocal } from "./profile-DplLGGqo.js";
|
|
5
5
|
import { _ as runLocalStore, a as readFeishuCredential, d as weixinSessionIdForTrajectory, f as LOCALSTORE_SCOPES, g as readStore, h as readGitTrack, i as matchWeixinRoute, l as sanitizeFileName$1, m as localstorePath, n as extractWeixinText, o as readWeixinCredential, p as checkLocalstoreGitCompliance, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-COOFLTpU.js";
|
|
6
6
|
import { a as buildTrajectoryBasePrompt, c as createProfilePromptReader, d as resolveConstraintSystemPrompt, f as ACC_VERSION, i as TRAJECTORY_CONSTRAINT_HINT, l as profileMsmWhitelist, n as readSimpleSettings, o as constraintCognitiveTrajectory, p as findPackageRoot, r as registerSettingsSection, s as constraintSystemPromptSource, u as profileToolWhitelist } from "./settings-f9WZXcgi.js";
|
|
7
|
-
import { a as
|
|
7
|
+
import { a as listBoundSessionIds, i as carrierState, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-CYa8LbbM.js";
|
|
8
|
+
import { n as hasSessionLogById, r as sessionsRootDir } from "./cleanup-C-92qnDs.js";
|
|
8
9
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
9
10
|
import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-BMk2w2KY.js";
|
|
10
11
|
import { a as registerImAccountImpl, i as normalizeAccountChannel, n as getImAccountImpl, r as imAccountChannels } from "./accounts-BHQbakUh.js";
|
|
@@ -9595,215 +9596,6 @@ function createCheapWorkerTool(ctx) {
|
|
|
9595
9596
|
});
|
|
9596
9597
|
}
|
|
9597
9598
|
//#endregion
|
|
9598
|
-
//#region src/trajectory/cleanup.ts
|
|
9599
|
-
/**
|
|
9600
|
-
* trajectory/cleanup.ts — DSH 平台旧会话自动清理(v1.29,S142 需求③)
|
|
9601
|
-
*
|
|
9602
|
-
* 背景(实证):DSH 会话物理存储 = `$DSH_HOME/sessions/`(缺省 ~/.dsh/sessions;
|
|
9603
|
-
* base bundle cordis.patch.yml root: dshHomePath('sessions'))。磁盘布局:
|
|
9604
|
-
* <root>/--<project-slug>--/<encoded-session-id>/session[.vN].jsonl(.zstd)
|
|
9605
|
-
* **世代(v1.31.13 补)**:宿主自 DSH 0.1.5 起按世代写文件,且迁移**保留源文件**
|
|
9606
|
-
* ⇒ 一个会话目录常见 v0 与 v3 两个文件并存。本模块用 `sessionLogArtifacts()` 统一枚举世代。
|
|
9607
|
-
* DSH **无删除会话 API**(PersistenceCoordinator 仅 create/append/load/inspect/borrow,
|
|
9608
|
-
* 无 delete/purge;workspace archiveSession 只加归档集不删文件)——物理删除只能直接
|
|
9609
|
-
* 删文件。删除后一致性(利好):sessionPersistence.list() = readdir 扫描磁盘现存 →
|
|
9610
|
-
* 物理删后 list 不再返回;WorkspaceRegistry 下次启动重建 header index 自动消失。
|
|
9611
|
-
*
|
|
9612
|
-
* 方案(S142 用户拍板):lastActive 基准 + 手动触发 + 不过滤不归档直接物理删;
|
|
9613
|
-
* 安全底线 = live 会话保护跳过(删正在运行的会话 = 灾难)。
|
|
9614
|
-
*
|
|
9615
|
-
* 本模块纯逻辑(零 ctx 依赖):传入 sessionsRoot + liveIds,可单测。
|
|
9616
|
-
*/
|
|
9617
|
-
var cleanup_exports = /* @__PURE__ */ __exportAll({
|
|
9618
|
-
collectEligibleSessions: () => collectEligibleSessions,
|
|
9619
|
-
cutoffDaysAgo: () => cutoffDaysAgo,
|
|
9620
|
-
findSessionLog: () => findSessionLog,
|
|
9621
|
-
hasSessionLogById: () => hasSessionLogById,
|
|
9622
|
-
performCleanup: () => performCleanup,
|
|
9623
|
-
sessionLogArtifacts: () => sessionLogArtifacts,
|
|
9624
|
-
sessionLogBytesById: () => sessionLogBytesById,
|
|
9625
|
-
sessionsRootDir: () => sessionsRootDir
|
|
9626
|
-
});
|
|
9627
|
-
/** DSH 会话 root:env DSH_HOME → ~/.dsh(config-ops globalConfigPath 同款推导) */
|
|
9628
|
-
function sessionsRootDir() {
|
|
9629
|
-
const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
|
|
9630
|
-
return join(dshHome, "sessions");
|
|
9631
|
-
}
|
|
9632
|
-
/** 一天的毫秒数 */
|
|
9633
|
-
const DAY_MS = 864e5;
|
|
9634
|
-
/**
|
|
9635
|
-
* 扫描 sessions root,列出可清理候选:
|
|
9636
|
-
* - 递归 `<root>/<project>/<session-id>/session.jsonl(.zstd)`(任意深度下找 *session* 文件)
|
|
9637
|
-
* - 过滤 live(liveIds 集合)——安全底线
|
|
9638
|
-
* - 过滤 lastActive >= cutoffMs(最后活动未达阈值 → 保留)
|
|
9639
|
-
* root 不存在/为空 → 返回 [](非错误)。
|
|
9640
|
-
*/
|
|
9641
|
-
function collectEligibleSessions(root, cutoffMs, liveIds = /* @__PURE__ */ new Set()) {
|
|
9642
|
-
if (!existsSync(root)) return [];
|
|
9643
|
-
const out = [];
|
|
9644
|
-
for (const project of readdirSync(root, { withFileTypes: true })) {
|
|
9645
|
-
if (!project.isDirectory()) continue;
|
|
9646
|
-
const projectPath = join(root, project.name);
|
|
9647
|
-
for (const entry of readdirSync(projectPath, { withFileTypes: true })) {
|
|
9648
|
-
if (!entry.isDirectory()) continue;
|
|
9649
|
-
const dirPath = join(projectPath, entry.name);
|
|
9650
|
-
const artifacts = sessionLogArtifacts(dirPath);
|
|
9651
|
-
if (artifacts.length === 0) continue;
|
|
9652
|
-
const log = findSessionLog(dirPath);
|
|
9653
|
-
if (log === null) continue;
|
|
9654
|
-
if (liveIds.has(entry.name)) continue;
|
|
9655
|
-
let mtime = Number.NEGATIVE_INFINITY;
|
|
9656
|
-
for (const artifact of artifacts) try {
|
|
9657
|
-
mtime = Math.max(mtime, statSync(artifact.path).mtimeMs);
|
|
9658
|
-
} catch {}
|
|
9659
|
-
if (!Number.isFinite(mtime)) continue;
|
|
9660
|
-
if (mtime >= cutoffMs) continue;
|
|
9661
|
-
out.push({
|
|
9662
|
-
id: entry.name,
|
|
9663
|
-
project: project.name,
|
|
9664
|
-
logPath: log,
|
|
9665
|
-
lastActiveMs: mtime
|
|
9666
|
-
});
|
|
9667
|
-
}
|
|
9668
|
-
}
|
|
9669
|
-
return out;
|
|
9670
|
-
}
|
|
9671
|
-
/**
|
|
9672
|
-
* 会话日志文件名识别:`session.jsonl` / `session.jsonl.zstd`(= **v0 世代**)/
|
|
9673
|
-
* `session.v<N>.jsonl[.zstd]`(= 第 N 世代,DSH 0.1.5 起按世代写文件)。
|
|
9674
|
-
* 出处:`@deepseek-ai/dsh-session-persistence-jsonl` 的 `parseSessionFormatLogFilename` 同款形态。
|
|
9675
|
-
*/
|
|
9676
|
-
const SESSION_LOG_RE = /^session(?:\.v(\d+))?\.jsonl(\.zstd)?$/;
|
|
9677
|
-
/**
|
|
9678
|
-
* 列出会话目录下的**全部日志世代**,按代递增排序(同代内 `.jsonl` 在前、`.zstd` 在后 = 后者更代表"当前书写格式")。
|
|
9679
|
-
*
|
|
9680
|
-
* 为什么需要(R↓,v1.31.13 修复):宿主自 0.1.5 起改成**按世代写文件**,且迁移**保留源文件**
|
|
9681
|
-
* ⇒ 同一会话常见 `session.jsonl.zstd`(v0)与 `session.v3.jsonl.zstd`(v3)**并存**。
|
|
9682
|
-
* 旧实现只认前两个名字 → 新格式会话对本模块**完全不可见**(静默 no-op:清理永远不生效、磁盘只涨)。
|
|
9683
|
-
*/
|
|
9684
|
-
function sessionLogArtifacts(sessionDirPath) {
|
|
9685
|
-
let names;
|
|
9686
|
-
try {
|
|
9687
|
-
names = readdirSync(sessionDirPath);
|
|
9688
|
-
} catch {
|
|
9689
|
-
return [];
|
|
9690
|
-
}
|
|
9691
|
-
const out = [];
|
|
9692
|
-
for (const name of names) {
|
|
9693
|
-
const m = SESSION_LOG_RE.exec(name);
|
|
9694
|
-
if (m === null) continue;
|
|
9695
|
-
out.push({
|
|
9696
|
-
name,
|
|
9697
|
-
generation: m[1] === void 0 ? 0 : Number(m[1]),
|
|
9698
|
-
path: join(sessionDirPath, name)
|
|
9699
|
-
});
|
|
9700
|
-
}
|
|
9701
|
-
return out.sort((a, b) => a.generation - b.generation || (a.name.endsWith(".zstd") ? 1 : 0) - (b.name.endsWith(".zstd") ? 1 : 0));
|
|
9702
|
-
}
|
|
9703
|
-
/**
|
|
9704
|
-
* 在会话目录下找 session 日志文件;无 → null。
|
|
9705
|
-
* 多世代并存时返回**最高世代**(= 宿主 `findLog` 选中的当前世代;旧行为对单文件目录完全一致)。
|
|
9706
|
-
*/
|
|
9707
|
-
function findSessionLog(sessionDirPath) {
|
|
9708
|
-
const artifacts = sessionLogArtifacts(sessionDirPath);
|
|
9709
|
-
const latest = artifacts[artifacts.length - 1];
|
|
9710
|
-
return latest === void 0 ? null : latest.path;
|
|
9711
|
-
}
|
|
9712
|
-
/**
|
|
9713
|
-
* 执行清理(用户拍板:直接物理删,不过滤不归档;dryRun 预览不删)。
|
|
9714
|
-
* 删除 = rmSync 整个会话目录(recursive + force)。失败逐个记 errors 不中断。
|
|
9715
|
-
*/
|
|
9716
|
-
function performCleanup(root, cutoffMs, liveIds = /* @__PURE__ */ new Set(), opts = {}) {
|
|
9717
|
-
const candidates = collectEligibleSessions(root, cutoffMs, liveIds);
|
|
9718
|
-
if (opts.dryRun) return {
|
|
9719
|
-
candidates,
|
|
9720
|
-
result: null
|
|
9721
|
-
};
|
|
9722
|
-
const result = {
|
|
9723
|
-
deleted: [],
|
|
9724
|
-
errors: []
|
|
9725
|
-
};
|
|
9726
|
-
for (const c of candidates) {
|
|
9727
|
-
const dir = dirOf(c.logPath);
|
|
9728
|
-
try {
|
|
9729
|
-
rmSync(dir, {
|
|
9730
|
-
recursive: true,
|
|
9731
|
-
force: true
|
|
9732
|
-
});
|
|
9733
|
-
result.deleted.push(c.id);
|
|
9734
|
-
} catch (e) {
|
|
9735
|
-
result.errors.push({
|
|
9736
|
-
id: c.id,
|
|
9737
|
-
reason: e.message
|
|
9738
|
-
});
|
|
9739
|
-
}
|
|
9740
|
-
}
|
|
9741
|
-
return {
|
|
9742
|
-
candidates,
|
|
9743
|
-
result
|
|
9744
|
-
};
|
|
9745
|
-
}
|
|
9746
|
-
/** 从日志路径取会话目录(父目录) */
|
|
9747
|
-
function dirOf(logPath) {
|
|
9748
|
-
return logPath.slice(0, logPath.lastIndexOf("/")) || logPath;
|
|
9749
|
-
}
|
|
9750
|
-
/**
|
|
9751
|
-
* 某一 dsh 会话(目录名 / 会话 id)的**当前世代日志体积**(字节)。
|
|
9752
|
-
*
|
|
9753
|
-
* 为什么要它(S142 §0y「旧会话累积 ⇒ dsh 内存崩溃」,所有者令):
|
|
9754
|
-
* · 宿主恢复/继续一条会话必须**重放全部事件**(`scanLog` → `materializeAppendBatch`)
|
|
9755
|
-
* ⇒ **代价 ∝ 日志体积**;
|
|
9756
|
-
* · 实测(`_tmp/measure-session-load.mjs`,照宿主 `scanZstdFrames` 复刻):
|
|
9757
|
-
* 一条 **72.2 MB**(zstd)的日志 = 35 559 个 frame / 252.6 MB 文本 / 62 680 条事件,
|
|
9758
|
-
* **光是读一遍峰值 RSS +1 175 MB**(探针什么都不保留);
|
|
9759
|
-
* · 而宿主的解析缓存 `COLD_LOG_MEMO_MAX_ENTRIES = 2` 是**按条数限、不按字节限**的
|
|
9760
|
-
* (`dsh-session-persistence-jsonl/lib/index.js:2175`)⇒ 两条巨日志 = 上限内。
|
|
9761
|
-
* 🔴 **在本函数之前,ACC 侧没有任何地方知道"绑定的这条会话日志多大"——这个信号根本不存在。**
|
|
9762
|
-
* 这是"靠 agent 自觉 rebuild 不够"的机制层原因(keeper 的 K 量的是**模型上下文**,压缩后回落;
|
|
9763
|
-
* 而**日志文件只增不减** ⇒ rebuild 提醒永远不会因为"日志 72 MB"触发)。
|
|
9764
|
-
*
|
|
9765
|
-
* 只认会话目录(含任意世代日志);返回**最高世代**的日志(= 宿主 `findLog` 选中的当前世代)。
|
|
9766
|
-
* 未命中 / 不可读 → null(非错误,调用方按"未知"处理)。
|
|
9767
|
-
* @param root - DSH 会话 root({@link sessionsRootDir})
|
|
9768
|
-
* @param sessionId - 会话目录名(`.bindings.json` 的键,含 `session-` 前缀)或裸 id
|
|
9769
|
-
* @returns 字节数 + 日志绝对路径;未命中 → null
|
|
9770
|
-
*/
|
|
9771
|
-
function sessionLogBytesById(root, sessionId) {
|
|
9772
|
-
if (sessionId === "" || !existsSync(root)) return null;
|
|
9773
|
-
const dirName = sessionId.startsWith("session-") ? sessionId : `session-${sessionId}`;
|
|
9774
|
-
for (const project of readdirSync(root, { withFileTypes: true })) {
|
|
9775
|
-
if (!project.isDirectory()) continue;
|
|
9776
|
-
const log = findSessionLog(join(root, project.name, dirName));
|
|
9777
|
-
if (log === null) continue;
|
|
9778
|
-
try {
|
|
9779
|
-
return {
|
|
9780
|
-
bytes: statSync(log).size,
|
|
9781
|
-
path: log
|
|
9782
|
-
};
|
|
9783
|
-
} catch {
|
|
9784
|
-
return null;
|
|
9785
|
-
}
|
|
9786
|
-
}
|
|
9787
|
-
return null;
|
|
9788
|
-
}
|
|
9789
|
-
/**
|
|
9790
|
-
* **(c) 绑定一致性兜底**的判据:某会话的日志**是否仍在磁盘上**。
|
|
9791
|
-
*
|
|
9792
|
-
* 🔴 **fail-closed**:sessions root 本身不存在时**一律返回 true(当作"在")**——
|
|
9793
|
-
* 否则换 `DSH_HOME` / 换机器会让"全部绑定看起来都悬空",进而误清整张绑定表。
|
|
9794
|
-
* 只有"root 在、而这个会话目录确实不在"才算悬空。
|
|
9795
|
-
* @param sessionRoot - DSH 会话 root({@link sessionsRootDir})
|
|
9796
|
-
* @param sessionId - 会话目录名(`.bindings.json` 的键)或裸 id
|
|
9797
|
-
*/
|
|
9798
|
-
function hasSessionLogById(sessionRoot, sessionId) {
|
|
9799
|
-
if (!existsSync(sessionRoot)) return true;
|
|
9800
|
-
return sessionLogBytesById(sessionRoot, sessionId) !== null;
|
|
9801
|
-
}
|
|
9802
|
-
/** 便捷:默认 N 天前为 cutoff(供 API/调用方) */
|
|
9803
|
-
function cutoffDaysAgo(days, nowMs = Date.now()) {
|
|
9804
|
-
return nowMs - days * DAY_MS;
|
|
9805
|
-
}
|
|
9806
|
-
//#endregion
|
|
9807
9599
|
//#region src/wake/registry.ts
|
|
9808
9600
|
/**
|
|
9809
9601
|
* wake/registry.ts — 认知轨迹唤醒注册表(D58,S142 2026-09-13)
|
|
@@ -11912,6 +11704,21 @@ async function sendToCognitiveTrajectory(ctx, root, target, message, sender) {
|
|
|
11912
11704
|
let createdNote = "";
|
|
11913
11705
|
if ("error" in acquired) {
|
|
11914
11706
|
if (!acquired.notReady && acquired.noBinding === true) {
|
|
11707
|
+
const carrier = carrierState(root, t.dirName, (id) => {
|
|
11708
|
+
if (hostAgents(ctx)?.get?.(id)) return true;
|
|
11709
|
+
try {
|
|
11710
|
+
return hasSessionLogById(sessionsRootDir(), id);
|
|
11711
|
+
} catch {
|
|
11712
|
+
return true;
|
|
11713
|
+
}
|
|
11714
|
+
});
|
|
11715
|
+
if (carrier.state !== "never") {
|
|
11716
|
+
const why = carrier.state === "occupied" ? `该轨迹曾经由会话 ${carrier.sessionId} 承载,而它此刻不在了` : `该轨迹当前由会话 ${carrier.sessionId} 承载`;
|
|
11717
|
+
return {
|
|
11718
|
+
ok: false,
|
|
11719
|
+
detail: `${t.dirName}:${why} ⇒ **拒绝自动新建载体**(一条轨迹至多一个载体)。\n · 要让它重新跑起来:把那个会话恢复,或用 cognitive_trajectory rebuild 换代(显式 takeover)。\n · 不明所以就先 cognitive_trajectory show ${t.dirName} 看它的交代文件。`
|
|
11720
|
+
};
|
|
11721
|
+
}
|
|
11915
11722
|
acquired = await createAndBindTarget(ctx, root, t.dirName);
|
|
11916
11723
|
if ("error" in acquired) return {
|
|
11917
11724
|
ok: false,
|
|
@@ -12958,22 +12765,16 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12958
12765
|
if (!targetEntry) throw new Error(`Session not found: "${args.name}". Use "list" to see available sessions.`);
|
|
12959
12766
|
const targetDirName = targetEntry.dirName;
|
|
12960
12767
|
const currentDir = currentBoundDirName(exec, scope);
|
|
12961
|
-
|
|
12962
|
-
const force = args.force === true;
|
|
12963
|
-
if (switching && !force) throw new Error(`Session is bound to ${currentDir}. Switching to ${targetDirName} would orphan the current cognitive trajectory. Re-run with --force to switch (or close the current session first).`);
|
|
12768
|
+
if (currentDir !== null && targetDirName !== currentDir) throw new Error(`这个会话已经承载认知轨迹「${currentDir}」,不能改宗到「${targetDirName}」。\n一个会话终身只属于一条认知轨迹(2026-10-20 裁决,--force 已退役)。\n要继续用「${currentDir}」⇒ 不用任何操作;要给「${targetDirName}」开一个载体 ⇒ 新建一条会话再 use。`);
|
|
12964
12769
|
const active = useSession(root, args.name, scope);
|
|
12965
12770
|
const info = getActiveSessionInfo(scope);
|
|
12966
12771
|
if (info) renameDshSessionForActive(ctx, exec, info, args.summary);
|
|
12967
12772
|
const dsh = agentDshSession(exec);
|
|
12968
|
-
if (dsh && info) {
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
12972
|
-
|
|
12973
|
-
sessionId: info.sessionId,
|
|
12974
|
-
...switching ? { note: "forced switch" } : {}
|
|
12975
|
-
});
|
|
12976
|
-
}
|
|
12773
|
+
if (dsh && info) appendBound(dsh, "activate", {
|
|
12774
|
+
dirName: info.dirName,
|
|
12775
|
+
mdPath: info.mdPath,
|
|
12776
|
+
sessionId: info.sessionId
|
|
12777
|
+
});
|
|
12977
12778
|
const advisory = advisoryHint(targetEntry.dirName, join(targetEntry.path, "SESSION.md"));
|
|
12978
12779
|
const out = {
|
|
12979
12780
|
dir: active.dir,
|
|
@@ -12981,10 +12782,6 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12981
12782
|
context: active.context
|
|
12982
12783
|
};
|
|
12983
12784
|
if (advisory) out.advisory = advisory;
|
|
12984
|
-
if (dsh) {
|
|
12985
|
-
const pruned = pruneMissingBindings(root, (id) => !hasSessionLogById(sessionsRootDir(), id));
|
|
12986
|
-
if (pruned.length > 0) out.prunedBindings = pruned;
|
|
12987
|
-
}
|
|
12988
12785
|
return out;
|
|
12989
12786
|
}
|
|
12990
12787
|
case "rebuild": {
|
|
@@ -16343,7 +16140,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
16343
16140
|
const id = s.id ?? s.header?.id;
|
|
16344
16141
|
if (id) liveIds.add(id);
|
|
16345
16142
|
}
|
|
16346
|
-
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await
|
|
16143
|
+
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./cleanup-C-92qnDs.js").then((n) => n.t);
|
|
16347
16144
|
const root = sessionsRootDir();
|
|
16348
16145
|
if (req.method === "GET") {
|
|
16349
16146
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
@@ -18402,7 +18199,6 @@ async function queueRebuild(ctx, opts) {
|
|
|
18402
18199
|
const boundRec = {
|
|
18403
18200
|
dirName: basename(dirname(mdPath)),
|
|
18404
18201
|
mdPath,
|
|
18405
|
-
sessionId: sessionName.startsWith("S") ? sessionName : void 0,
|
|
18406
18202
|
note: "rebuild queued"
|
|
18407
18203
|
};
|
|
18408
18204
|
appendBound(session, "rebuild", boundRec);
|
|
@@ -25189,11 +24985,9 @@ function apply(ctx, config) {
|
|
|
25189
24985
|
(async () => {
|
|
25190
24986
|
try {
|
|
25191
24987
|
const domain = await openBindingDomain(ctx);
|
|
25192
|
-
|
|
24988
|
+
bindingStore(domain);
|
|
25193
24989
|
if (!domain) console.log("[serenity-hooks] §0L 存储域不可用 → 绑定继续走 CCC 内 .bindings.json(向前兼容回落)");
|
|
25194
|
-
} catch {
|
|
25195
|
-
setBindingStore(null);
|
|
25196
|
-
}
|
|
24990
|
+
} catch {}
|
|
25197
24991
|
})();
|
|
25198
24992
|
if (config.tools) {
|
|
25199
24993
|
ctx.tools.register(ccFsTool);
|