dsh-session-ops 0.0.0-stage → 0.2.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.
@@ -0,0 +1,136 @@
1
+ /**
2
+ * 搬迁引擎:把一条会话日志从「旧 cwd 的 project 目录」搬到「新 cwd 的 project 目录」,
3
+ * 并改写 header 里的 cwd。不依赖 cordis,便于单测(见 test/core.test.mjs)。
4
+ *
5
+ * 命名与格式事实(都来自 dsh-session-persistence-jsonl 的实测与源码):
6
+ * - 日志路径 `<root>/<projectKey(cwd)>/<encodeSegment(id)>/session[.vN].jsonl[.zstd]`;
7
+ * - `findLog` 会扫**所有** project 目录找同一个 id,多于一处就抛
8
+ * `duplicate JSONL session id ... appears in multiple project directories`;
9
+ * - generation 文件名必须规范(v0 不带 `vN`),临时名/带后缀的备份名不会被当成 generation。
10
+ * 因此搬迁必须做到「任何时刻都不存在两份规范名的日志」,见 {@link applyPlan} 的三步改名。
11
+ *
12
+ * @module dsh-session-ops/engine
13
+ */
14
+ import type { SessionHeader } from './store/frames.js';
15
+ import { findGeneration, parseGenerationName } from './store/paths.js';
16
+ import type { GenerationEntry } from './store/paths.js';
17
+ /** 迁移中使用的临时文件名后缀(都**不**匹配 generation 规范名)。 */
18
+ export declare const TMP_SUFFIX = ".session-move-tmp";
19
+ export declare const PARKED_SUFFIX = ".session-move-old";
20
+ /**
21
+ * `findGeneration` / `parseGenerationName`(「目录里哪份才算规范 generation」)已经搬到
22
+ * `lib/store/paths.js`——删除路径也要认得同一套命名,规则只该有一份。
23
+ * 这里保留再导出,是为了不打断本仓库既有的导入者(`test/core.test.mjs` 就按
24
+ * `../lib/engine.js` 导入它们)。
25
+ */
26
+ export { findGeneration, parseGenerationName };
27
+ /** {@link planMove} 的选项。 */
28
+ export interface PlanMoveOptions {
29
+ root: string;
30
+ sessionId: string;
31
+ fromCwd: string;
32
+ toCwd: string;
33
+ compression?: string;
34
+ }
35
+ /** 一次搬迁的计划(含 `buffer`,供 {@link applyPlan} 直接落盘)。 */
36
+ export interface MovePlan {
37
+ root: string;
38
+ compression: string;
39
+ sessionId: string;
40
+ fromCwd: string;
41
+ toCwd: string;
42
+ fromKey: string;
43
+ toKey: string;
44
+ generation: GenerationEntry;
45
+ /** 源目录里除 generation 与 `session.lock` 之外的文件(真正会留在原地的)。 */
46
+ otherFiles: string[];
47
+ /** 源目录里是否有写租约文件;有则由 {@link applyPlan} 随目录一起搬走。 */
48
+ leaseFile: string | null;
49
+ otherGenerations: string[];
50
+ sourceDir: string;
51
+ targetDir: string;
52
+ sourcePath: string;
53
+ targetPath: string;
54
+ bytesBefore: number;
55
+ bytesAfter: number;
56
+ frameCount: number | null;
57
+ header: SessionHeader;
58
+ rewrittenHeader: SessionHeader;
59
+ buffer: Buffer;
60
+ }
61
+ /**
62
+ * 计算一次搬迁的计划:读源日志、改写 cwd、给出目标路径。**只读**,不写任何东西。
63
+ *
64
+ * @param options - `{root, sessionId, fromCwd, toCwd, compression}`。
65
+ * @returns 计划对象(含 `buffer`,供 {@link applyPlan} 直接落盘)。
66
+ * @throws 源日志缺失、header 与请求不符、目标已存在、源与目标同目录时。
67
+ */
68
+ export declare function planMove(options: PlanMoveOptions): MovePlan;
69
+ /** {@link applyPlan} 的搬迁报告(HTTP/工具直接回话用的就是它)。 */
70
+ export interface MoveReport {
71
+ sessionId: string;
72
+ from: string;
73
+ to: string;
74
+ generation: string;
75
+ bytesBefore: number;
76
+ bytesAfter: number;
77
+ frames: number | null;
78
+ sourcePath: string;
79
+ targetPath: string;
80
+ moved: boolean;
81
+ /** 目标目录里已有租约文件、无法把源租约搬过去时,记下冲突路径。 */
82
+ leaseConflict?: string;
83
+ /** 搬租约失败时的错误文案。 */
84
+ leaseMoveError?: string;
85
+ /** 租约文件是否搬到了目标目录。 */
86
+ leaseMoved?: boolean;
87
+ /** 清空源目录失败时的错误文案。 */
88
+ sourceDirCleanupError?: string;
89
+ /** 源目录是否被清掉。 */
90
+ removedSourceDir?: boolean;
91
+ /** 源目录里剩下的文件(清空失败时非空)。 */
92
+ leftInSourceDir?: string[];
93
+ /** 落盘后读回的 header。 */
94
+ verifiedHeader?: {
95
+ id: string;
96
+ cwd: string | null;
97
+ version?: number;
98
+ } | null;
99
+ verifiedBytes?: number;
100
+ ok?: boolean;
101
+ }
102
+ /**
103
+ * 执行计划。三步改名保证「任何时刻都不存在两份规范名日志」:
104
+ * 1. 新日志先以临时名写进目标目录(此时还不是 generation);
105
+ * 2. 源文件改名成 `*.session-move-old`(源目录里不再有规范名);
106
+ * 3. 临时名改成规范名(此刻新日志才生效),随后删掉 `.old` 与空的源目录。
107
+ * 崩在 2 与 3 之间时,源数据仍在 `.old` 里、新数据仍在 `.tmp` 里,人工可救。
108
+ *
109
+ * @param plan - {@link planMove} 的返回值。
110
+ * @returns 搬迁报告(含 `dshReadbackSafe` 之外的事实字段,供 HTTP/工具直接回话)。
111
+ */
112
+ export declare function applyPlan(plan: MovePlan): MoveReport;
113
+ /** {@link verifyStoredLog} 的选项。 */
114
+ export interface VerifyStoredLogOptions {
115
+ root: string;
116
+ sessionId: string;
117
+ cwd: string;
118
+ compression?: string;
119
+ }
120
+ /** {@link verifyStoredLog} 的报告(`header`/`bytes`/`magic` 只在可读时出现)。 */
121
+ export interface StoredLogVerification {
122
+ ok: boolean;
123
+ error?: string;
124
+ header?: {
125
+ id: string;
126
+ cwd: string | null;
127
+ };
128
+ bytes?: number;
129
+ magic?: string;
130
+ }
131
+ /**
132
+ * 只读校验:目标位置上是否有一份可解析、id/cwd 都对得上的日志。
133
+ * @param options - `{root, sessionId, cwd, compression}`。
134
+ * @returns `{ok, header, bytes}` 或 `{ok:false, error}`。
135
+ */
136
+ export declare function verifyStoredLog(options: VerifyStoredLogOptions): StoredLogVerification;
@@ -0,0 +1,281 @@
1
+ /**
2
+ * `dsh-session-ops` Host 半边(插件入口)。
3
+ *
4
+ * 这个包把原先两个独立插件合并成**一个** DSH 插件:
5
+ * - `dsh-session-admin`(删除)→ {@link module:dsh-session-ops/admin};
6
+ * - `dsh-session-move`(搬迁)→ {@link module:dsh-session-ops/move}。
7
+ *
8
+ * 两个半边的代码是**逐字搬过来的**(见两个模块头部列出的差异清单),这里只做合并后的
9
+ * 那一层:一份配置 schema、一个活配置对象、一次 `loader/volatile-update` 订阅、
10
+ * 一个 `apply`,把两条路由与两个工具注册出去。
11
+ *
12
+ * ## 对外契约(合并后一条都不许变)
13
+ *
14
+ * - HTTP:`/session-admin/api/<method>` 与 `/session-move/api/<method>` 的方法名、
15
+ * 入参、`{ok:true,value}` / `{ok:false,error:{code,message}}` 响应形状;
16
+ * - 模型工具:`session_admin` 与 `session_move` 的名字、`parameters`、`output.schema`;
17
+ * - 文档与排障产物:`$DSH_HOME/logs/dsh-session-admin.log`(`[时间] 级别 正文`)与
18
+ * `$DSH_HOME/logs/dsh-session-move.log`(`时间 [级别] 正文`)两个文件与两种行格式;
19
+ * - 安全默认:`allowWorkspaceSessions` / `allowLiveSessions` / `moveAllowHosts` /
20
+ * `adminAllowHosts` / `memoryFollow` 一律落在安全的一侧(见 {@link Config})。
21
+ *
22
+ * ## 配置键的合并方式
23
+ *
24
+ * 两半原先各有一个 `allowHosts`,但语义不同(删除半边按 `Host` 头解析 authority,
25
+ * 搬迁半边按 socket 的 `remoteAddress` 精确匹配),所以**拆成两个键**而不是合并:
26
+ *
27
+ * | 旧键(dsh-session-admin) | 旧键(dsh-session-move) | 新键 |
28
+ * | --- | --- | --- |
29
+ * | `enabled` | `enabled` | `enabled`(两半共用一个总开关) |
30
+ * | `allowHosts` | — | `adminAllowHosts` |
31
+ * | — | `allowHosts` | `moveAllowHosts` |
32
+ * | — | `root` | `root` |
33
+ * | `headerDeleteButton` | — | `headerDeleteButton` |
34
+ * | `allowWorkspaceSessions` | — | `allowWorkspaceSessions` |
35
+ * | `allowLiveSessions` | — | `allowLiveSessions` |
36
+ * | `maxBatch` | — | `maxBatch` |
37
+ * | — | `refuseRunning` | `refuseRunning` |
38
+ * | — | `maxSessions` | `maxSessions` |
39
+ * | — | `memoryFollow` | `memoryFollow` |
40
+ *
41
+ * 只有 `enabled` 是**语义合并**(两半原先各一个,现在一个):任一旧值为 `false` 的
42
+ * 组合在新键下都会变成「全开或全关」,profile patch 要按新语义重写。
43
+ *
44
+ * @module dsh-session-ops
45
+ */
46
+ import z from '@deepseek-ai/schemastery';
47
+ import { ADMIN_API_PREFIX, createSessionAdmin, isInside, isTrustedRequest as isTrustedAdminRequest, mountAdmin } from './admin.js';
48
+ import { MOVE_API_PREFIX, isTrustedRequest as isTrustedMoveRequest, mountForkMembershipRepair, mountMove, sweepForkChildrenOnBoot } from './move.js';
49
+ import type { WorkspaceRegistryLike } from './store/registry.js';
50
+ import type { SessionPersistenceLike } from './move.js';
51
+ export { ADMIN_API_PREFIX, MOVE_API_PREFIX, createSessionAdmin, isInside, isTrustedAdminRequest, isTrustedMoveRequest, mountAdmin, mountForkMembershipRepair, mountMove, sweepForkChildrenOnBoot, };
52
+ /**
53
+ * 搬迁半边原来的公开名(`dsh-session-move` 的 `API_PREFIX = '/session-move/api'`)。
54
+ *
55
+ * 合并后两半各有一个前缀,明确的名字是 {@link ADMIN_API_PREFIX} / {@link MOVE_API_PREFIX};
56
+ * 这个别名留着,是为了让按 `dsh-session-move/lib/index.js` 写过 import 的脚本/测试不改也能跑。
57
+ */
58
+ export declare const API_PREFIX = "/session-move/api";
59
+ /** Cordis 插件名(profile 行 id 是 `session-toolbox`,两者刻意不同名)。 */
60
+ export declare const name = "dsh-session-ops";
61
+ /**
62
+ * 依赖的服务。**这不是可选的**:cordis 里没有声明 inject 就拿不到服务,
63
+ * `ctx.webServer` / `ctx.tools` / `ctx.sessions` / `ctx.sessionPersistence` 会静默是
64
+ * undefined——表现是日志照常打「mounted」但路由 404、工具根本没注册、守卫形同虚设。
65
+ *
66
+ * 两份清单的并集:删除半边要 `sessions`(Host 进程里挂着的会话),搬迁半边要
67
+ * `sessionPersistence`(会话根与写入句柄)。`workspaceRegistry` / `webServer` / `tools`
68
+ * 两边都要。
69
+ */
70
+ export declare const inject: string[];
71
+ /** 设置命名空间;与 client 半边 `settings.section` 的 id 必须一致。 */
72
+ export declare const SETTINGS_NS = "session-toolbox";
73
+ /**
74
+ * 插件配置(两份原配置的**并集**,键名与默认值见 {@link module:dsh-session-ops} 的表)。
75
+ *
76
+ * 键分两类,区别是**能不能在线改**:
77
+ *
78
+ * - **旋钮**:标了 `.volatile()` 的字段由 Loader 以活引用(`{ get() }`)交给 `apply`,
79
+ * 写盘后原地更新引用、再补发一次 `loader/volatile-update`。插件页上本行的「配置」
80
+ * 卡片写的就是这 7 个字段,落盘在 profile 的 `cordis.patch.yml` 里,**不重启、不重挂**。
81
+ * - **组合事实**:`enabled`(挂载期决定注册什么)、`root`(挂载期解析会话根)、
82
+ * `adminAllowHosts` / `moveAllowHosts`(挂载期决定 HTTP 信任栅栏)。刻意不标 volatile:
83
+ * 不进表单,要改就改 profile 的 patch(改完本行会重挂一次)。
84
+ *
85
+ * `description` 会随 schema 进入表单描述,是界面文案的权威来源。
86
+ */
87
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
88
+ /** 总开关(两半共用)。关掉后两条路由与两个工具都不注册。改它需要重挂本行(非 volatile)。 */
89
+ enabled: z<boolean, boolean, "defined">;
90
+ /** 会话根目录;留空则从 `sessionPersistence.root` 推断(jsonl 后端会暴露它)。搬迁半边用。 */
91
+ root: z<string, string, "defined">;
92
+ /**
93
+ * 删除半边的额外信任 authority(`host` 或 `host:port`,按 `Host` 头解析)。
94
+ * 原 `dsh-session-admin` 的 `allowHosts`。默认空——只信 loopback。
95
+ */
96
+ adminAllowHosts: z<string[], string[], "defined">;
97
+ /**
98
+ * 搬迁半边的额外信任 authority(`host` 或 `host:port`,按 socket 的 `remoteAddress`
99
+ * 精确匹配)。原 `dsh-session-move` 的 `allowHosts`。默认空——只信 loopback。
100
+ */
101
+ moveAllowHosts: z<string[], string[], "defined">;
102
+ /** 是否允许删「属于某个工作区」的会话。默认否——工作区会话通常还想留。 */
103
+ allowWorkspaceSessions: z<boolean, boolean, "volatile-defined">;
104
+ /** 是否允许删仍然挂在 Host 进程里的会话(正在浏览/正在跑)。默认否。 */
105
+ allowLiveSessions: z<boolean, boolean, "volatile-defined">;
106
+ /** 一次调用最多删多少个,防手滑。 */
107
+ maxBatch: z<number, number, "volatile-defined">;
108
+ /**
109
+ * 是否在会话标题栏显示「删除当前会话」的垃圾桶图标。**默认开启。**
110
+ *
111
+ * 形态刻意是「图标 + 二次确认」而不是带字面标签的按钮:标签会招来点击,而这里
112
+ * 删的正是你正在用的会话。图标不占字面位置,误点最多弹出一个确认框,离真正删除
113
+ * 还隔着一步。清理由右侧「会话管理」面板或设置页承担,那是显式挑选动作。
114
+ *
115
+ * 这是个 volatile 旋钮:浏览器半边在挂载时问一次 Host(`config` 接口),所以
116
+ * 改完**刷新页面**就生效——不需要重启 dsh web,也不需要重挂本插件。
117
+ */
118
+ headerDeleteButton: z<boolean, boolean, "volatile-defined">;
119
+ /** 是否拒绝搬"正在跑一轮"的会话。默认拒绝——搬运期间它的写入句柄可能落盘。 */
120
+ refuseRunning: z<boolean, boolean, "volatile-defined">;
121
+ /** `list` 一次最多返回多少条会话(浏览器半边还要筛选)。 */
122
+ maxSessions: z<number, number, "volatile-defined">;
123
+ /**
124
+ * 项目作用域记忆的条目要不要跟着会话走。**默认 off**。
125
+ *
126
+ * 项目记忆是**工作区共享**的(一次写入、那个工作区里所有会话都读),所以把一条
127
+ * 会话的条目拖到另一个工作区会改变那边的记忆视图——只有"这条会话本来就该在目标
128
+ * 工作区"(纠正错放)时才该这么做:
129
+ * - `off` :什么都不做。报告里仍给出**只读预览**(若开启会复制哪几条)。
130
+ * - `copy` :把该会话产出的条目(frontmatter `session:` 精确匹配)复制到目标
131
+ * 工作区,按文件名去重,源工作区不动。
132
+ * - `move` :同上,但复制后从源工作区移除(不可逆,谨慎)。
133
+ */
134
+ memoryFollow: z<string, string, "volatile-defined">;
135
+ /**
136
+ * 启动时是否做一轮「fork 子会话漏挂工作区」的定向扫描。
137
+ *
138
+ * 创建期的 `session/created` 补挂只挡住「Host fork 时就没挂」。记账写进去之后
139
+ * 又被抹掉的那一类(同机两个 Host 共用 `$DSH_HOME` 时,另一个进程用旧内存快照
140
+ * 重写 `storages/workspace.json`)只有启动期扫描能补回来,所以默认开启。
141
+ *
142
+ * 扫描范围收得很窄:只认带 `parentSession` 的普通 fork 子会话(排除 `origin:
143
+ * subagent` 与 `delegationDepth > 0`),且父会话已被某个工作区记账、子会话
144
+ * `cwd` 规范化后等于该工作区路径。刻意 ungrouped 的普通会话/迁移会话不碰。
145
+ */
146
+ repairForkChildrenOnBoot: z<boolean, boolean, "defined">;
147
+ }>>, Schemastery.ObjectT<NoInfer<{
148
+ /** 总开关(两半共用)。关掉后两条路由与两个工具都不注册。改它需要重挂本行(非 volatile)。 */
149
+ enabled: z<boolean, boolean, "defined">;
150
+ /** 会话根目录;留空则从 `sessionPersistence.root` 推断(jsonl 后端会暴露它)。搬迁半边用。 */
151
+ root: z<string, string, "defined">;
152
+ /**
153
+ * 删除半边的额外信任 authority(`host` 或 `host:port`,按 `Host` 头解析)。
154
+ * 原 `dsh-session-admin` 的 `allowHosts`。默认空——只信 loopback。
155
+ */
156
+ adminAllowHosts: z<string[], string[], "defined">;
157
+ /**
158
+ * 搬迁半边的额外信任 authority(`host` 或 `host:port`,按 socket 的 `remoteAddress`
159
+ * 精确匹配)。原 `dsh-session-move` 的 `allowHosts`。默认空——只信 loopback。
160
+ */
161
+ moveAllowHosts: z<string[], string[], "defined">;
162
+ /** 是否允许删「属于某个工作区」的会话。默认否——工作区会话通常还想留。 */
163
+ allowWorkspaceSessions: z<boolean, boolean, "volatile-defined">;
164
+ /** 是否允许删仍然挂在 Host 进程里的会话(正在浏览/正在跑)。默认否。 */
165
+ allowLiveSessions: z<boolean, boolean, "volatile-defined">;
166
+ /** 一次调用最多删多少个,防手滑。 */
167
+ maxBatch: z<number, number, "volatile-defined">;
168
+ /**
169
+ * 是否在会话标题栏显示「删除当前会话」的垃圾桶图标。**默认开启。**
170
+ *
171
+ * 形态刻意是「图标 + 二次确认」而不是带字面标签的按钮:标签会招来点击,而这里
172
+ * 删的正是你正在用的会话。图标不占字面位置,误点最多弹出一个确认框,离真正删除
173
+ * 还隔着一步。清理由右侧「会话管理」面板或设置页承担,那是显式挑选动作。
174
+ *
175
+ * 这是个 volatile 旋钮:浏览器半边在挂载时问一次 Host(`config` 接口),所以
176
+ * 改完**刷新页面**就生效——不需要重启 dsh web,也不需要重挂本插件。
177
+ */
178
+ headerDeleteButton: z<boolean, boolean, "volatile-defined">;
179
+ /** 是否拒绝搬"正在跑一轮"的会话。默认拒绝——搬运期间它的写入句柄可能落盘。 */
180
+ refuseRunning: z<boolean, boolean, "volatile-defined">;
181
+ /** `list` 一次最多返回多少条会话(浏览器半边还要筛选)。 */
182
+ maxSessions: z<number, number, "volatile-defined">;
183
+ /**
184
+ * 项目作用域记忆的条目要不要跟着会话走。**默认 off**。
185
+ *
186
+ * 项目记忆是**工作区共享**的(一次写入、那个工作区里所有会话都读),所以把一条
187
+ * 会话的条目拖到另一个工作区会改变那边的记忆视图——只有"这条会话本来就该在目标
188
+ * 工作区"(纠正错放)时才该这么做:
189
+ * - `off` :什么都不做。报告里仍给出**只读预览**(若开启会复制哪几条)。
190
+ * - `copy` :把该会话产出的条目(frontmatter `session:` 精确匹配)复制到目标
191
+ * 工作区,按文件名去重,源工作区不动。
192
+ * - `move` :同上,但复制后从源工作区移除(不可逆,谨慎)。
193
+ */
194
+ memoryFollow: z<string, string, "volatile-defined">;
195
+ /**
196
+ * 启动时是否做一轮「fork 子会话漏挂工作区」的定向扫描。
197
+ *
198
+ * 创建期的 `session/created` 补挂只挡住「Host fork 时就没挂」。记账写进去之后
199
+ * 又被抹掉的那一类(同机两个 Host 共用 `$DSH_HOME` 时,另一个进程用旧内存快照
200
+ * 重写 `storages/workspace.json`)只有启动期扫描能补回来,所以默认开启。
201
+ *
202
+ * 扫描范围收得很窄:只认带 `parentSession` 的普通 fork 子会话(排除 `origin:
203
+ * subagent` 与 `delegationDepth > 0`),且父会话已被某个工作区记账、子会话
204
+ * `cwd` 规范化后等于该工作区路径。刻意 ungrouped 的普通会话/迁移会话不碰。
205
+ */
206
+ repairForkChildrenOnBoot: z<boolean, boolean, "defined">;
207
+ }>>, "plain">;
208
+ /**
209
+ * 解包后的插件配置(键名与 {@link Config} 一一对应,值都已收敛成普通值)。
210
+ *
211
+ * 手写而不是从 schemastery 推导:推到 `Config` 的完整类型会把 `.volatile()` 的
212
+ * `Volatile<T>` 包装也带出来,而 `apply` 里看到的永远是解包后的普通值。
213
+ */
214
+ export interface SessionOpsConfig {
215
+ enabled: boolean;
216
+ root: string;
217
+ adminAllowHosts: string[];
218
+ moveAllowHosts: string[];
219
+ allowWorkspaceSessions: boolean;
220
+ allowLiveSessions: boolean;
221
+ maxBatch: number;
222
+ headerDeleteButton: boolean;
223
+ refuseRunning: boolean;
224
+ maxSessions: number;
225
+ memoryFollow: string;
226
+ repairForkChildrenOnBoot: boolean;
227
+ }
228
+ /**
229
+ * 解包 entry 配置里的 `.volatile()` 活引用,并交给 schema 校验补默认值。
230
+ *
231
+ * 实现在 `lib/store/host.js`:先解包输入、再校验、最后**再解包一次**——
232
+ * schemastery 对 `.volatile()` 字段输出的也是活引用,不收敛的话读到的就是 `{ get() }`
233
+ * 对象(HTTP 面上会变成一个空对象)。
234
+ * @param raw - entry 配置或普通配置对象。
235
+ * @returns 普通值配置。
236
+ */
237
+ export declare function resolveConfig(raw?: Record<string, unknown>): SessionOpsConfig;
238
+ /**
239
+ * Host cordis 上下文里本入口用到的那部分。
240
+ *
241
+ * DSH/Cordis 的 `Context` 在编译期拿不到(本包没有把 `@deepseek-ai/cordis` 列为依赖),
242
+ * 而这里只需要这几个成员:`inject` 声明过的服务、`ctx.get()`、`ctx.effect()`、`ctx.on()`。
243
+ */
244
+ export interface SessionOpsContext {
245
+ logger?: Record<string, ((message: string) => unknown) | undefined> | undefined;
246
+ on: (event: string, handler: (...args: any[]) => unknown) => unknown;
247
+ effect: (fn: () => unknown, label?: string) => unknown;
248
+ get: (name: string) => any;
249
+ webServer: {
250
+ register(options: unknown): unknown;
251
+ };
252
+ tools: {
253
+ register(definition: unknown): unknown;
254
+ };
255
+ workspaceRegistry: WorkspaceRegistryLike;
256
+ sessionPersistence?: SessionPersistenceLike | undefined;
257
+ }
258
+ /**
259
+ * 挂载插件:两条路由 + 两个工具。
260
+ *
261
+ * 两个半边共用**同一个活配置对象**(见下),各自的信任栅栏读各自的那个键
262
+ * (`adminAllowHosts` / `moveAllowHosts`),所以 volatile 更新只需要在这里重读一次。
263
+ *
264
+ * 日志刻意保留两份、两种行格式:`$DSH_HOME/logs/dsh-session-admin.log`
265
+ * (`[时间] 级别 正文`)与 `$DSH_HOME/logs/dsh-session-move.log`(`时间 [级别] 正文`)
266
+ * 是合并前就存在的排障产物,合并后依然按各自的格式写各自那份。
267
+ *
268
+ * @param ctx - Host cordis 上下文(已 inject 五个服务)。
269
+ * @param rawConfig - 部署层配置。
270
+ * @returns 无。
271
+ */
272
+ export declare function apply(ctx: SessionOpsContext, rawConfig?: Record<string, unknown>): void;
273
+ /**
274
+ * 会话存储原语全量再导出(`lib/store/` 的五个模块):
275
+ * `dshHome`、`encodeSegment`、`readSessionHeader`、`hasZstd`、`decodeProjectKey` 等。
276
+ *
277
+ * 原来的会话存储共享库根入口就是这个形状;合并成单包后保留同一份公开面,
278
+ * 外部按包名导入的脚本(如 dsh-open-in-dsh)只改包名即可,不必换符号名。
279
+ * 需要显式分层时也可以用子路径 `dsh-session-ops/store`(见 package.json exports)。
280
+ */
281
+ export * from './store/index.js';
@@ -0,0 +1,106 @@
1
+ /**
2
+ * 项目作用域记忆的「随会话走」支持(**可选,默认关**)。
3
+ *
4
+ * 背景:`dsh-cross-memory` 的项目作用域是**工作区级**的 —— 存到
5
+ * `$DSH_HOME/memories/projects/<slug>/entries/<id>.md`,slug 由工作区根目录派生
6
+ * (`slugify(basename)`,只取项目名、与机器路径无关,见其 storage.js)。
7
+ * 所以搬会话**不需要**搬记忆:会话换了 cwd,作用域自然跟到目标工作区。
8
+ * 但"这条会话在源工作区下写下的条目"留在源工作区,如果这次搬迁属于**纠正错放**
9
+ * (会话本来就该在目标工作区),那些条目也该一起过去。本模块只负责这一件事。
10
+ *
11
+ * 两条设计原则:
12
+ * 1. **默认不做**。项目记忆是工作区共享的,把单条会话的条目拖过去会改变另一个
13
+ * 工作区的记忆视图——这种事必须显式开启(`memoryFollow: 'copy' | 'move'`)。
14
+ * 2. **文件级、可逆、可预演**。`dsh-cross-memory` 的存储契约写明「条目文件才是真相源,
15
+ * 索引是可重建的缓存,手改/删除索引会在下次读取时自愈」,所以复制 `.md` 文件
16
+ * 就是受支持的写入方式,也不必依赖它的运行时。
17
+ *
18
+ * @module dsh-session-ops/memory
19
+ */
20
+ /** 一个已存在的项目记忆目录。 */
21
+ export interface ProjectMemoryDir {
22
+ slug: string;
23
+ root: string;
24
+ descriptorFile: string;
25
+ dir: string;
26
+ }
27
+ /** 一条被选中的记忆条目文件。 */
28
+ export interface MemoryFollowEntry {
29
+ id: string;
30
+ title: string;
31
+ sourceFile: string;
32
+ targetFile: string;
33
+ existsInTarget: boolean;
34
+ }
35
+ /** {@link planMemoryFollow} 的计划(**只读**)。 */
36
+ export interface MemoryFollowPlan {
37
+ projectsDir: string;
38
+ source: {
39
+ slug: string;
40
+ root: string;
41
+ dir: string;
42
+ } | null;
43
+ target: {
44
+ slug: string;
45
+ root: string;
46
+ dir: string;
47
+ descriptorFile: string;
48
+ existed: boolean;
49
+ planned?: boolean;
50
+ };
51
+ sessionId: string;
52
+ totalInSource: number;
53
+ matched: MemoryFollowEntry[];
54
+ }
55
+ /** {@link planMemoryFollow} 的选项。 */
56
+ export interface MemoryFollowOptions {
57
+ dshHome: string;
58
+ sessionId: string;
59
+ fromCwd: string;
60
+ toCwd: string;
61
+ }
62
+ /** {@link applyMemoryFollow} 的结果。 */
63
+ export interface MemoryFollowOutcome {
64
+ mode: string;
65
+ copied: number;
66
+ skipped: number;
67
+ removed: number;
68
+ files: string[];
69
+ removeError?: string;
70
+ }
71
+ /** 与 dsh-cross-memory/lib/storage.js 的 slugify 逐字一致。 */
72
+ export declare function slugify(value: unknown): string;
73
+ /**
74
+ * 与 dsh-cross-memory/lib/storage.js 的 projectSlug 逐字一致:**只取项目名**,
75
+ * 不再拼 `sha1(resolve(root))` 后缀。dsh-cross-memory 已把项目 slug 改成
76
+ * 「一台机器上算出来、另一台也对得上」的名字派生(旧的后缀方案会把同一个
77
+ * 项目按机器拆成多个目录,迁移逻辑见其 storage.ts 的 LEGACY_PROJECT_SLUG)。
78
+ */
79
+ export declare function projectSlug(root: string): string;
80
+ /** `$DSH_HOME/memories/projects`。 */
81
+ export declare function projectsDirOf(dshHome: string): string;
82
+ /**
83
+ * 在 projects 目录里按 root 找已存在的项目(优先命中已有 slug,避免算出第二个目录)。
84
+ * @param projectsDir - `…/memories/projects`。
85
+ * @param root - 工作区根目录。
86
+ * @returns `{slug, root, descriptorFile, dir}`,没有时 `undefined`。
87
+ */
88
+ export declare function findProjectByRoot(projectsDir: string, root: string): ProjectMemoryDir | undefined;
89
+ /** 极简 frontmatter 解析:只认 `key: value` 单行(dsh-cross-memory 的条目就是这种形状)。 */
90
+ export declare function parseFrontmatter(text: string): Record<string, string>;
91
+ /**
92
+ * 计算一次「记忆随会话走」的计划。**只读**。
93
+ *
94
+ * @param options - `{dshHome, sessionId, fromCwd, toCwd}`。
95
+ * @returns 计划:源/目标项目、匹配到的条目(按 frontmatter 的 `session:` 归属过滤)。
96
+ */
97
+ export declare function planMemoryFollow(options: MemoryFollowOptions): MemoryFollowPlan;
98
+ /**
99
+ * 执行计划。`copy` 只加不删;`move` 复制后从源项目移除(= 纠正错放)。
100
+ * 目标已有同名条目时跳过(不覆盖、按名去重)。
101
+ *
102
+ * @param plan - {@link planMemoryFollow} 的返回值。
103
+ * @param mode - `'copy'` 或 `'move'`。
104
+ * @returns `{mode, copied, skipped, removed, files}`。
105
+ */
106
+ export declare function applyMemoryFollow(plan: MemoryFollowPlan, mode: string): MemoryFollowOutcome;