dsh-session-ops 0.1.4 → 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.
@@ -22,12 +22,14 @@
22
22
  * 拿不到 `agents` 服务时 admin 保守地把全部驻留会话当成「在跑」(宁可拒绝),
23
23
  * move 则把状态当成未知(`undefined`,界面照常显示)。
24
24
  *
25
+ * 这里声明的都是**最小本地接口**:Cordis/DSH 的宿主对象在编译期拿不到类型,
26
+ * 但本模块只用到这几个字段,把它们写清楚比整片 `any` 更能挡住拼写错误。
27
+ *
25
28
  * @module dsh-session-ops/store/registry
26
29
  */
27
-
30
+ import { messageOf } from './util.js';
28
31
  /** agent 表示「这一轮还在跑」的状态值。 */
29
- export const SESSION_RUNNING_STATUS = 'running'
30
-
32
+ export const SESSION_RUNNING_STATUS = 'running';
31
33
  /**
32
34
  * Host 进程里仍挂着的会话 id 集合(`ctx.sessions.list()`)。
33
35
  *
@@ -37,16 +39,17 @@ export const SESSION_RUNNING_STATUS = 'running'
37
39
  * @returns `Set<string>`。
38
40
  */
39
41
  export function residentSessionIds(sessions) {
40
- const ids = new Set()
41
- try {
42
- const list = typeof sessions?.list === 'function' ? sessions.list() : []
43
- for (const session of list) ids.add(String(session.id))
44
- } catch {
45
- /* 没有会话存储时视为无驻留 */
46
- }
47
- return ids
42
+ const ids = new Set();
43
+ try {
44
+ const list = typeof sessions?.list === 'function' ? sessions.list() : [];
45
+ for (const session of list)
46
+ ids.add(String(session.id));
47
+ }
48
+ catch {
49
+ /* 没有会话存储时视为无驻留 */
50
+ }
51
+ return ids;
48
52
  }
49
-
50
53
  /**
51
54
  * 一条会话的 agent 状态(`ctx.get('agents').get(id)?.status`)。
52
55
  * @param agents - `ctx.get('agents')` 拿到的 agent 服务(可为 undefined)。
@@ -54,14 +57,14 @@ export function residentSessionIds(sessions) {
54
57
  * @returns 状态字符串;没有 agent、拿不到服务或抛错时 `undefined`(**未知**)。
55
58
  */
56
59
  export function agentSessionStatus(agents, sessionId) {
57
- try {
58
- const agent = typeof agents?.get === 'function' ? agents.get(sessionId) : undefined
59
- return agent !== undefined && typeof agent.status === 'string' ? agent.status : undefined
60
- } catch {
61
- return undefined
62
- }
60
+ try {
61
+ const agent = typeof agents?.get === 'function' ? agents.get(sessionId) : undefined;
62
+ return agent !== undefined && typeof agent.status === 'string' ? agent.status : undefined;
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
63
67
  }
64
-
65
68
  /**
66
69
  * 真正「正在跑一轮」的会话:驻留集合里 agent 状态为 {@link SESSION_RUNNING_STATUS} 的那部分。
67
70
  *
@@ -72,37 +75,37 @@ export function agentSessionStatus(agents, sessionId) {
72
75
  * @returns `Set<string>`(可能就是 `residents` 本身)。
73
76
  */
74
77
  export function runningSessionIds(residents, agents) {
75
- if (agents === undefined || typeof agents.get !== 'function') return residents
76
- const running = new Set()
77
- for (const id of residents) {
78
- try {
79
- if (agents.get(id)?.status === SESSION_RUNNING_STATUS) running.add(id)
80
- } catch {
81
- running.add(id)
78
+ if (agents === undefined || typeof agents.get !== 'function')
79
+ return residents;
80
+ const running = new Set();
81
+ for (const id of residents) {
82
+ try {
83
+ if (agents.get(id)?.status === SESSION_RUNNING_STATUS)
84
+ running.add(id);
85
+ }
86
+ catch {
87
+ running.add(id);
88
+ }
82
89
  }
83
- }
84
- return running
90
+ return running;
85
91
  }
86
-
87
92
  /**
88
93
  * 工作区的**完整记账**成员表(`workspace.record.sessionIds`)。
89
94
  * @param workspace - 工作区实体。
90
95
  * @returns id 数组;实体没有 record 视图时 `undefined`。
91
96
  */
92
97
  export function accountedSessionIds(workspace) {
93
- const record = workspace?.record
94
- return record !== undefined && Array.isArray(record.sessionIds) ? record.sessionIds : undefined
98
+ const record = workspace?.record;
99
+ return record !== undefined && Array.isArray(record.sessionIds) ? record.sessionIds : undefined;
95
100
  }
96
-
97
101
  /**
98
102
  * 归档集合(`registry.archivedSessionIds`)。
99
103
  * @param registry - `ctx.workspaceRegistry`。
100
104
  * @returns `Set<string>`(id 归一成字符串)。
101
105
  */
102
106
  export function archivedSessionIds(registry) {
103
- return new Set(Array.isArray(registry.archivedSessionIds) ? registry.archivedSessionIds.map(String) : [])
107
+ return new Set(Array.isArray(registry.archivedSessionIds) ? registry.archivedSessionIds.map(String) : []);
104
108
  }
105
-
106
109
  /**
107
110
  * 按**完整记账**找出会话所属的工作区。
108
111
  * @param workspaces - 工作区实体数组(通常是 `registry.list()`)。
@@ -110,13 +113,13 @@ export function archivedSessionIds(registry) {
110
113
  * @returns 工作区实体,或 `undefined`。
111
114
  */
112
115
  export function workspaceOwningSession(workspaces, sessionId) {
113
- for (const workspace of workspaces) {
114
- const ids = accountedSessionIds(workspace)
115
- if (ids !== undefined && ids.includes(sessionId)) return workspace
116
- }
117
- return undefined
116
+ for (const workspace of workspaces) {
117
+ const ids = accountedSessionIds(workspace);
118
+ if (ids !== undefined && ids.includes(sessionId))
119
+ return workspace;
120
+ }
121
+ return undefined;
118
122
  }
119
-
120
123
  /**
121
124
  * 按 id → path → title 的顺序找一个工作区(后两者不区分大小写)。
122
125
  * @param workspaces - 工作区实体数组(通常是 `registry.list()`)。
@@ -124,14 +127,20 @@ export function workspaceOwningSession(workspaces, sessionId) {
124
127
  * @returns 工作区实体,或 `undefined`。
125
128
  */
126
129
  export function findWorkspace(workspaces, key) {
127
- if (typeof key !== 'string' || key.length === 0) return undefined
128
- for (const workspace of workspaces) if (String(workspace.id) === key) return workspace
129
- const lowered = key.toLowerCase()
130
- for (const workspace of workspaces) if (String(workspace.path).toLowerCase() === lowered) return workspace
131
- for (const workspace of workspaces) if (String(workspace.title).toLowerCase() === lowered) return workspace
132
- return undefined
130
+ if (typeof key !== 'string' || key.length === 0)
131
+ return undefined;
132
+ for (const workspace of workspaces)
133
+ if (String(workspace.id) === key)
134
+ return workspace;
135
+ const lowered = key.toLowerCase();
136
+ for (const workspace of workspaces)
137
+ if (String(workspace.path).toLowerCase() === lowered)
138
+ return workspace;
139
+ for (const workspace of workspaces)
140
+ if (String(workspace.title).toLowerCase() === lowered)
141
+ return workspace;
142
+ return undefined;
133
143
  }
134
-
135
144
  /**
136
145
  * 把一个即将被删除的会话从它登记过的每个持久结构里摘掉。
137
146
  *
@@ -148,17 +157,17 @@ export function findWorkspace(workspaces, key) {
148
157
  * @returns 全部持久写入完成之后 resolve;值是真正被 detach 的工作区 id 列表。
149
158
  */
150
159
  export async function releaseSessionEverywhere(registry, sessionId) {
151
- const detached = []
152
- for (const workspace of registry.list()) {
153
- if (!workspace.sessionIds.includes(sessionId)) continue
154
- await workspace.detachSession(sessionId)
155
- detached.push(String(workspace.id))
156
- }
157
- await registry.unarchiveSession(sessionId)
158
- await registry.unpinSession(sessionId)
159
- return detached
160
+ const detached = [];
161
+ for (const workspace of registry.list()) {
162
+ if (!workspace.sessionIds.includes(sessionId))
163
+ continue;
164
+ await workspace.detachSession(sessionId);
165
+ detached.push(String(workspace.id));
166
+ }
167
+ await registry.unarchiveSession(sessionId);
168
+ await registry.unpinSession(sessionId);
169
+ return detached;
160
170
  }
161
-
162
171
  /**
163
172
  * 持久化后端的**写入句柄表**(`persistence.tracker.writers`)。
164
173
  *
@@ -167,10 +176,11 @@ export async function releaseSessionEverywhere(registry, sessionId) {
167
176
  * @returns `Map<id, handle>`,或 `undefined`(后端不暴露 tracker 时)。
168
177
  */
169
178
  export function sessionWriteHandles(persistence) {
170
- const tracker = persistence?.tracker
171
- return tracker !== undefined && tracker.writers instanceof Map ? tracker.writers : undefined
179
+ const tracker = persistence?.tracker;
180
+ return tracker !== undefined && tracker.writers instanceof Map
181
+ ? tracker.writers
182
+ : undefined;
172
183
  }
173
-
174
184
  /**
175
185
  * 把「这条会话以后往哪写」在**内存里**也改成新 cwd。
176
186
  *
@@ -189,35 +199,36 @@ export function sessionWriteHandles(persistence) {
189
199
  * ——`flushError` 只在 flush 抛错时出现;`liveSession` 是驻留会话对象(没有则 undefined)。
190
200
  */
191
201
  export async function redirectSessionWriters(options) {
192
- const { writers, sessions, registry, sessionId, cwd } = options
193
- const outcome = {
194
- writeHandleRedirected: false,
195
- liveHeaderUpdated: false,
196
- registryHeaderCacheUpdated: false,
197
- liveSession: undefined,
198
- }
199
- const handle = writers === undefined ? undefined : writers.get(sessionId)
200
- if (handle !== undefined && handle !== null && typeof handle.header === 'object') {
201
- try {
202
- await handle.flush()
203
- } catch (error) {
204
- outcome.flushError = String(error?.message ?? error)
202
+ const { writers, sessions, registry, sessionId, cwd } = options;
203
+ const outcome = {
204
+ writeHandleRedirected: false,
205
+ liveHeaderUpdated: false,
206
+ registryHeaderCacheUpdated: false,
207
+ liveSession: undefined,
208
+ };
209
+ const handle = writers === undefined ? undefined : writers.get(sessionId);
210
+ if (handle !== undefined && handle !== null && typeof handle.header === 'object') {
211
+ try {
212
+ await handle.flush();
213
+ }
214
+ catch (error) {
215
+ outcome.flushError = messageOf(error);
216
+ }
217
+ handle.header = { ...handle.header, cwd };
218
+ outcome.writeHandleRedirected = true;
219
+ }
220
+ const live = typeof sessions?.get === 'function' ? sessions.get(sessionId) : undefined;
221
+ outcome.liveSession = live;
222
+ if (live !== undefined) {
223
+ live.header = { ...live.header, cwd };
224
+ outcome.liveHeaderUpdated = true;
205
225
  }
206
- handle.header = { ...handle.header, cwd }
207
- outcome.writeHandleRedirected = true
208
- }
209
- const live = typeof sessions?.get === 'function' ? sessions.get(sessionId) : undefined
210
- outcome.liveSession = live
211
- if (live !== undefined) {
212
- live.header = { ...live.header, cwd }
213
- outcome.liveHeaderUpdated = true
214
- }
215
- if (registry.headers instanceof Map) {
216
- const cached = registry.headers.get(sessionId)
217
- if (cached !== undefined) {
218
- registry.headers.set(sessionId, { ...cached, cwd })
219
- outcome.registryHeaderCacheUpdated = true
226
+ if (registry.headers instanceof Map) {
227
+ const cached = registry.headers.get(sessionId);
228
+ if (cached !== undefined) {
229
+ registry.headers.set(sessionId, { ...cached, cwd });
230
+ outcome.registryHeaderCacheUpdated = true;
231
+ }
220
232
  }
221
- }
222
- return outcome
233
+ return outcome;
223
234
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 本包内部的小工具:只放「宿主边界上重复出现、但语义必须逐字一致」的那几行。
3
+ *
4
+ * 目前只有一处:把 `unknown` 类型抛出的错误取成可读文案。JS 里的写法是
5
+ * `String(error?.message ?? error)`——它同时容忍 Error、任意带 `message` 的对象、
6
+ * 以及 `throw 'oops'` 这样的原始值。TS 的 `catch` 变量是 `unknown`,这份语义必须
7
+ * 原样保留(错误文案会被用户可见的报告与日志引用),所以收成一个函数。
8
+ *
9
+ * @module dsh-session-ops/store/util
10
+ */
11
+ /**
12
+ * 取一个 unknown 错误的可读文案,语义与 `String(error?.message ?? error)` 逐字一致。
13
+ * @param error - catch 到的错误。
14
+ * @returns 可读文案。
15
+ */
16
+ export function messageOf(error) {
17
+ const candidate = error;
18
+ return String(candidate?.message ?? error);
19
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * `dsh-session-ops` 的**删除半边**(原 `dsh-session-admin` 插件的 Host 半边)。
3
+ *
4
+ * 这一半的代码搬自原 `dsh-session-admin` 的 `lib/index.js`,只有四处差异:
5
+ * - 插件 entry 的 `name` / `inject` / `SETTINGS_NS` / `Config` / `resolveConfig` / `apply`
6
+ * 上移到 `lib/index.js`——合并后的包只有一个 entry、一份配置 schema、一个 `apply`;
7
+ * - `API_PREFIX` 改名 `ADMIN_API_PREFIX`(搬迁半边占着 `MOVE_API_PREFIX`);
8
+ * - 配置键 `allowHosts` 改名 `adminAllowHosts`(两半的信任栅栏语义不同,见 `lib/index.js`);
9
+ * - 原 `apply` 里"建配置/建日志/订阅 volatile/看 enabled"那段上移到入口,剩下的接线
10
+ * 成为 `mountAdmin(ctx, config, log)`。
11
+ * 其余一字未动:扫描与范围筛选、逐条守卫、`/session-admin/api` 的方法表、
12
+ * `session_admin` 工具定义、Host 头版信任栅栏,连注释都保持原样。
13
+ *
14
+ * 能力:把会话的持久化数据真正从磁盘移除。一个会话在磁盘上最多占三处:
15
+ * - `<home>/sessions/<项目目录键>/<session-id>/` ← 会话日志(jsonl.zstd)
16
+ * - `<home>/storages/session_projcache/sessions/<session-id>.json` ← 投影缓存
17
+ * - `<home>/rewind-snapshots/<session-id>/` ← 回退快照(可有可无)
18
+ * 三处一起删,并顺手清掉因此变空的项目目录。
19
+ *
20
+ * 刻意零 `@deepseek-ai/*` 运行时依赖:只用 Node 内置模块和一份 schemastery 配置
21
+ * schema。工具注册用普通对象(与 `dsh-subagent-in-process-driver` 同款),不做
22
+ * `defineTool` 导入——profile 的 node_modules 里未必解析得到 `@deepseek-ai/dsh-tools`。
23
+ * 代价是 `parameters` / `output.schema` 都必须自己写成**已编译的 JSON Schema**:
24
+ * register 不做编译,只把对象原样透传给 provider(详见 `TOOL_PARAMETERS` 注释)。
25
+ *
26
+ * @module dsh-session-ops/admin
27
+ */
28
+ import { dshHome } from './store/host.js';
29
+ import type { LogFn } from './store/host.js';
30
+ import type { HttpRequest } from './store/http.js';
31
+ import { decodeProjectKey } from './store/paths.js';
32
+ import type { WorkspaceRegistryLike } from './store/registry.js';
33
+ import type { SessionOpsConfig } from './index.js';
34
+ /** HTTP 路由前缀(两半各一条,合并后仍然分开)。 */
35
+ export declare const ADMIN_API_PREFIX = "/session-admin/api";
36
+ /**
37
+ * `$DSH_HOME`(环境变量优先,默认 `~/.dsh`)。
38
+ *
39
+ * 实现在 `lib/store/host.js`——它与搬迁半边用的是同一份;
40
+ * 这里再导出,保持本仓库的公开面不变。
41
+ */
42
+ export { dshHome };
43
+ /**
44
+ * 把 `sessions/` 下的项目目录键还原成路径。
45
+ *
46
+ * 实现在 `lib/store/paths.js`:它与搬迁半边的 `projectKey`
47
+ * 是同一套规则的两个方向,规则只该有一份。这里再导出,保持本仓库的公开面不变。
48
+ */
49
+ export { decodeProjectKey };
50
+ /** 路径是否落在某个根目录内部(防目录穿越)。 */
51
+ export declare function isInside(root: string, candidate: string): boolean;
52
+ /** 扫描出来的一条会话记录。 */
53
+ export interface SessionRecord {
54
+ id: string;
55
+ dir: string;
56
+ project: string;
57
+ cwd: string | null;
58
+ title: string | null;
59
+ origin: string | null;
60
+ parentSessionId: string | null;
61
+ isSeeded: boolean | null;
62
+ headerReadable: boolean;
63
+ workspaceId: string | null;
64
+ workspaceTitle: string | null;
65
+ ungrouped: boolean;
66
+ archived: boolean;
67
+ subagent: boolean;
68
+ cwdMissing: boolean;
69
+ temp: boolean;
70
+ bytes: number;
71
+ files: number;
72
+ updatedAt: number;
73
+ hasProjCache: boolean;
74
+ hasRewind: boolean;
75
+ }
76
+ /** {@link SessionAdmin.scan} 的选项。 */
77
+ export interface ScanOptions {
78
+ /** `false` 跳过逐会话的体积统计(只要个数的调用方用它压开销)。 */
79
+ measure?: boolean;
80
+ }
81
+ /** {@link SessionAdmin.select} 的范围查询。 */
82
+ export interface SelectQuery {
83
+ scope?: string;
84
+ cwdPrefix?: string;
85
+ search?: string;
86
+ }
87
+ /** {@link SessionAdmin.remove} 的选项。 */
88
+ export interface RemoveOptions {
89
+ dryRun?: boolean;
90
+ includeWorkspace?: boolean;
91
+ includeArchived?: boolean;
92
+ includeSubagent?: boolean;
93
+ includeLive?: boolean;
94
+ /** **正在跑**、因此现在不能删的 id 集合。 */
95
+ live?: Set<string>;
96
+ callerSessionId?: string;
97
+ maxBatch?: number;
98
+ }
99
+ /** {@link SessionAdmin.remove} 里逐条的结果。 */
100
+ export interface RemoveResult {
101
+ id: string;
102
+ status: 'skipped' | 'would-remove' | 'failed' | 'removed';
103
+ reason?: string;
104
+ bytes?: number;
105
+ }
106
+ /** {@link SessionAdmin.remove} 的汇总。 */
107
+ export interface RemoveReport {
108
+ /** 早退(目标集合为空)时这个键不存在——与改动前的返回形状一致。 */
109
+ dryRun?: boolean;
110
+ removed: number;
111
+ reclaimed: number;
112
+ pruned: string[];
113
+ results: RemoveResult[];
114
+ }
115
+ /** {@link createSessionAdmin} 的选项。 */
116
+ export interface CreateSessionAdminOptions {
117
+ /** DSH 主目录(默认 `dshHome()`)。 */
118
+ home?: string;
119
+ /** 视为「临时目录」的前缀。 */
120
+ tempRoots?: string[];
121
+ /** 删文件**之前**把一个会话从登记表里摘掉的异步回调。 */
122
+ releaseSession?: (sessionId: string) => Promise<unknown>;
123
+ }
124
+ /** 会话管理员(扫描 / 过滤 / 删除三个入口)。 */
125
+ export interface SessionAdmin {
126
+ home: string;
127
+ sessionsRoot: string;
128
+ projcacheRoot: string;
129
+ rewindRoot: string;
130
+ scan(options?: ScanOptions): SessionRecord[];
131
+ select(records: SessionRecord[], query?: SelectQuery): SessionRecord[];
132
+ remove(ids: string[], options?: RemoveOptions): Promise<RemoveReport>;
133
+ }
134
+ /**
135
+ * 构造一个会话管理员。所有根目录都可注入,便于在测试里指向沙箱。
136
+ * @param options - `home`(DSH 主目录)、`tempRoots`(视为「临时目录」的前缀)、
137
+ * `releaseSession`(删文件**之前**把一个会话从登记表里摘掉的异步回调;见 `remove`)。
138
+ * @returns 扫描 / 过滤 / 删除三个入口。
139
+ */
140
+ export declare function createSessionAdmin(options?: CreateSessionAdminOptions): SessionAdmin;
141
+ /**
142
+ * 判断一个请求是否有权访问本插件的路由。
143
+ *
144
+ * 本机 `dsh web` 常被绑到 `0.0.0.0`(局域网访问),所以「能连上端口」不等于
145
+ * 「是这台机器上的人」:非回环 authority 默认一律拒绝,除非在配置的
146
+ * `allowHosts` 里显式列出。同时拒绝 `sec-fetch-site: cross-site` 和跨源 Origin。
147
+ *
148
+ * **这份栅栏刻意留在本仓库**:`dsh-session-move` 那份按 socket 的 `remoteAddress`
149
+ * 判断回环、`allowHosts` 只做精确字符串匹配,既不解析 authority 也不看
150
+ * `sec-fetch-site`/`Origin`——两边语义不同,合并必然改掉其中一边的对外行为。
151
+ * 共享包只提供路由骨架(`createApiHandler`),栅栏以回调形式传进去。
152
+ * @param req - Node HTTP 请求。
153
+ * @param allowHosts - 额外信任的 authority 列表。
154
+ * @returns 是否放行。
155
+ */
156
+ export declare function isTrustedRequest(req: HttpRequest, allowHosts?: string[]): boolean;
157
+ /**
158
+ * Host cordis 上下文里删除半边用到的那部分(最小本地接口,见 `src/index.ts` 的说明)。
159
+ */
160
+ export interface AdminContext {
161
+ logger?: Record<string, ((message: string) => unknown) | undefined> | undefined;
162
+ effect: (fn: () => unknown, label?: string) => unknown;
163
+ get: (name: string) => any;
164
+ webServer: {
165
+ register(options: unknown): unknown;
166
+ };
167
+ tools: {
168
+ register(definition: unknown): unknown;
169
+ };
170
+ workspaceRegistry: WorkspaceRegistryLike;
171
+ }
172
+ /**
173
+ * 挂载删除半边:路由 + 工具。
174
+ *
175
+ * 与搬迁半边刻意不同的三点(原样保留的理由):
176
+ * - 信任栅栏按 `Host` 头判断(`isTrustedRequest`),额外拒绝 `sec-fetch-site: cross-site`
177
+ * 与跨源 `Origin`;搬迁半边看 socket 的 `remoteAddress`;
178
+ * - 响应带 `content-length`;
179
+ * - 请求体按**宽松模式**读(空白 body 走 `JSON.parse` 报错,非对象不拦)。
180
+ *
181
+ * @param ctx - Host cordis 上下文(`webServer` / `tools` / `sessions` / `workspaceRegistry`)。
182
+ * @param config - 已解包的**活配置对象**:身份由入口保持,volatile 更新时原地覆盖,
183
+ * 所以下面这些闭包自动看到最新值。
184
+ * @param log - `(level, message) => void`(入口传的是 admin 那套行格式的文件日志)。
185
+ * @returns 无。
186
+ */
187
+ export declare function mountAdmin(ctx: AdminContext, config: SessionOpsConfig, log: LogFn): void;
@@ -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;