@michengai/dsh-archive-manager 0.1.26 → 0.1.28

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/lib/workspace.js CHANGED
@@ -1,703 +1,658 @@
1
1
  import { rm } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
  import {
4
- WorkspaceRegistry
4
+ WorkspaceRegistry
5
5
  } from "@deepseek-ai/dsh-workspace";
6
6
  import { bindTypertRemote, Remote } from "@deepseek-ai/dsh-typert-protocol";
7
7
  import { sessionDir } from "@deepseek-ai/dsh-spill-local";
8
8
  import { trackTombstone } from "./tombstone.js";
9
- //#region lib/types/index.js
10
- /**
11
- * dsh-archive-manager 宿主侧归档会话管理。
12
- *
13
- * `deleteSession(sessionId)` 的顺序即语义:校验已知会话;实时会话先
14
- * flush 再 detach;等待投影缓存写入完成;移除归档标记和工作区记账;
15
- * 删除投影缓存,级联删除 SUBAGENT 子会话并清理 spill;最后才删除转录
16
- * 目录。fork 分支虽有 `parentSession`,但属于独立用户会话,不参与级联。
17
- *
18
- * 父会话的可失败记账清理先于其转录目录删除;父目录删除失败时保留头部
19
- * 索引,允许再次调用同一入口完成删除。SUBAGENT 级联跨多个转录目录,无法
20
- * 组成事务:子会话可能已先删除,重试会跳过它们并继续处理仍保留的父会话。
21
- * 物理删除成功后必须遗忘父类内存索引(headers / sessionPaths /
22
- * invalidSessionPaths)并打上删除墓碑:父类 sessionKnown 以 headers.has
23
- * 短路,indexHeaders 只增不减,stale list() 否则会把已删 id 救活,
24
- * 进而被 archiveSession 重新写回 archivedSessionIds。
25
- * 墓碑同时记录被删生命周期的日志身份(createdAt/cwd):其他进程以同 id
26
- * 重建并落盘新会话时(冷复用),sessionKnown 的探针以身份差异区分新旧
27
- * 生命周期并撤掉墓碑,避免已重建的会话在本进程永久 UNKNOWN_SESSION。
28
- * 单会话与批量恢复/删除方法均通过 Typert Remote 暴露给浏览器,并注册到
29
- * typert.local,避免生产环境只靠 SRC 扫描时 404。
30
- */
31
9
  function markRemoteMethod(instance, method) {
32
- // 模拟 TS 装饰器管线 `@Remote(method)`:`Remote` 返回标准方法装饰器,
33
- // 这里构造一个 addInitializer 立即以 `this` = instance 执行的装饰器上下文。
34
- const context = {
35
- private: false,
36
- static: false,
37
- name: method,
38
- addInitializer(fn) {
39
- fn.call(instance);
40
- }
41
- };
42
- Remote(method)(void 0, context);
10
+ const context = {
11
+ private: false,
12
+ static: false,
13
+ name: method,
14
+ addInitializer(fn) {
15
+ fn.call(instance);
16
+ }
17
+ };
18
+ Remote(method)(void 0, context);
43
19
  }
44
20
  function unknownSessionMessage(sessionId) {
45
- return `unknown session "${sessionId}" (UNKNOWN_SESSION)`;
21
+ return `unknown session "${sessionId}" (UNKNOWN_SESSION)`;
46
22
  }
47
23
  var ArchiveUnknownSessionError = class extends Error {
48
- sessionId;
49
- constructor(sessionId) {
50
- super(unknownSessionMessage(sessionId));
51
- this.sessionId = sessionId;
52
- this.name = "ArchiveUnknownSessionError";
53
- }
54
- }
55
- /** 头部投影到“日志身份”字段(与投影缓存的 identity 语义一致)。cwd 缺失统一归一为 null,避免一侧带键一侧不带键时的比较歧义。 */
24
+ sessionId;
25
+ constructor(sessionId) {
26
+ super(unknownSessionMessage(sessionId));
27
+ this.sessionId = sessionId;
28
+ this.name = "ArchiveUnknownSessionError";
29
+ }
30
+ };
56
31
  function headerIdentity(header) {
57
- return {
58
- createdAt: header.createdAt,
59
- cwd: header.cwd ?? null
60
- };
32
+ return {
33
+ createdAt: header.createdAt,
34
+ cwd: header.cwd ?? null
35
+ };
61
36
  }
62
37
  const sessionIdSchema = {
63
- parse(value) {
64
- if (typeof value !== "string" || value.length === 0) throw new TypeError(`sessionId must be a non-empty string, got ${String(value)}`);
65
- return value;
66
- }
38
+ parse(value) {
39
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`sessionId must be a non-empty string, got ${String(value)}`);
40
+ return value;
41
+ }
67
42
  };
68
43
  const workspaceIdSchema = {
69
- parse(value) {
70
- if (typeof value !== "string" || value.length === 0) throw new TypeError(`workspaceId must be a non-empty string, got ${String(value)}`);
71
- return value;
72
- }
44
+ parse(value) {
45
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`workspaceId must be a non-empty string, got ${String(value)}`);
46
+ return value;
47
+ }
73
48
  };
74
49
  const archivedSetSchema = {
75
- parse(value) {
76
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
77
- const ids = value.archivedSessionIds;
78
- if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
79
- return value;
80
- }
50
+ parse(value) {
51
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
52
+ const ids = value.archivedSessionIds;
53
+ if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
54
+ return value;
55
+ }
81
56
  };
82
57
  const deletedSchema = {
83
- parse(value) {
84
- if (typeof value !== "object" || value === null || Array.isArray(value) || value.deleted !== true) throw new TypeError("deleted must be true");
85
- return value;
86
- }
58
+ parse(value) {
59
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value.deleted !== true) throw new TypeError("deleted must be true");
60
+ return value;
61
+ }
87
62
  };
88
63
  const archivedBatchTargetSchema = {
89
- parse(value) {
90
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("target must be an object");
91
- if (value.scope === "all" || value.scope === "ungrouped") return value;
92
- if (value.scope === "workspace" && typeof value.workspaceId === "string" && value.workspaceId.length > 0) return value;
93
- if (value.scope === "sessions" && Array.isArray(value.sessionIds) && value.sessionIds.length > 0 && value.sessionIds.every((id) => typeof id === "string" && id.length > 0)) return value;
94
- throw new TypeError("target.scope must be all, ungrouped, workspace with a non-empty workspaceId, or sessions with non-empty sessionIds");
95
- }
64
+ parse(value) {
65
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("target must be an object");
66
+ if (value.scope === "all" || value.scope === "ungrouped") return value;
67
+ if (value.scope === "workspace" && typeof value.workspaceId === "string" && value.workspaceId.length > 0) return value;
68
+ if (value.scope === "sessions" && Array.isArray(value.sessionIds) && value.sessionIds.length > 0 && value.sessionIds.every((id) => typeof id === "string" && id.length > 0)) return value;
69
+ throw new TypeError("target.scope must be all, ungrouped, workspace with a non-empty workspaceId, or sessions with non-empty sessionIds");
70
+ }
96
71
  };
97
72
  const unarchivedBatchSchema = {
98
- parse(value) {
99
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
100
- if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
101
- if (!Array.isArray(value.unarchivedSessionIds) || value.unarchivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("unarchivedSessionIds must be a string array");
102
- return value;
103
- }
73
+ parse(value) {
74
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
75
+ if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
76
+ if (!Array.isArray(value.unarchivedSessionIds) || value.unarchivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("unarchivedSessionIds must be a string array");
77
+ return value;
78
+ }
104
79
  };
105
80
  const archivedWorkspaceBatchSchema = {
106
- parse(value) {
107
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
108
- if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
109
- if (!Array.isArray(value.archivedSessionIdsAdded) || value.archivedSessionIdsAdded.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIdsAdded must be a string array");
110
- return value;
111
- }
81
+ parse(value) {
82
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
83
+ if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
84
+ if (!Array.isArray(value.archivedSessionIdsAdded) || value.archivedSessionIdsAdded.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIdsAdded must be a string array");
85
+ return value;
86
+ }
112
87
  };
113
88
  const deletedBatchSchema = {
114
- parse(value) {
115
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
116
- for (const key of ["requestedSessionIds", "deletedSessionIds", "skippedSessionIds"]) {
117
- if (!Array.isArray(value[key]) || value[key].some((id) => typeof id !== "string")) throw new TypeError(`${key} must be a string array`);
118
- }
119
- if (!Array.isArray(value.failures) || value.failures.some((failure) => typeof failure !== "object" || failure === null || typeof failure.sessionId !== "string" || typeof failure.message !== "string")) throw new TypeError("failures must contain sessionId/message objects");
120
- return value;
121
- }
89
+ parse(value) {
90
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
91
+ for (const key of ["requestedSessionIds", "deletedSessionIds", "skippedSessionIds"]) {
92
+ if (!Array.isArray(value[key]) || value[key].some((id) => typeof id !== "string")) throw new TypeError(`${key} must be a string array`);
93
+ }
94
+ if (!Array.isArray(value.failures) || value.failures.some((failure) => typeof failure !== "object" || failure === null || typeof failure.sessionId !== "string" || typeof failure.message !== "string")) throw new TypeError("failures must contain sessionId/message objects");
95
+ return value;
96
+ }
122
97
  };
123
98
  const archivedSessionMetadataSchema = {
124
- parse(value) {
125
- if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.items)) throw new TypeError("result.items must be an array");
126
- if (value.items.some((item) => typeof item !== "object" || item === null || typeof item.sessionId !== "string" || typeof item.createdAt !== "number" || !Number.isFinite(item.createdAt))) throw new TypeError("items must contain sessionId/createdAt objects");
127
- if (value.repairedSessionIds !== void 0 && (!Array.isArray(value.repairedSessionIds) || value.repairedSessionIds.some((id) => typeof id !== "string"))) throw new TypeError("repairedSessionIds must be a string array");
128
- return value;
129
- }
99
+ parse(value) {
100
+ if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.items)) throw new TypeError("result.items must be an array");
101
+ if (value.items.some((item) => typeof item !== "object" || item === null || typeof item.sessionId !== "string" || typeof item.createdAt !== "number" || !Number.isFinite(item.createdAt))) throw new TypeError("items must contain sessionId/createdAt objects");
102
+ if (value.repairedSessionIds !== void 0 && (!Array.isArray(value.repairedSessionIds) || value.repairedSessionIds.some((id) => typeof id !== "string"))) throw new TypeError("repairedSessionIds must be a string array");
103
+ return value;
104
+ }
130
105
  };
131
- /**
132
- * Host 严格描述符。网关优先读 typert.local,避免 SRC 扫描缓存
133
- * 或协议包双份导致 /api/workspaceRegistry/deleteSession 在生产环境 404。
134
- */
135
106
  const ARCHIVE_MANAGER_INVOCATIONS = [
136
- {
137
- id: "@michengai/dsh-archive-manager#workspaceRegistry/unarchiveSession",
138
- service: "workspaceRegistry",
139
- namespace: "workspaceRegistry",
140
- method: "unarchiveSession",
141
- invocation: { kind: "direct" },
142
- parameters: [{
143
- name: "sessionId",
144
- wire: "sessionId",
145
- source: "json",
146
- codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-session/types#SessionId", schema: sessionIdSchema }
147
- }],
148
- result: {
149
- mode: "strict",
150
- typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionIds",
151
- schema: archivedSetSchema
152
- },
153
- sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
154
- },
155
- {
156
- id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteSession",
157
- service: "workspaceRegistry",
158
- namespace: "workspaceRegistry",
159
- method: "deleteSession",
160
- invocation: { kind: "direct" },
161
- parameters: [{
162
- name: "sessionId",
163
- wire: "sessionId",
164
- source: "json",
165
- codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-session/types#SessionId", schema: sessionIdSchema }
166
- }],
167
- result: {
168
- mode: "strict",
169
- typeSymbol: "@michengai/dsh-archive-manager/types#Deleted",
170
- schema: deletedSchema
171
- },
172
- sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
173
- },
174
- {
175
- id: "@michengai/dsh-archive-manager#workspaceRegistry/unarchiveSessions",
176
- service: "workspaceRegistry",
177
- namespace: "workspaceRegistry",
178
- method: "unarchiveSessions",
179
- invocation: { kind: "direct" },
180
- parameters: [{
181
- name: "target",
182
- wire: "target",
183
- source: "json",
184
- codec: { mode: "strict", typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedBatchTarget", schema: archivedBatchTargetSchema }
185
- }],
186
- result: {
187
- mode: "strict",
188
- typeSymbol: "@michengai/dsh-archive-manager/types#UnarchivedBatch",
189
- schema: unarchivedBatchSchema
190
- },
191
- sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
192
- },
193
- {
194
- id: "@michengai/dsh-archive-manager#workspaceRegistry/archiveWorkspaceSessions",
195
- service: "workspaceRegistry",
196
- namespace: "workspaceRegistry",
197
- method: "archiveWorkspaceSessions",
198
- invocation: { kind: "direct" },
199
- parameters: [{
200
- name: "workspaceId",
201
- wire: "workspaceId",
202
- source: "json",
203
- codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-workspace/types#WorkspaceId", schema: workspaceIdSchema }
204
- }],
205
- result: {
206
- mode: "strict",
207
- typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedWorkspaceBatch",
208
- schema: archivedWorkspaceBatchSchema
209
- },
210
- sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
211
- },
212
- {
213
- id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteArchivedSessions",
214
- service: "workspaceRegistry",
215
- namespace: "workspaceRegistry",
216
- method: "deleteArchivedSessions",
217
- invocation: { kind: "direct" },
218
- parameters: [{
219
- name: "target",
220
- wire: "target",
221
- source: "json",
222
- codec: { mode: "strict", typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedBatchTarget", schema: archivedBatchTargetSchema }
223
- }],
224
- result: {
225
- mode: "strict",
226
- typeSymbol: "@michengai/dsh-archive-manager/types#DeletedBatch",
227
- schema: deletedBatchSchema
228
- },
229
- sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
230
- },
231
- {
232
- id: "@michengai/dsh-archive-manager#workspaceRegistry/archivedSessionMetadata",
233
- service: "workspaceRegistry",
234
- namespace: "workspaceRegistry",
235
- method: "archivedSessionMetadata",
236
- invocation: { kind: "direct" },
237
- parameters: [],
238
- result: {
239
- mode: "strict",
240
- typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionMetadata",
241
- schema: archivedSessionMetadataSchema
242
- },
243
- sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
244
- }
107
+ {
108
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/unarchiveSession",
109
+ service: "workspaceRegistry",
110
+ namespace: "workspaceRegistry",
111
+ method: "unarchiveSession",
112
+ invocation: { kind: "direct" },
113
+ parameters: [{
114
+ name: "sessionId",
115
+ wire: "sessionId",
116
+ source: "json",
117
+ codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-session/types#SessionId", schema: sessionIdSchema }
118
+ }],
119
+ result: {
120
+ mode: "strict",
121
+ typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionIds",
122
+ schema: archivedSetSchema
123
+ },
124
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
125
+ },
126
+ {
127
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteSession",
128
+ service: "workspaceRegistry",
129
+ namespace: "workspaceRegistry",
130
+ method: "deleteSession",
131
+ invocation: { kind: "direct" },
132
+ parameters: [{
133
+ name: "sessionId",
134
+ wire: "sessionId",
135
+ source: "json",
136
+ codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-session/types#SessionId", schema: sessionIdSchema }
137
+ }],
138
+ result: {
139
+ mode: "strict",
140
+ typeSymbol: "@michengai/dsh-archive-manager/types#Deleted",
141
+ schema: deletedSchema
142
+ },
143
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
144
+ },
145
+ {
146
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/unarchiveSessions",
147
+ service: "workspaceRegistry",
148
+ namespace: "workspaceRegistry",
149
+ method: "unarchiveSessions",
150
+ invocation: { kind: "direct" },
151
+ parameters: [{
152
+ name: "target",
153
+ wire: "target",
154
+ source: "json",
155
+ codec: { mode: "strict", typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedBatchTarget", schema: archivedBatchTargetSchema }
156
+ }],
157
+ result: {
158
+ mode: "strict",
159
+ typeSymbol: "@michengai/dsh-archive-manager/types#UnarchivedBatch",
160
+ schema: unarchivedBatchSchema
161
+ },
162
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
163
+ },
164
+ {
165
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/archiveWorkspaceSessions",
166
+ service: "workspaceRegistry",
167
+ namespace: "workspaceRegistry",
168
+ method: "archiveWorkspaceSessions",
169
+ invocation: { kind: "direct" },
170
+ parameters: [{
171
+ name: "workspaceId",
172
+ wire: "workspaceId",
173
+ source: "json",
174
+ codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-workspace/types#WorkspaceId", schema: workspaceIdSchema }
175
+ }],
176
+ result: {
177
+ mode: "strict",
178
+ typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedWorkspaceBatch",
179
+ schema: archivedWorkspaceBatchSchema
180
+ },
181
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
182
+ },
183
+ {
184
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteArchivedSessions",
185
+ service: "workspaceRegistry",
186
+ namespace: "workspaceRegistry",
187
+ method: "deleteArchivedSessions",
188
+ invocation: { kind: "direct" },
189
+ parameters: [{
190
+ name: "target",
191
+ wire: "target",
192
+ source: "json",
193
+ codec: { mode: "strict", typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedBatchTarget", schema: archivedBatchTargetSchema }
194
+ }],
195
+ result: {
196
+ mode: "strict",
197
+ typeSymbol: "@michengai/dsh-archive-manager/types#DeletedBatch",
198
+ schema: deletedBatchSchema
199
+ },
200
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
201
+ },
202
+ {
203
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/archivedSessionMetadata",
204
+ service: "workspaceRegistry",
205
+ namespace: "workspaceRegistry",
206
+ method: "archivedSessionMetadata",
207
+ invocation: { kind: "direct" },
208
+ parameters: [],
209
+ result: {
210
+ mode: "strict",
211
+ typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionMetadata",
212
+ schema: archivedSessionMetadataSchema
213
+ },
214
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
215
+ }
245
216
  ];
246
217
  const ARCHIVE_MANAGER_TYPERT = {
247
- package: "@michengai/dsh-archive-manager",
248
- face: "host",
249
- schemas: [],
250
- model: { services: [], events: [], objects: [] },
251
- invocations: ARCHIVE_MANAGER_INVOCATIONS
218
+ package: "@michengai/dsh-archive-manager",
219
+ face: "host",
220
+ schemas: [],
221
+ model: { services: [], events: [], objects: [] },
222
+ invocations: ARCHIVE_MANAGER_INVOCATIONS
252
223
  };
253
224
  function registerHostRemote(ctx) {
254
- const existing = ctx.get("typert");
255
- if (existing !== undefined) {
256
- existing.register(ARCHIVE_MANAGER_TYPERT);
257
- return;
258
- }
259
- ctx.inject(["typert"], (typertCtx) => {
260
- typertCtx.typert.register(ARCHIVE_MANAGER_TYPERT);
261
- });
225
+ const existing = ctx.get("typert");
226
+ if (existing !== void 0) {
227
+ existing.register(ARCHIVE_MANAGER_TYPERT);
228
+ return;
229
+ }
230
+ ctx.inject(["typert"], (typertCtx) => {
231
+ typertCtx.typert.register(ARCHIVE_MANAGER_TYPERT);
232
+ });
262
233
  }
263
234
  var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
264
- static inject = [
265
- "storageDomain",
266
- "sessionPersistence",
267
- "sessionProjectionCache",
268
- "typert"
269
- ];
270
- /** 本进程内已物理删除的会话;阻止父类把 stale list() 重新编入索引。 */
271
- deletedSessionIds = /* @__PURE__ */ new Set();
272
- /** 墓碑插入顺序,用于在上限处淘汰最旧项。 */
273
- deletedSessionOrder = [];
274
- /** 墓碑上限:足够挡住 stale list(),又避免长驻进程无限增长。 */
275
- deletedSessionTombstoneLimit = 4096;
276
- /** 被删生命周期的日志身份(createdAt/cwd):冷复用探针区分“同 id 新会话”与 stale list() 的依据。 */
277
- deletedIdentities = /* @__PURE__ */ new Map();
278
- constructor(ctx) {
279
- super(ctx);
280
- this.typertRemote = bindTypertRemote(this, this.name);
281
- markRemoteMethod(this, "unarchiveSession");
282
- markRemoteMethod(this, "deleteSession");
283
- markRemoteMethod(this, "unarchiveSessions");
284
- markRemoteMethod(this, "archiveWorkspaceSessions");
285
- markRemoteMethod(this, "deleteArchivedSessions");
286
- markRemoteMethod(this, "archivedSessionMetadata");
287
- registerHostRemote(this.ctx);
288
- }
289
- /**
290
- * 归档设置页创建时间排序所需的最小元数据。老用户可能仍有会话原文和
291
- * 归档标记、却没有投影缓存;这里按需从完整日志重建一次,再通知客户端
292
- * 刷新会话列表。已有缓存不读原文,新老 DSH 的缓存布局都走同一 put。
293
- */
294
- async archivedSessionMetadata() {
295
- const items = [];
296
- const repairedSessionIds = [];
297
- for (const sessionId of [...new Set(this.requireState().archivedSessionIds)]) {
298
- try {
299
- const header = await this.readSessionHeader(sessionId);
300
- if (await this.repairArchivedProjection(header)) repairedSessionIds.push(sessionId);
301
- if (typeof header.createdAt === "number" && Number.isFinite(header.createdAt)) items.push({ sessionId, createdAt: header.createdAt });
302
- } catch (error) {
303
- this.ctx.logger.warn(`archive-manager: could not read creation time for archived session "${sessionId}": ${String(error)}`);
304
- }
305
- }
306
- return { items, ...repairedSessionIds.length === 0 ? {} : { repairedSessionIds } };
307
- }
308
- /** 从会话原文补齐缺失的派生缓存;任何失败都只降级为原有无摘要列表。 */
309
- async repairArchivedProjection(header) {
310
- const cache = this.ctx.get("sessionProjectionCache");
311
- const persistence = this.ctx.get("sessionPersistence");
312
- const projections = this.ctx.get("sessionProjections");
313
- if (cache === void 0 || typeof cache.cachedSnapshot !== "function" || typeof cache.put !== "function") return false;
314
- if (persistence === void 0 || typeof persistence.readFrom !== "function" || projections === void 0 || typeof projections.restore !== "function") return false;
315
- try {
316
- // 未播种会话的继承事件数恒为零,先查缓存可避免读取完整会话原文。
317
- if (!header.isSeeded && cache.cachedSnapshot(header, 0) !== void 0) return false;
318
- const stored = await persistence.readFrom(header.id, 0);
319
- const meta = stored.meta ?? header;
320
- if (meta.isSeeded === true && stored.inheritedEventCount === void 0) {
321
- this.ctx.logger.warn(`archive-manager: projection repair for seeded archived session "${header.id}" skipped because its inherited event count is unavailable`);
322
- return false;
323
- }
324
- const inheritedEventCount = stored.inheritedEventCount ?? 0;
325
- if (meta.isSeeded !== true && inheritedEventCount !== 0) {
326
- this.ctx.logger.warn(`archive-manager: projection repair for unseeded archived session "${header.id}" skipped because its inherited event count is not zero`);
327
- return false;
328
- }
329
- if (cache.cachedSnapshot(meta, inheritedEventCount) !== void 0) return false;
330
- const restored = projections.restore({}, stored.events, 0, meta, inheritedEventCount);
331
- if (restored === void 0 || typeof restored !== "object" || restored.checkpoint === void 0) return false;
332
- await cache.put(header.id, {
333
- createdAt: meta.createdAt,
334
- ...(meta.cwd === void 0 ? {} : { cwd: meta.cwd }),
335
- isSeeded: meta.isSeeded ?? false,
336
- inheritedEventCount
337
- }, restored.checkpoint);
338
- return true;
339
- } catch (error) {
340
- this.ctx.logger.warn(`archive-manager: projection repair for archived session "${header.id}" failed: ${String(error)}`);
341
- return false;
342
- }
343
- }
344
- /**
345
- * 把一个会话移出注册表全局归档集合,恢复其正常可见性(其记账位从未
346
- * 移动,会话在原工作区位置重新出现)。幂等:未归档的已知会话直接返回
347
- * 当前集合不写入;未知会话与 `archiveSession` 一样抛错。
348
- * @param sessionId - 要取消归档的会话。
349
- * @returns 更新后的完整归档集合。
350
- */
351
- async unarchiveSession(sessionId) {
352
- return this.enqueueOperation(async () => {
353
- if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
354
- const state = this.requireState();
355
- if (!state.archivedSessionIds.includes(sessionId)) return { archivedSessionIds: [...state.archivedSessionIds] };
356
- const next = {
357
- ...state,
358
- archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
359
- };
360
- await this.setState(next);
361
- return { archivedSessionIds: [...next.archivedSessionIds] };
362
- });
363
- }
364
- /**
365
- * 将一个工作区内所有会话加入归档集合。先确认所有待归档会话仍存在,
366
- * 再执行单次状态写入,因此未知会话不会导致项目只归档一部分。
367
- */
368
- async archiveWorkspaceSessions(workspaceId) {
369
- return this.enqueueOperation(async () => {
370
- workspaceId = workspaceIdSchema.parse(workspaceId);
371
- const workspace = this.requireTable().get(workspaceId);
372
- if (workspace === void 0) throw new Error(`unknown workspace "${workspaceId}"`);
373
- const state = this.requireState();
374
- const archived = new Set(state.archivedSessionIds);
375
- const archivedSessionIdsAdded = [...new Set(workspace.sessionIds)].filter((sessionId) => !archived.has(sessionId));
376
- for (const sessionId of archivedSessionIdsAdded) {
377
- if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
378
- }
379
- if (archivedSessionIdsAdded.length === 0) return {
380
- archivedSessionIds: [...state.archivedSessionIds],
381
- archivedSessionIdsAdded
382
- };
383
- const next = {
384
- ...state,
385
- archivedSessionIds: [...state.archivedSessionIds, ...archivedSessionIdsAdded]
386
- };
387
- await this.setState(next);
388
- return {
389
- archivedSessionIds: [...next.archivedSessionIds],
390
- archivedSessionIdsAdded
391
- };
392
- });
393
- }
394
- /**
395
- * 按宿主权威归档集合一次恢复全部、一个工作区或未分组的归档会话。
396
- * 目标全部来自已归档集合,因此即使日志已被外部移除,也会清掉陈旧归档标记。
397
- */
398
- async unarchiveSessions(target) {
399
- return this.enqueueOperation(async () => {
400
- const unarchivedSessionIds = this.archivedSessionIdsForTarget(target);
401
- if (unarchivedSessionIds.length === 0) return {
402
- archivedSessionIds: [...this.requireState().archivedSessionIds],
403
- unarchivedSessionIds: []
404
- };
405
- const restored = new Set(unarchivedSessionIds);
406
- const state = this.requireState();
407
- const next = {
408
- ...state,
409
- archivedSessionIds: state.archivedSessionIds.filter((id) => !restored.has(id))
410
- };
411
- await this.setState(next);
412
- return {
413
- archivedSessionIds: [...next.archivedSessionIds],
414
- unarchivedSessionIds
415
- };
416
- });
417
- }
418
- /**
419
- * 按作用域永久删除归档会话。跨会话文件删除无法组成事务,因此继续处理
420
- * 后续目标并把成功、并发消失和失败分别返回给客户端。
421
- */
422
- async deleteArchivedSessions(target) {
423
- return this.enqueueOperation(async () => {
424
- const requestedSessionIds = this.archivedSessionIdsForTarget(target);
425
- const deletedSessionIds = [];
426
- const skippedSessionIds = [];
427
- const failures = [];
428
- for (const sessionId of requestedSessionIds) {
429
- try {
430
- await this.deleteSessionCore(sessionId);
431
- deletedSessionIds.push(sessionId);
432
- } catch (error) {
433
- if (error instanceof ArchiveUnknownSessionError) {
434
- // 转录已消失时仍需清完可达的持久痕迹;只有全部完成才算
435
- // skipped,否则保留归档标记并作为 failure 暴露,允许重试。
436
- try {
437
- await this.cleanupUnknownArchivedSession(sessionId);
438
- skippedSessionIds.push(sessionId);
439
- } catch (cleanupError) {
440
- failures.push({ sessionId, message: String(cleanupError) });
441
- }
442
- continue;
443
- }
444
- failures.push({ sessionId, message: String(error) });
445
- }
446
- }
447
- return { requestedSessionIds, deletedSessionIds, skippedSessionIds, failures };
448
- });
449
- }
450
- /**
451
- * 清理已无转录的陈旧归档项。缓存墓碑只在清除在途写入期间短暂持有:
452
- * workspace 没有可记录的旧 header 身份,永久保留它会挡住未来的冷复用。
453
- * 归档标记最后清除,前序可失败步骤出错时批量入口仍能再次命中。
454
- */
455
- async cleanupUnknownArchivedSession(sessionId) {
456
- const projCache = this.ctx.get("sessionProjectionCache");
457
- await projCache?.whenIdle?.();
458
- if (projCache !== void 0) {
459
- await projCache.delete(sessionId);
460
- // 等待删除期间可能已进入的写回观察到墓碑并完成补删,再允许
461
- // 同 id 的未来新生命周期写入缓存。
462
- await projCache.whenIdle?.();
463
- projCache.clearTombstone?.(sessionId);
464
- }
465
- await this.cleanSpill(sessionId);
466
- await this.removeFromWorkspaceAccounts(sessionId);
467
- const state = this.requireState();
468
- if (state.archivedSessionIds.includes(sessionId)) {
469
- await this.setState({
470
- ...state,
471
- archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
472
- });
473
- }
474
- }
475
- /** 以归档集合顺序解析批量目标,避免依赖浏览器尚未加载完整的摘要投影。 */
476
- archivedSessionIdsForTarget(target) {
477
- target = archivedBatchTargetSchema.parse(target);
478
- const state = this.requireState();
479
- const archivedSessionIds = [...new Set(state.archivedSessionIds)];
480
- if (target.scope === "all") return archivedSessionIds;
481
- if (target.scope === "sessions") {
482
- const selected = new Set(target.sessionIds);
483
- return archivedSessionIds.filter((id) => selected.has(id));
484
- }
485
- if (target.scope === "workspace") {
486
- const workspace = this.requireTable().get(target.workspaceId);
487
- if (workspace === void 0) throw new Error(`unknown workspace "${target.workspaceId}"`);
488
- const accounted = new Set(workspace.sessionIds);
489
- return archivedSessionIds.filter((id) => accounted.has(id));
490
- }
491
- const accounted = /* @__PURE__ */ new Set();
492
- const table = this.requireTable();
493
- for (const workspaceId of state.workspaceIds) {
494
- for (const sessionId of table.get(workspaceId)?.sessionIds ?? []) accounted.add(sessionId);
495
- }
496
- return archivedSessionIds.filter((id) => !accounted.has(id));
497
- }
498
- /**
499
- * 永久删除一个会话及其全部痕迹(转录目录、工作区记账、归档标记、
500
- * 投影缓存行)。
501
- * @param sessionId - 要删除的会话。
502
- * @returns 持久化完成后的 `{ deleted: true }`。
503
- * @throws {@link ArchiveUnknownSessionError} 会话未知时抛出。
504
- */
505
- async deleteSession(sessionId) {
506
- return this.enqueueOperation(() => this.deleteSessionCore(sessionId));
507
- }
508
- /** 串行化后的删除主体(级联路径复用:它已持有操作链,绝不能再入队)。 */
509
- async deleteSessionCore(sessionId) {
510
- if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
511
- const sessions = this.ctx.get("sessions");
512
- const live = sessions?.get(sessionId);
513
- // 先记录被删生命周期的日志身份:目录删除后头部不可再读,
514
- // 冷复用探针(sessionKnown 墓碑分支)靠它区分同 id 的新生命周期。
515
- const deletedHeader = this.headers.get(sessionId) ?? live?.header;
516
- if (live !== void 0) {
517
- // 持久化屏障先行:不能有未落盘的转录写入与目录删除竞争
518
- //(持久化后端按批关闭句柄,flush 过的会话不再持有打开的文件)。
519
- await sessions.flush(live);
520
- // 从存储分离;`session/disposed` 同步触发,驱动浏览器端的
521
- // `host/session-removed` 帧并启动投影缓存的最终写后落盘。
522
- const entry = sessions.liveEntryFor(live);
523
- sessions.detachEntered(entry);
524
- } else if (sessions !== void 0) await this.publishColdSessionRemoval(sessionId, sessions);
525
- const projCache = this.ctx.get("sessionProjectionCache");
526
- // dispose 的写后落盘必须先于缓存行删除完成,
527
- // 否则该行会在删除之后被写回(复活)。
528
- await projCache?.whenIdle?.();
529
- // 先完成父会话的可失败记账清理;SUBAGENT 级联跨目录且非事务,可能
530
- // 部分完成,但父转录失败时仍保留索引供重试。
531
- const state = this.requireState();
532
- if (state.archivedSessionIds.includes(sessionId)) {
533
- await this.setState({
534
- ...state,
535
- archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
536
- });
537
- }
538
- await this.removeFromWorkspaceAccounts(sessionId);
539
- if (projCache !== void 0) await projCache.delete(sessionId);
540
- await this.deleteDescendants(sessionId);
541
- await this.cleanSpill(sessionId);
542
- await this.removeTranscriptDirectory(sessionId);
543
- // 物理删除已成功:此时再清父类索引。失败时保留索引,便于重试。
544
- this.forgetIndexedSession(sessionId);
545
- if (deletedHeader !== void 0) this.deletedIdentities.set(sessionId, headerIdentity(deletedHeader));
546
- return { deleted: true };
547
- }
548
- /**
549
- * 从父类内存索引中遗忘已删除会话,并阻止后续 indexHeaders 把它加回。
550
- * 实时会话以同 id 重新出现时(自定义 id 复用)会撤掉墓碑。
551
- */
552
- clearTombstone(sessionId) {
553
- this.deletedSessionIds.delete(sessionId);
554
- this.deletedIdentities.delete(sessionId);
555
- const idx = this.deletedSessionOrder.indexOf(sessionId);
556
- if (idx !== -1) this.deletedSessionOrder.splice(idx, 1);
557
- // workspace 与 projection-cache 共同描述同一删除生命周期;新生命周期
558
- // 被接纳时必须同步撤销两处墓碑,否则缓存写入仍会永久被拦截。
559
- this.ctx.get("sessionProjectionCache")?.clearTombstone?.(sessionId);
560
- }
561
- forgetIndexedSession(sessionId) {
562
- for (const evicted of trackTombstone(this.deletedSessionIds, this.deletedSessionOrder, sessionId, this.deletedSessionTombstoneLimit)) this.deletedIdentities.delete(evicted);
563
- this.headers.delete(sessionId);
564
- this.sessionPaths.delete(sessionId);
565
- this.invalidSessionPaths.delete(sessionId);
566
- }
567
- /**
568
- * 已删除会话对归档/删除入口都视为未知。实时复用同一 id 时撤墓碑,
569
- * 避免挡住新会话。
570
- */
571
- async sessionKnown(id) {
572
- if (this.ctx.get("sessions")?.get(id) !== void 0) {
573
- this.clearTombstone(id);
574
- return true;
575
- }
576
- if (this.deletedSessionIds.has(id)) return this.coldReuseKnown(id);
577
- return super.sessionKnown(id);
578
- }
579
- /**
580
- * 墓碑分支的冷复用探针:其他进程以同 id 重建并落盘的新会话(日志身份
581
- * 不同)撤墓碑放行并重新编入索引;stale list() 里同生命周期的旧头部
582
- * 仍视为未知。身份不可考(删除时未取到头部)时保守维持未知。
583
- */
584
- async coldReuseKnown(id) {
585
- const deletedIdentity = this.deletedIdentities.get(id);
586
- if (deletedIdentity === void 0) return false;
587
- const persistence = this.ctx.get("sessionPersistence");
588
- if (persistence === void 0 || typeof persistence.list !== "function") return false;
589
- let header;
590
- try {
591
- header = (await persistence.list()).find((item) => item.id === id);
592
- } catch (error) {
593
- this.ctx.logger.warn(`archive-manager: cold-reuse probe for "${id}" failed: ${String(error)}`);
594
- return false;
595
- }
596
- if (header === void 0) return false;
597
- const listed = headerIdentity(header);
598
- if (listed.createdAt === deletedIdentity.createdAt && listed.cwd === deletedIdentity.cwd) return false;
599
- this.clearTombstone(id);
600
- await this.indexHeader(header);
601
- return true;
602
- }
603
- /**
604
- * 父类 indexHeaders 只增不减;跳过墓碑 id,避免 stale persistence.list()
605
- * 把已删除会话重新编入 headers。
606
- */
607
- async indexHeader(header) {
608
- if (this.deletedSessionIds.has(header.id)) return;
609
- return super.indexHeader(header);
610
- }
611
- /** 为未处于实时状态的持久化会话发布相同的移除事件。 */
612
- async publishColdSessionRemoval(sessionId, sessions) {
613
- const persistence = this.ctx.get("sessionPersistence");
614
- if (persistence === void 0 || typeof persistence.prepare !== "function") return;
615
- try {
616
- const preparation = await persistence.prepare(sessionId);
617
- const detach = sessions.enter(preparation.session);
618
- try {
619
- sessions.announce(preparation.session);
620
- } finally {
621
- detach();
622
- preparation[Symbol.dispose]();
623
- }
624
- } catch (error) {
625
- this.ctx.logger.warn(`archive-manager: could not publish removal for stored session "${sessionId}": ${String(error)}`);
626
- }
627
- }
628
- /** 删除会话的转录目录;仅在所有记账清理完成后调用。 */
629
- async removeTranscriptDirectory(sessionId) {
630
- const persistence = this.ctx.get("sessionPersistence");
631
- if (persistence === void 0 || typeof persistence.locate !== "function") {
632
- throw new Error(`cannot delete session "${sessionId}": the session persistence backend does not expose locate() to resolve its transcript directory`);
633
- }
634
- const header = await this.readSessionHeader(sessionId);
635
- const location = persistence.locate(header);
636
- if (location === void 0 || typeof location.path !== "string") {
637
- throw new Error(`cannot delete session "${sessionId}": the session persistence backend could not resolve its transcript directory`);
638
- }
639
- const transcriptDir = dirname(location.path);
640
- try {
641
- await rm(transcriptDir, { recursive: true, force: true });
642
- } catch (error) {
643
- const message = `cannot delete session "${sessionId}": transcript directory "${transcriptDir}" remains after bookkeeping cleanup`;
644
- const detail = `${message}: ${String(error)}`;
645
- this.ctx.logger.warn(`archive-manager: ${detail}`);
646
- throw new Error(detail, { cause: error });
647
- }
648
- }
649
- /** 把 id 从每个工作区记录中移除,并刷新实体快照。 */
650
- async removeFromWorkspaceAccounts(sessionId) {
651
- const table = this.requireTable();
652
- const state = this.requireState();
653
- for (const workspaceId of state.workspaceIds) {
654
- const record = table.get(workspaceId);
655
- if (record === void 0 || !record.sessionIds.includes(sessionId)) continue;
656
- const next = await table.update(workspaceId, (current) => ({
657
- ...current,
658
- sessionIds: current.sessionIds.filter((id) => id !== sessionId),
659
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
660
- }));
661
- const entity = this.entities.get(workspaceId);
662
- if (entity !== void 0) entity.record = next;
663
- }
664
- }
665
- /** 尽力而为的级联删除:删除 `sessionId` 的 SUBAGENT 子会话。
666
- * 仅头部标记 `origin: "subagent"` 的会话参与:单凭 `parentSession` 有歧义
667
- *(fork 分支也携带它),而 fork 分支是独立的用户会话,绝不能被级联删除。 */
668
- async deleteDescendants(sessionId) {
669
- try {
670
- const descendants = [];
671
- const sessions = this.ctx.get("sessions");
672
- if (sessions !== void 0) for (const session of sessions.list()) {
673
- if (session.header.parentSession === sessionId && session.header.origin === "subagent") descendants.push(session.id);
674
- }
675
- for (const header of await this.ctx.sessionPersistence.list()) {
676
- if (header.parentSession === sessionId && header.origin === "subagent" && !descendants.includes(header.id)) descendants.push(header.id);
677
- }
678
- for (const childId of descendants) {
679
- try {
680
- if (!await this.sessionKnown(childId)) continue;
681
- await this.deleteSessionCore(childId);
682
- } catch (error) {
683
- if (error instanceof ArchiveUnknownSessionError) continue;
684
- this.ctx.logger.warn(`archive-manager: cascade delete of subagent session "${childId}" (child of "${sessionId}") failed: ${String(error)}`);
685
- }
686
- }
687
- } catch (error) {
688
- this.ctx.logger.warn(`archive-manager: descendant enumeration for deleted session "${sessionId}" failed: ${String(error)}`);
689
- }
690
- }
691
- /** 尽力而为的 spill 清理:移除该会话作用域的 spill 目录。 */
692
- async cleanSpill(sessionId) {
693
- try {
694
- const spill = this.ctx.get("spillStore");
695
- if (spill === void 0 || typeof spill.root !== "string") return;
696
- await rm(sessionDir(spill.root, sessionId), { recursive: true, force: true });
697
- } catch (error) {
698
- this.ctx.logger.warn(`archive-manager: spill cleanup for deleted session "${sessionId}" failed: ${String(error)}`);
699
- }
700
- }
235
+ static inject = [
236
+ "storageDomain",
237
+ "sessionPersistence",
238
+ "sessionProjectionCache",
239
+ "typert"
240
+ ];
241
+ /** 本进程内已物理删除的会话;阻止父类把 stale list() 重新编入索引。 */
242
+ deletedSessionIds = /* @__PURE__ */ new Set();
243
+ /** 墓碑插入顺序,用于在上限处淘汰最旧项。 */
244
+ deletedSessionOrder = [];
245
+ /** 墓碑上限:足够挡住 stale list(),又避免长驻进程无限增长。 */
246
+ deletedSessionTombstoneLimit = 4096;
247
+ /** 被删生命周期的日志身份(createdAt/cwd):冷复用探针区分“同 id 新会话”与 stale list() 的依据。 */
248
+ deletedIdentities = /* @__PURE__ */ new Map();
249
+ constructor(ctx) {
250
+ super(ctx);
251
+ this.typertRemote = bindTypertRemote(this, this.name);
252
+ markRemoteMethod(this, "unarchiveSession");
253
+ markRemoteMethod(this, "deleteSession");
254
+ markRemoteMethod(this, "unarchiveSessions");
255
+ markRemoteMethod(this, "archiveWorkspaceSessions");
256
+ markRemoteMethod(this, "deleteArchivedSessions");
257
+ markRemoteMethod(this, "archivedSessionMetadata");
258
+ registerHostRemote(this.ctx);
259
+ }
260
+ /**
261
+ * 归档设置页创建时间排序所需的最小元数据。老用户可能仍有会话原文和
262
+ * 归档标记、却没有投影缓存;这里按需从完整日志重建一次,再通知客户端
263
+ * 刷新会话列表。已有缓存不读原文,新老 DSH 的缓存布局都走同一 put。
264
+ */
265
+ async archivedSessionMetadata() {
266
+ const items = [];
267
+ const repairedSessionIds = [];
268
+ for (const sessionId of [...new Set(this.requireState().archivedSessionIds)]) {
269
+ try {
270
+ const header = await this.readSessionHeader(sessionId);
271
+ if (await this.repairArchivedProjection(header)) repairedSessionIds.push(sessionId);
272
+ if (typeof header.createdAt === "number" && Number.isFinite(header.createdAt)) items.push({ sessionId, createdAt: header.createdAt });
273
+ } catch (error) {
274
+ this.ctx.logger.warn(`archive-manager: could not read creation time for archived session "${sessionId}": ${String(error)}`);
275
+ }
276
+ }
277
+ return { items, ...repairedSessionIds.length === 0 ? {} : { repairedSessionIds } };
278
+ }
279
+ /** 从会话原文补齐缺失的派生缓存;任何失败都只降级为原有无摘要列表。 */
280
+ async repairArchivedProjection(header) {
281
+ const cache = this.ctx.get("sessionProjectionCache");
282
+ const persistence = this.ctx.get("sessionPersistence");
283
+ const projections = this.ctx.get("sessionProjections");
284
+ if (cache === void 0 || typeof cache.cachedSnapshot !== "function" || typeof cache.put !== "function") return false;
285
+ if (persistence === void 0 || typeof persistence.readFrom !== "function" || projections === void 0 || typeof projections.restore !== "function") return false;
286
+ try {
287
+ if (!header.isSeeded && cache.cachedSnapshot(header, 0) !== void 0) return false;
288
+ const stored = await persistence.readFrom(header.id, 0);
289
+ const meta = stored.meta ?? header;
290
+ if (meta.isSeeded === true && stored.inheritedEventCount === void 0) {
291
+ this.ctx.logger.warn(`archive-manager: projection repair for seeded archived session "${header.id}" skipped because its inherited event count is unavailable`);
292
+ return false;
293
+ }
294
+ const inheritedEventCount = stored.inheritedEventCount ?? 0;
295
+ if (meta.isSeeded !== true && inheritedEventCount !== 0) {
296
+ this.ctx.logger.warn(`archive-manager: projection repair for unseeded archived session "${header.id}" skipped because its inherited event count is not zero`);
297
+ return false;
298
+ }
299
+ if (cache.cachedSnapshot(meta, inheritedEventCount) !== void 0) return false;
300
+ const restored = projections.restore({}, stored.events, 0, meta, inheritedEventCount);
301
+ if (restored === void 0 || typeof restored !== "object" || restored.checkpoint === void 0) return false;
302
+ await cache.put(header.id, {
303
+ createdAt: meta.createdAt,
304
+ ...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
305
+ isSeeded: meta.isSeeded ?? false,
306
+ inheritedEventCount
307
+ }, restored.checkpoint);
308
+ return true;
309
+ } catch (error) {
310
+ this.ctx.logger.warn(`archive-manager: projection repair for archived session "${header.id}" failed: ${String(error)}`);
311
+ return false;
312
+ }
313
+ }
314
+ /**
315
+ * 把一个会话移出注册表全局归档集合,恢复其正常可见性(其记账位从未
316
+ * 移动,会话在原工作区位置重新出现)。幂等:未归档的已知会话直接返回
317
+ * 当前集合不写入;未知会话与 `archiveSession` 一样抛错。
318
+ * @param sessionId - 要取消归档的会话。
319
+ * @returns 更新后的完整归档集合。
320
+ */
321
+ async unarchiveSession(sessionId) {
322
+ return this.enqueueOperation(async () => {
323
+ if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
324
+ const state = this.requireState();
325
+ if (!state.archivedSessionIds.includes(sessionId)) return { archivedSessionIds: [...state.archivedSessionIds] };
326
+ const next = {
327
+ ...state,
328
+ archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
329
+ };
330
+ await this.setState(next);
331
+ return { archivedSessionIds: [...next.archivedSessionIds] };
332
+ });
333
+ }
334
+ /**
335
+ * 将一个工作区内所有会话加入归档集合。先确认所有待归档会话仍存在,
336
+ * 再执行单次状态写入,因此未知会话不会导致项目只归档一部分。
337
+ */
338
+ async archiveWorkspaceSessions(workspaceId) {
339
+ return this.enqueueOperation(async () => {
340
+ workspaceId = workspaceIdSchema.parse(workspaceId);
341
+ const workspace = this.requireTable().get(workspaceId);
342
+ if (workspace === void 0) throw new Error(`unknown workspace "${workspaceId}"`);
343
+ const state = this.requireState();
344
+ const archived = new Set(state.archivedSessionIds);
345
+ const archivedSessionIdsAdded = [...new Set(workspace.sessionIds)].filter((sessionId) => !archived.has(sessionId));
346
+ for (const sessionId of archivedSessionIdsAdded) {
347
+ if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
348
+ }
349
+ if (archivedSessionIdsAdded.length === 0) return {
350
+ archivedSessionIds: [...state.archivedSessionIds],
351
+ archivedSessionIdsAdded
352
+ };
353
+ const next = {
354
+ ...state,
355
+ archivedSessionIds: [...state.archivedSessionIds, ...archivedSessionIdsAdded]
356
+ };
357
+ await this.setState(next);
358
+ return {
359
+ archivedSessionIds: [...next.archivedSessionIds],
360
+ archivedSessionIdsAdded
361
+ };
362
+ });
363
+ }
364
+ /**
365
+ * 按宿主权威归档集合一次恢复全部、一个工作区或未分组的归档会话。
366
+ * 目标全部来自已归档集合,因此即使日志已被外部移除,也会清掉陈旧归档标记。
367
+ */
368
+ async unarchiveSessions(target) {
369
+ return this.enqueueOperation(async () => {
370
+ const unarchivedSessionIds = this.archivedSessionIdsForTarget(target);
371
+ if (unarchivedSessionIds.length === 0) return {
372
+ archivedSessionIds: [...this.requireState().archivedSessionIds],
373
+ unarchivedSessionIds: []
374
+ };
375
+ const restored = new Set(unarchivedSessionIds);
376
+ const state = this.requireState();
377
+ const next = {
378
+ ...state,
379
+ archivedSessionIds: state.archivedSessionIds.filter((id) => !restored.has(id))
380
+ };
381
+ await this.setState(next);
382
+ return {
383
+ archivedSessionIds: [...next.archivedSessionIds],
384
+ unarchivedSessionIds
385
+ };
386
+ });
387
+ }
388
+ /**
389
+ * 按作用域永久删除归档会话。跨会话文件删除无法组成事务,因此继续处理
390
+ * 后续目标并把成功、并发消失和失败分别返回给客户端。
391
+ */
392
+ async deleteArchivedSessions(target) {
393
+ return this.enqueueOperation(async () => {
394
+ const requestedSessionIds = this.archivedSessionIdsForTarget(target);
395
+ const deletedSessionIds = [];
396
+ const skippedSessionIds = [];
397
+ const failures = [];
398
+ for (const sessionId of requestedSessionIds) {
399
+ try {
400
+ await this.deleteSessionCore(sessionId);
401
+ deletedSessionIds.push(sessionId);
402
+ } catch (error) {
403
+ if (error instanceof ArchiveUnknownSessionError) {
404
+ try {
405
+ await this.cleanupUnknownArchivedSession(sessionId);
406
+ skippedSessionIds.push(sessionId);
407
+ } catch (cleanupError) {
408
+ failures.push({ sessionId, message: String(cleanupError) });
409
+ }
410
+ continue;
411
+ }
412
+ failures.push({ sessionId, message: String(error) });
413
+ }
414
+ }
415
+ return { requestedSessionIds, deletedSessionIds, skippedSessionIds, failures };
416
+ });
417
+ }
418
+ /**
419
+ * 清理已无转录的陈旧归档项。缓存墓碑只在清除在途写入期间短暂持有:
420
+ * workspace 没有可记录的旧 header 身份,永久保留它会挡住未来的冷复用。
421
+ * 归档标记最后清除,前序可失败步骤出错时批量入口仍能再次命中。
422
+ */
423
+ async cleanupUnknownArchivedSession(sessionId) {
424
+ const projCache = this.ctx.get("sessionProjectionCache");
425
+ await projCache?.whenIdle?.();
426
+ if (projCache !== void 0) {
427
+ await projCache.delete(sessionId);
428
+ await projCache.whenIdle?.();
429
+ projCache.clearTombstone?.(sessionId);
430
+ }
431
+ await this.cleanSpill(sessionId);
432
+ await this.removeFromWorkspaceAccounts(sessionId);
433
+ const state = this.requireState();
434
+ if (state.archivedSessionIds.includes(sessionId)) {
435
+ await this.setState({
436
+ ...state,
437
+ archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
438
+ });
439
+ }
440
+ }
441
+ /** 以归档集合顺序解析批量目标,避免依赖浏览器尚未加载完整的摘要投影。 */
442
+ archivedSessionIdsForTarget(target) {
443
+ target = archivedBatchTargetSchema.parse(target);
444
+ const state = this.requireState();
445
+ const archivedSessionIds = [...new Set(state.archivedSessionIds)];
446
+ if (target.scope === "all") return archivedSessionIds;
447
+ if (target.scope === "sessions") {
448
+ const selected = new Set(target.sessionIds);
449
+ return archivedSessionIds.filter((id) => selected.has(id));
450
+ }
451
+ if (target.scope === "workspace") {
452
+ const workspace = this.requireTable().get(target.workspaceId);
453
+ if (workspace === void 0) throw new Error(`unknown workspace "${target.workspaceId}"`);
454
+ const accounted2 = new Set(workspace.sessionIds);
455
+ return archivedSessionIds.filter((id) => accounted2.has(id));
456
+ }
457
+ const accounted = /* @__PURE__ */ new Set();
458
+ const table = this.requireTable();
459
+ for (const workspaceId of state.workspaceIds) {
460
+ for (const sessionId of table.get(workspaceId)?.sessionIds ?? []) accounted.add(sessionId);
461
+ }
462
+ return archivedSessionIds.filter((id) => !accounted.has(id));
463
+ }
464
+ /**
465
+ * 永久删除一个会话及其全部痕迹(转录目录、工作区记账、归档标记、
466
+ * 投影缓存行)。
467
+ * @param sessionId - 要删除的会话。
468
+ * @returns 持久化完成后的 `{ deleted: true }`。
469
+ * @throws {@link ArchiveUnknownSessionError} 会话未知时抛出。
470
+ */
471
+ async deleteSession(sessionId) {
472
+ return this.enqueueOperation(() => this.deleteSessionCore(sessionId));
473
+ }
474
+ /** 串行化后的删除主体(级联路径复用:它已持有操作链,绝不能再入队)。 */
475
+ async deleteSessionCore(sessionId) {
476
+ if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
477
+ const sessions = this.ctx.get("sessions");
478
+ const live = sessions?.get(sessionId);
479
+ const deletedHeader = this.headers.get(sessionId) ?? live?.header;
480
+ if (live !== void 0) {
481
+ await sessions.flush(live);
482
+ const entry = sessions.liveEntryFor(live);
483
+ sessions.detachEntered(entry);
484
+ } else if (sessions !== void 0) await this.publishColdSessionRemoval(sessionId, sessions);
485
+ const projCache = this.ctx.get("sessionProjectionCache");
486
+ await projCache?.whenIdle?.();
487
+ const state = this.requireState();
488
+ if (state.archivedSessionIds.includes(sessionId)) {
489
+ await this.setState({
490
+ ...state,
491
+ archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
492
+ });
493
+ }
494
+ await this.removeFromWorkspaceAccounts(sessionId);
495
+ if (projCache !== void 0) await projCache.delete(sessionId);
496
+ await this.deleteDescendants(sessionId);
497
+ await this.cleanSpill(sessionId);
498
+ await this.removeTranscriptDirectory(sessionId);
499
+ this.forgetIndexedSession(sessionId);
500
+ if (deletedHeader !== void 0) this.deletedIdentities.set(sessionId, headerIdentity(deletedHeader));
501
+ return { deleted: true };
502
+ }
503
+ /**
504
+ * 从父类内存索引中遗忘已删除会话,并阻止后续 indexHeaders 把它加回。
505
+ * 实时会话以同 id 重新出现时(自定义 id 复用)会撤掉墓碑。
506
+ */
507
+ clearTombstone(sessionId) {
508
+ this.deletedSessionIds.delete(sessionId);
509
+ this.deletedIdentities.delete(sessionId);
510
+ const idx = this.deletedSessionOrder.indexOf(sessionId);
511
+ if (idx !== -1) this.deletedSessionOrder.splice(idx, 1);
512
+ this.ctx.get("sessionProjectionCache")?.clearTombstone?.(sessionId);
513
+ }
514
+ forgetIndexedSession(sessionId) {
515
+ for (const evicted of trackTombstone(this.deletedSessionIds, this.deletedSessionOrder, sessionId, this.deletedSessionTombstoneLimit)) this.deletedIdentities.delete(evicted);
516
+ this.headers.delete(sessionId);
517
+ this.sessionPaths.delete(sessionId);
518
+ this.invalidSessionPaths.delete(sessionId);
519
+ }
520
+ /**
521
+ * 已删除会话对归档/删除入口都视为未知。实时复用同一 id 时撤墓碑,
522
+ * 避免挡住新会话。
523
+ */
524
+ async sessionKnown(id) {
525
+ if (this.ctx.get("sessions")?.get(id) !== void 0) {
526
+ this.clearTombstone(id);
527
+ return true;
528
+ }
529
+ if (this.deletedSessionIds.has(id)) return this.coldReuseKnown(id);
530
+ return super.sessionKnown(id);
531
+ }
532
+ /**
533
+ * 墓碑分支的冷复用探针:其他进程以同 id 重建并落盘的新会话(日志身份
534
+ * 不同)撤墓碑放行并重新编入索引;stale list() 里同生命周期的旧头部
535
+ * 仍视为未知。身份不可考(删除时未取到头部)时保守维持未知。
536
+ */
537
+ async coldReuseKnown(id) {
538
+ const deletedIdentity = this.deletedIdentities.get(id);
539
+ if (deletedIdentity === void 0) return false;
540
+ const persistence = this.ctx.get("sessionPersistence");
541
+ if (persistence === void 0 || typeof persistence.list !== "function") return false;
542
+ let header;
543
+ try {
544
+ header = (await persistence.list()).find((item) => item.id === id);
545
+ } catch (error) {
546
+ this.ctx.logger.warn(`archive-manager: cold-reuse probe for "${id}" failed: ${String(error)}`);
547
+ return false;
548
+ }
549
+ if (header === void 0) return false;
550
+ const listed = headerIdentity(header);
551
+ if (listed.createdAt === deletedIdentity.createdAt && listed.cwd === deletedIdentity.cwd) return false;
552
+ this.clearTombstone(id);
553
+ await this.indexHeader(header);
554
+ return true;
555
+ }
556
+ /**
557
+ * 父类 indexHeaders 只增不减;跳过墓碑 id,避免 stale persistence.list()
558
+ * 把已删除会话重新编入 headers。
559
+ */
560
+ async indexHeader(header) {
561
+ if (this.deletedSessionIds.has(header.id)) return;
562
+ return super.indexHeader(header);
563
+ }
564
+ /** 为未处于实时状态的持久化会话发布相同的移除事件。 */
565
+ async publishColdSessionRemoval(sessionId, sessions) {
566
+ const persistence = this.ctx.get("sessionPersistence");
567
+ if (persistence === void 0 || typeof persistence.prepare !== "function") return;
568
+ try {
569
+ const preparation = await persistence.prepare(sessionId);
570
+ const detach = sessions.enter(preparation.session);
571
+ try {
572
+ sessions.announce(preparation.session);
573
+ } finally {
574
+ detach();
575
+ preparation[Symbol.dispose]();
576
+ }
577
+ } catch (error) {
578
+ this.ctx.logger.warn(`archive-manager: could not publish removal for stored session "${sessionId}": ${String(error)}`);
579
+ }
580
+ }
581
+ /** 删除会话的转录目录;仅在所有记账清理完成后调用。 */
582
+ async removeTranscriptDirectory(sessionId) {
583
+ const persistence = this.ctx.get("sessionPersistence");
584
+ if (persistence === void 0 || typeof persistence.locate !== "function") {
585
+ throw new Error(`cannot delete session "${sessionId}": the session persistence backend does not expose locate() to resolve its transcript directory`);
586
+ }
587
+ const header = await this.readSessionHeader(sessionId);
588
+ const location = persistence.locate(header);
589
+ if (location === void 0 || typeof location.path !== "string") {
590
+ throw new Error(`cannot delete session "${sessionId}": the session persistence backend could not resolve its transcript directory`);
591
+ }
592
+ const transcriptDir = dirname(location.path);
593
+ try {
594
+ await rm(transcriptDir, { recursive: true, force: true });
595
+ } catch (error) {
596
+ const message = `cannot delete session "${sessionId}": transcript directory "${transcriptDir}" remains after bookkeeping cleanup`;
597
+ const detail = `${message}: ${String(error)}`;
598
+ this.ctx.logger.warn(`archive-manager: ${detail}`);
599
+ throw new Error(detail, { cause: error });
600
+ }
601
+ }
602
+ /** 把 id 从每个工作区记录中移除,并刷新实体快照。 */
603
+ async removeFromWorkspaceAccounts(sessionId) {
604
+ const table = this.requireTable();
605
+ const state = this.requireState();
606
+ for (const workspaceId of state.workspaceIds) {
607
+ const record = table.get(workspaceId);
608
+ if (record === void 0 || !record.sessionIds.includes(sessionId)) continue;
609
+ const next = await table.update(workspaceId, (current) => ({
610
+ ...current,
611
+ sessionIds: current.sessionIds.filter((id) => id !== sessionId),
612
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
613
+ }));
614
+ const entity = this.entities.get(workspaceId);
615
+ if (entity !== void 0) entity.record = next;
616
+ }
617
+ }
618
+ /** 尽力而为的级联删除:删除 `sessionId` 的 SUBAGENT 子会话。
619
+ * 仅头部标记 `origin: "subagent"` 的会话参与:单凭 `parentSession` 有歧义
620
+ *(fork 分支也携带它),而 fork 分支是独立的用户会话,绝不能被级联删除。 */
621
+ async deleteDescendants(sessionId) {
622
+ try {
623
+ const descendants = [];
624
+ const sessions = this.ctx.get("sessions");
625
+ if (sessions !== void 0) for (const session of sessions.list()) {
626
+ if (session.header.parentSession === sessionId && session.header.origin === "subagent") descendants.push(session.id);
627
+ }
628
+ for (const header of await this.ctx.sessionPersistence.list()) {
629
+ if (header.parentSession === sessionId && header.origin === "subagent" && !descendants.includes(header.id)) descendants.push(header.id);
630
+ }
631
+ for (const childId of descendants) {
632
+ try {
633
+ if (!await this.sessionKnown(childId)) continue;
634
+ await this.deleteSessionCore(childId);
635
+ } catch (error) {
636
+ if (error instanceof ArchiveUnknownSessionError) continue;
637
+ this.ctx.logger.warn(`archive-manager: cascade delete of subagent session "${childId}" (child of "${sessionId}") failed: ${String(error)}`);
638
+ }
639
+ }
640
+ } catch (error) {
641
+ this.ctx.logger.warn(`archive-manager: descendant enumeration for deleted session "${sessionId}" failed: ${String(error)}`);
642
+ }
643
+ }
644
+ /** 尽力而为的 spill 清理:移除该会话作用域的 spill 目录。 */
645
+ async cleanSpill(sessionId) {
646
+ try {
647
+ const spill = this.ctx.get("spillStore");
648
+ if (spill === void 0 || typeof spill.root !== "string") return;
649
+ await rm(sessionDir(spill.root, sessionId), { recursive: true, force: true });
650
+ } catch (error) {
651
+ this.ctx.logger.warn(`archive-manager: spill cleanup for deleted session "${sessionId}" failed: ${String(error)}`);
652
+ }
653
+ }
654
+ };
655
+ export {
656
+ ArchiveWorkspaceRegistry,
657
+ ArchiveWorkspaceRegistry as default
701
658
  };
702
- //#endregion
703
- export { ArchiveWorkspaceRegistry, ArchiveWorkspaceRegistry as default };