@michengai/dsh-archive-manager 0.1.37 → 0.1.39
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/CHANGELOG.md +10 -0
- package/CHANGELOG.zh-CN.md +10 -0
- package/README.md +21 -8
- package/README.zh-CN.md +21 -8
- package/cordis.patch.yml +3 -3
- package/lib/client.js +78 -207
- package/lib/workspace.js +374 -178
- package/package.json +42 -40
package/lib/workspace.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { lstat, rm } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
WorkspaceRegistry
|
|
5
|
-
} from "@deepseek-ai/dsh-workspace";
|
|
3
|
+
import { WorkspaceRegistry } from "@deepseek-ai/dsh-workspace";
|
|
6
4
|
import { bindTypertRemote, Remote } from "@deepseek-ai/dsh-typert-protocol";
|
|
7
5
|
import { sessionDir } from "@deepseek-ai/dsh-spill-local";
|
|
8
6
|
import { trackTombstone } from "./tombstone.js";
|
|
9
7
|
function jsonlSessionDirectory(persistence, header, location) {
|
|
10
|
-
if (persistence.name !== "session-persistence-jsonl" || location.kind !== "jsonl")
|
|
8
|
+
if (persistence.name !== "session-persistence-jsonl" || location.kind !== "jsonl")
|
|
9
|
+
return;
|
|
11
10
|
const root = persistence.root ?? persistence.config?.root;
|
|
12
|
-
if (typeof root !== "string" || !isAbsolute(root) || !isAbsolute(location.path))
|
|
13
|
-
|
|
11
|
+
if (typeof root !== "string" || !isAbsolute(root) || !isAbsolute(location.path))
|
|
12
|
+
return;
|
|
13
|
+
if (!/^session(?:\.v[1-9][0-9]*)?\.jsonl(?:\.zstd)?$/.test(
|
|
14
|
+
basename(location.path)
|
|
15
|
+
))
|
|
16
|
+
return;
|
|
14
17
|
if (typeof header.id !== "string" || header.id.length === 0) return;
|
|
15
|
-
const encode = (text) => text.replace(
|
|
18
|
+
const encode = (text) => text.replace(
|
|
19
|
+
/[^A-Za-z0-9._-]/g,
|
|
20
|
+
(ch) => `~${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`
|
|
21
|
+
);
|
|
16
22
|
const segment = header.id === "." ? "~002E" : header.id === ".." ? "~002E~002E" : encode(header.id);
|
|
17
23
|
let project = "_no-cwd";
|
|
18
24
|
if (header.cwd !== void 0) {
|
|
@@ -54,70 +60,104 @@ function headerIdentity(header) {
|
|
|
54
60
|
}
|
|
55
61
|
const sessionIdSchema = {
|
|
56
62
|
parse(value) {
|
|
57
|
-
if (typeof value !== "string" || value.length === 0)
|
|
63
|
+
if (typeof value !== "string" || value.length === 0)
|
|
64
|
+
throw new TypeError(
|
|
65
|
+
`sessionId must be a non-empty string, got ${String(value)}`
|
|
66
|
+
);
|
|
58
67
|
return value;
|
|
59
68
|
}
|
|
60
69
|
};
|
|
61
70
|
const workspaceIdSchema = {
|
|
62
71
|
parse(value) {
|
|
63
|
-
if (typeof value !== "string" || value.length === 0)
|
|
72
|
+
if (typeof value !== "string" || value.length === 0)
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
`workspaceId must be a non-empty string, got ${String(value)}`
|
|
75
|
+
);
|
|
64
76
|
return value;
|
|
65
77
|
}
|
|
66
78
|
};
|
|
67
79
|
const archivedSetSchema = {
|
|
68
80
|
parse(value) {
|
|
69
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
81
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
82
|
+
throw new TypeError("result must be an object");
|
|
70
83
|
const ids = value.archivedSessionIds;
|
|
71
|
-
if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string"))
|
|
84
|
+
if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string"))
|
|
85
|
+
throw new TypeError("archivedSessionIds must be a string array");
|
|
72
86
|
return value;
|
|
73
87
|
}
|
|
74
88
|
};
|
|
75
89
|
const deletedSchema = {
|
|
76
90
|
parse(value) {
|
|
77
|
-
if (typeof value !== "object" || value === null || Array.isArray(value) || value.deleted !== true)
|
|
91
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value.deleted !== true)
|
|
92
|
+
throw new TypeError("deleted must be true");
|
|
78
93
|
return value;
|
|
79
94
|
}
|
|
80
95
|
};
|
|
81
96
|
const archivedBatchTargetSchema = {
|
|
82
97
|
parse(value) {
|
|
83
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
98
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
99
|
+
throw new TypeError("target must be an object");
|
|
84
100
|
if (value.scope === "all" || value.scope === "ungrouped") return value;
|
|
85
|
-
if (value.scope === "workspace" && typeof value.workspaceId === "string" && value.workspaceId.length > 0)
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
if (value.scope === "workspace" && typeof value.workspaceId === "string" && value.workspaceId.length > 0)
|
|
102
|
+
return value;
|
|
103
|
+
if (value.scope === "sessions" && Array.isArray(value.sessionIds) && value.sessionIds.length > 0 && value.sessionIds.every((id) => typeof id === "string" && id.length > 0))
|
|
104
|
+
return value;
|
|
105
|
+
throw new TypeError(
|
|
106
|
+
"target.scope must be all, ungrouped, workspace with a non-empty workspaceId, or sessions with non-empty sessionIds"
|
|
107
|
+
);
|
|
88
108
|
}
|
|
89
109
|
};
|
|
90
110
|
const unarchivedBatchSchema = {
|
|
91
111
|
parse(value) {
|
|
92
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
93
|
-
|
|
94
|
-
if (!Array.isArray(value.
|
|
112
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
113
|
+
throw new TypeError("result must be an object");
|
|
114
|
+
if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string"))
|
|
115
|
+
throw new TypeError("archivedSessionIds must be a string array");
|
|
116
|
+
if (!Array.isArray(value.unarchivedSessionIds) || value.unarchivedSessionIds.some((id) => typeof id !== "string"))
|
|
117
|
+
throw new TypeError("unarchivedSessionIds must be a string array");
|
|
95
118
|
return value;
|
|
96
119
|
}
|
|
97
120
|
};
|
|
98
121
|
const archivedWorkspaceBatchSchema = {
|
|
99
122
|
parse(value) {
|
|
100
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
101
|
-
|
|
102
|
-
if (!Array.isArray(value.
|
|
123
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
124
|
+
throw new TypeError("result must be an object");
|
|
125
|
+
if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string"))
|
|
126
|
+
throw new TypeError("archivedSessionIds must be a string array");
|
|
127
|
+
if (!Array.isArray(value.archivedSessionIdsAdded) || value.archivedSessionIdsAdded.some((id) => typeof id !== "string"))
|
|
128
|
+
throw new TypeError("archivedSessionIdsAdded must be a string array");
|
|
103
129
|
return value;
|
|
104
130
|
}
|
|
105
131
|
};
|
|
106
132
|
const deletedBatchSchema = {
|
|
107
133
|
parse(value) {
|
|
108
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
109
|
-
|
|
110
|
-
|
|
134
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
135
|
+
throw new TypeError("result must be an object");
|
|
136
|
+
for (const key of [
|
|
137
|
+
"requestedSessionIds",
|
|
138
|
+
"deletedSessionIds",
|
|
139
|
+
"skippedSessionIds"
|
|
140
|
+
]) {
|
|
141
|
+
if (!Array.isArray(value[key]) || value[key].some((id) => typeof id !== "string"))
|
|
142
|
+
throw new TypeError(`${key} must be a string array`);
|
|
111
143
|
}
|
|
112
|
-
if (!Array.isArray(value.failures) || value.failures.some(
|
|
144
|
+
if (!Array.isArray(value.failures) || value.failures.some(
|
|
145
|
+
(failure) => typeof failure !== "object" || failure === null || typeof failure.sessionId !== "string" || typeof failure.message !== "string"
|
|
146
|
+
))
|
|
147
|
+
throw new TypeError("failures must contain sessionId/message objects");
|
|
113
148
|
return value;
|
|
114
149
|
}
|
|
115
150
|
};
|
|
116
151
|
const archivedSessionMetadataSchema = {
|
|
117
152
|
parse(value) {
|
|
118
|
-
if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.items))
|
|
119
|
-
|
|
120
|
-
if (value.
|
|
153
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.items))
|
|
154
|
+
throw new TypeError("result.items must be an array");
|
|
155
|
+
if (value.items.some(
|
|
156
|
+
(item) => typeof item !== "object" || item === null || typeof item.sessionId !== "string" || typeof item.createdAt !== "number" || !Number.isFinite(item.createdAt)
|
|
157
|
+
))
|
|
158
|
+
throw new TypeError("items must contain sessionId/createdAt objects");
|
|
159
|
+
if (value.repairedSessionIds !== void 0 && (!Array.isArray(value.repairedSessionIds) || value.repairedSessionIds.some((id) => typeof id !== "string")))
|
|
160
|
+
throw new TypeError("repairedSessionIds must be a string array");
|
|
121
161
|
return value;
|
|
122
162
|
}
|
|
123
163
|
};
|
|
@@ -128,18 +168,28 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
|
128
168
|
namespace: "workspaceRegistry",
|
|
129
169
|
method: "unarchiveSession",
|
|
130
170
|
invocation: { kind: "direct" },
|
|
131
|
-
parameters: [
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
171
|
+
parameters: [
|
|
172
|
+
{
|
|
173
|
+
name: "sessionId",
|
|
174
|
+
wire: "sessionId",
|
|
175
|
+
source: "json",
|
|
176
|
+
codec: {
|
|
177
|
+
mode: "strict",
|
|
178
|
+
typeSymbol: "@deepseek-ai/dsh-session/types#SessionId",
|
|
179
|
+
schema: sessionIdSchema
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
],
|
|
137
183
|
result: {
|
|
138
184
|
mode: "strict",
|
|
139
185
|
typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionIds",
|
|
140
186
|
schema: archivedSetSchema
|
|
141
187
|
},
|
|
142
|
-
sourceLocation: {
|
|
188
|
+
sourceLocation: {
|
|
189
|
+
file: "@michengai/dsh-archive-manager/lib/workspace.js",
|
|
190
|
+
line: 1,
|
|
191
|
+
column: 1
|
|
192
|
+
}
|
|
143
193
|
},
|
|
144
194
|
{
|
|
145
195
|
id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteSession",
|
|
@@ -147,18 +197,28 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
|
147
197
|
namespace: "workspaceRegistry",
|
|
148
198
|
method: "deleteSession",
|
|
149
199
|
invocation: { kind: "direct" },
|
|
150
|
-
parameters: [
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
200
|
+
parameters: [
|
|
201
|
+
{
|
|
202
|
+
name: "sessionId",
|
|
203
|
+
wire: "sessionId",
|
|
204
|
+
source: "json",
|
|
205
|
+
codec: {
|
|
206
|
+
mode: "strict",
|
|
207
|
+
typeSymbol: "@deepseek-ai/dsh-session/types#SessionId",
|
|
208
|
+
schema: sessionIdSchema
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
],
|
|
156
212
|
result: {
|
|
157
213
|
mode: "strict",
|
|
158
214
|
typeSymbol: "@michengai/dsh-archive-manager/types#Deleted",
|
|
159
215
|
schema: deletedSchema
|
|
160
216
|
},
|
|
161
|
-
sourceLocation: {
|
|
217
|
+
sourceLocation: {
|
|
218
|
+
file: "@michengai/dsh-archive-manager/lib/workspace.js",
|
|
219
|
+
line: 1,
|
|
220
|
+
column: 1
|
|
221
|
+
}
|
|
162
222
|
},
|
|
163
223
|
{
|
|
164
224
|
id: "@michengai/dsh-archive-manager#workspaceRegistry/unarchiveSessions",
|
|
@@ -166,18 +226,28 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
|
166
226
|
namespace: "workspaceRegistry",
|
|
167
227
|
method: "unarchiveSessions",
|
|
168
228
|
invocation: { kind: "direct" },
|
|
169
|
-
parameters: [
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
229
|
+
parameters: [
|
|
230
|
+
{
|
|
231
|
+
name: "target",
|
|
232
|
+
wire: "target",
|
|
233
|
+
source: "json",
|
|
234
|
+
codec: {
|
|
235
|
+
mode: "strict",
|
|
236
|
+
typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedBatchTarget",
|
|
237
|
+
schema: archivedBatchTargetSchema
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
],
|
|
175
241
|
result: {
|
|
176
242
|
mode: "strict",
|
|
177
243
|
typeSymbol: "@michengai/dsh-archive-manager/types#UnarchivedBatch",
|
|
178
244
|
schema: unarchivedBatchSchema
|
|
179
245
|
},
|
|
180
|
-
sourceLocation: {
|
|
246
|
+
sourceLocation: {
|
|
247
|
+
file: "@michengai/dsh-archive-manager/lib/workspace.js",
|
|
248
|
+
line: 1,
|
|
249
|
+
column: 1
|
|
250
|
+
}
|
|
181
251
|
},
|
|
182
252
|
{
|
|
183
253
|
id: "@michengai/dsh-archive-manager#workspaceRegistry/archiveWorkspaceSessions",
|
|
@@ -185,18 +255,28 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
|
185
255
|
namespace: "workspaceRegistry",
|
|
186
256
|
method: "archiveWorkspaceSessions",
|
|
187
257
|
invocation: { kind: "direct" },
|
|
188
|
-
parameters: [
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
258
|
+
parameters: [
|
|
259
|
+
{
|
|
260
|
+
name: "workspaceId",
|
|
261
|
+
wire: "workspaceId",
|
|
262
|
+
source: "json",
|
|
263
|
+
codec: {
|
|
264
|
+
mode: "strict",
|
|
265
|
+
typeSymbol: "@deepseek-ai/dsh-workspace/types#WorkspaceId",
|
|
266
|
+
schema: workspaceIdSchema
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
],
|
|
194
270
|
result: {
|
|
195
271
|
mode: "strict",
|
|
196
272
|
typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedWorkspaceBatch",
|
|
197
273
|
schema: archivedWorkspaceBatchSchema
|
|
198
274
|
},
|
|
199
|
-
sourceLocation: {
|
|
275
|
+
sourceLocation: {
|
|
276
|
+
file: "@michengai/dsh-archive-manager/lib/workspace.js",
|
|
277
|
+
line: 1,
|
|
278
|
+
column: 1
|
|
279
|
+
}
|
|
200
280
|
},
|
|
201
281
|
{
|
|
202
282
|
id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteArchivedSessions",
|
|
@@ -204,18 +284,28 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
|
204
284
|
namespace: "workspaceRegistry",
|
|
205
285
|
method: "deleteArchivedSessions",
|
|
206
286
|
invocation: { kind: "direct" },
|
|
207
|
-
parameters: [
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
287
|
+
parameters: [
|
|
288
|
+
{
|
|
289
|
+
name: "target",
|
|
290
|
+
wire: "target",
|
|
291
|
+
source: "json",
|
|
292
|
+
codec: {
|
|
293
|
+
mode: "strict",
|
|
294
|
+
typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedBatchTarget",
|
|
295
|
+
schema: archivedBatchTargetSchema
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
],
|
|
213
299
|
result: {
|
|
214
300
|
mode: "strict",
|
|
215
301
|
typeSymbol: "@michengai/dsh-archive-manager/types#DeletedBatch",
|
|
216
302
|
schema: deletedBatchSchema
|
|
217
303
|
},
|
|
218
|
-
sourceLocation: {
|
|
304
|
+
sourceLocation: {
|
|
305
|
+
file: "@michengai/dsh-archive-manager/lib/workspace.js",
|
|
306
|
+
line: 1,
|
|
307
|
+
column: 1
|
|
308
|
+
}
|
|
219
309
|
},
|
|
220
310
|
{
|
|
221
311
|
id: "@michengai/dsh-archive-manager#workspaceRegistry/archivedSessionMetadata",
|
|
@@ -229,7 +319,11 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
|
229
319
|
typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionMetadata",
|
|
230
320
|
schema: archivedSessionMetadataSchema
|
|
231
321
|
},
|
|
232
|
-
sourceLocation: {
|
|
322
|
+
sourceLocation: {
|
|
323
|
+
file: "@michengai/dsh-archive-manager/lib/workspace.js",
|
|
324
|
+
line: 1,
|
|
325
|
+
column: 1
|
|
326
|
+
}
|
|
233
327
|
}
|
|
234
328
|
];
|
|
235
329
|
const ARCHIVE_MANAGER_TYPERT = {
|
|
@@ -276,114 +370,160 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
276
370
|
registerHostRemote(this.ctx);
|
|
277
371
|
}
|
|
278
372
|
/**
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
373
|
+
* 归档设置页创建时间排序所需的最小元数据。老用户可能仍有会话原文和
|
|
374
|
+
* 归档标记、却没有投影缓存;这里按需从完整日志重建一次,再通知客户端
|
|
375
|
+
* 刷新会话列表。已有缓存不读原文,新老 DSH 的缓存布局都走同一 put。
|
|
376
|
+
*/
|
|
283
377
|
async archivedSessionMetadata() {
|
|
284
378
|
const items = [];
|
|
285
379
|
const repairedSessionIds = [];
|
|
286
|
-
for (const sessionId of [
|
|
380
|
+
for (const sessionId of [
|
|
381
|
+
...new Set(this.requireState().archivedSessionIds)
|
|
382
|
+
]) {
|
|
287
383
|
try {
|
|
288
384
|
const header = await this.readSessionHeader(sessionId);
|
|
289
|
-
if (await this.repairArchivedProjection(header))
|
|
290
|
-
|
|
385
|
+
if (await this.repairArchivedProjection(header))
|
|
386
|
+
repairedSessionIds.push(sessionId);
|
|
387
|
+
if (typeof header.createdAt === "number" && Number.isFinite(header.createdAt))
|
|
388
|
+
items.push({ sessionId, createdAt: header.createdAt });
|
|
291
389
|
} catch (error) {
|
|
292
|
-
this.ctx.logger.warn(
|
|
390
|
+
this.ctx.logger.warn(
|
|
391
|
+
`archive-manager: could not read creation time for archived session "${sessionId}": ${String(error)}`
|
|
392
|
+
);
|
|
293
393
|
}
|
|
294
394
|
}
|
|
295
|
-
return {
|
|
395
|
+
return {
|
|
396
|
+
items,
|
|
397
|
+
...repairedSessionIds.length === 0 ? {} : { repairedSessionIds }
|
|
398
|
+
};
|
|
296
399
|
}
|
|
297
400
|
/** 从会话原文补齐缺失的派生缓存;任何失败都只降级为原有无摘要列表。 */
|
|
298
401
|
async repairArchivedProjection(header) {
|
|
299
402
|
const cache = this.ctx.get("sessionProjectionCache");
|
|
300
403
|
const persistence = this.ctx.get("sessionPersistence");
|
|
301
404
|
const projections = this.ctx.get("sessionProjections");
|
|
302
|
-
if (cache === void 0 || typeof cache.cachedSnapshot !== "function" || typeof cache.put !== "function")
|
|
303
|
-
|
|
405
|
+
if (cache === void 0 || typeof cache.cachedSnapshot !== "function" || typeof cache.put !== "function")
|
|
406
|
+
return false;
|
|
407
|
+
if (persistence === void 0 || typeof persistence.readFrom !== "function" && typeof persistence.open !== "function" || projections === void 0 || typeof projections.restore !== "function")
|
|
408
|
+
return false;
|
|
304
409
|
try {
|
|
305
|
-
if (!header.isSeeded && cache.cachedSnapshot(header, 0) !== void 0)
|
|
410
|
+
if (!header.isSeeded && cache.cachedSnapshot(header, 0) !== void 0)
|
|
411
|
+
return false;
|
|
306
412
|
const stored = await this.readStoredProjectionSource(persistence, header.id);
|
|
307
413
|
const meta = stored.meta ?? header;
|
|
308
414
|
if (meta.isSeeded === true && stored.inheritedEventCount === void 0) {
|
|
309
|
-
this.ctx.logger.warn(
|
|
415
|
+
this.ctx.logger.warn(
|
|
416
|
+
`archive-manager: projection repair for seeded archived session "${header.id}" skipped because its inherited event count is unavailable`
|
|
417
|
+
);
|
|
310
418
|
return false;
|
|
311
419
|
}
|
|
312
420
|
const inheritedEventCount = stored.inheritedEventCount ?? 0;
|
|
313
421
|
if (meta.isSeeded !== true && inheritedEventCount !== 0) {
|
|
314
|
-
this.ctx.logger.warn(
|
|
422
|
+
this.ctx.logger.warn(
|
|
423
|
+
`archive-manager: projection repair for unseeded archived session "${header.id}" skipped because its inherited event count is not zero`
|
|
424
|
+
);
|
|
315
425
|
return false;
|
|
316
426
|
}
|
|
317
427
|
if (cache.cachedSnapshot(meta, inheritedEventCount) !== void 0) return false;
|
|
318
|
-
const restored = projections.restore(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
createdAt: meta.createdAt,
|
|
324
|
-
...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
325
|
-
isSeeded: meta.isSeeded ?? false,
|
|
428
|
+
const restored = projections.restore(
|
|
429
|
+
{},
|
|
430
|
+
stored.events,
|
|
431
|
+
0,
|
|
432
|
+
meta,
|
|
326
433
|
inheritedEventCount
|
|
327
|
-
|
|
434
|
+
);
|
|
435
|
+
if (restored === void 0 || typeof restored !== "object" || restored.checkpoint === void 0)
|
|
436
|
+
return false;
|
|
437
|
+
await cache.put(
|
|
438
|
+
header.id,
|
|
439
|
+
{
|
|
440
|
+
// 支持的宿主头部均有 version;新版缓存校验格式代际,旧版逐字段比较忽略此键。
|
|
441
|
+
formatVersion: meta.version,
|
|
442
|
+
createdAt: meta.createdAt,
|
|
443
|
+
...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
444
|
+
isSeeded: meta.isSeeded ?? false,
|
|
445
|
+
inheritedEventCount
|
|
446
|
+
},
|
|
447
|
+
restored.checkpoint
|
|
448
|
+
);
|
|
328
449
|
return true;
|
|
329
450
|
} catch (error) {
|
|
330
|
-
this.ctx.logger.warn(
|
|
451
|
+
this.ctx.logger.warn(
|
|
452
|
+
`archive-manager: projection repair for archived session "${header.id}" failed: ${String(error)}`
|
|
453
|
+
);
|
|
331
454
|
return false;
|
|
332
455
|
}
|
|
333
456
|
}
|
|
334
457
|
/** 新版读句柄必须关闭;旧版仍沿用 readFrom,避免激活 Agent 或写入会话日志。 */
|
|
335
458
|
async readStoredProjectionSource(persistence, sessionId) {
|
|
336
|
-
if (typeof persistence.readFrom === "function")
|
|
459
|
+
if (typeof persistence.readFrom === "function")
|
|
460
|
+
return persistence.readFrom(sessionId, 0);
|
|
337
461
|
const handle = await persistence.open(sessionId, "read");
|
|
338
462
|
try {
|
|
339
463
|
const { events } = await handle.read(0);
|
|
340
|
-
return {
|
|
464
|
+
return {
|
|
465
|
+
meta: handle.header,
|
|
466
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
467
|
+
events
|
|
468
|
+
};
|
|
341
469
|
} finally {
|
|
342
470
|
await handle.close();
|
|
343
471
|
}
|
|
344
472
|
}
|
|
345
473
|
/**
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
474
|
+
* 把一个会话移出注册表全局归档集合,恢复其正常可见性(其记账位从未
|
|
475
|
+
* 移动,会话在原工作区位置重新出现)。幂等:未归档的已知会话直接返回
|
|
476
|
+
* 当前集合不写入;未知会话与 `archiveSession` 一样抛错。
|
|
477
|
+
* @param sessionId - 要取消归档的会话。
|
|
478
|
+
* @returns 更新后的完整归档集合。
|
|
479
|
+
*/
|
|
352
480
|
async unarchiveSession(sessionId) {
|
|
353
481
|
return this.enqueueOperation(async () => {
|
|
354
|
-
if (!await this.sessionKnown(sessionId))
|
|
482
|
+
if (!await this.sessionKnown(sessionId))
|
|
483
|
+
throw new ArchiveUnknownSessionError(sessionId);
|
|
355
484
|
const state = this.requireState();
|
|
356
|
-
if (!state.archivedSessionIds.includes(sessionId))
|
|
485
|
+
if (!state.archivedSessionIds.includes(sessionId))
|
|
486
|
+
return { archivedSessionIds: [...state.archivedSessionIds] };
|
|
357
487
|
const next = {
|
|
358
488
|
...state,
|
|
359
|
-
archivedSessionIds: state.archivedSessionIds.filter(
|
|
489
|
+
archivedSessionIds: state.archivedSessionIds.filter(
|
|
490
|
+
(id) => id !== sessionId
|
|
491
|
+
)
|
|
360
492
|
};
|
|
361
493
|
await this.setState(next);
|
|
362
494
|
return { archivedSessionIds: [...next.archivedSessionIds] };
|
|
363
495
|
});
|
|
364
496
|
}
|
|
365
497
|
/**
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
498
|
+
* 将一个工作区内所有会话加入归档集合。先确认所有待归档会话仍存在,
|
|
499
|
+
* 再执行单次状态写入,因此未知会话不会导致项目只归档一部分。
|
|
500
|
+
*/
|
|
369
501
|
async archiveWorkspaceSessions(workspaceId) {
|
|
370
502
|
return this.enqueueOperation(async () => {
|
|
371
503
|
workspaceId = workspaceIdSchema.parse(workspaceId);
|
|
372
504
|
const workspace = this.requireTable().get(workspaceId);
|
|
373
|
-
if (workspace === void 0)
|
|
505
|
+
if (workspace === void 0)
|
|
506
|
+
throw new Error(`unknown workspace "${workspaceId}"`);
|
|
374
507
|
const state = this.requireState();
|
|
375
508
|
const archived = new Set(state.archivedSessionIds);
|
|
376
|
-
const archivedSessionIdsAdded = [...new Set(workspace.sessionIds)].filter(
|
|
509
|
+
const archivedSessionIdsAdded = [...new Set(workspace.sessionIds)].filter(
|
|
510
|
+
(sessionId) => !archived.has(sessionId)
|
|
511
|
+
);
|
|
377
512
|
for (const sessionId of archivedSessionIdsAdded) {
|
|
378
|
-
if (!await this.sessionKnown(sessionId))
|
|
513
|
+
if (!await this.sessionKnown(sessionId))
|
|
514
|
+
throw new ArchiveUnknownSessionError(sessionId);
|
|
379
515
|
}
|
|
380
|
-
if (archivedSessionIdsAdded.length === 0)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
516
|
+
if (archivedSessionIdsAdded.length === 0)
|
|
517
|
+
return {
|
|
518
|
+
archivedSessionIds: [...state.archivedSessionIds],
|
|
519
|
+
archivedSessionIdsAdded
|
|
520
|
+
};
|
|
384
521
|
const next = {
|
|
385
522
|
...state,
|
|
386
|
-
archivedSessionIds: [
|
|
523
|
+
archivedSessionIds: [
|
|
524
|
+
...state.archivedSessionIds,
|
|
525
|
+
...archivedSessionIdsAdded
|
|
526
|
+
]
|
|
387
527
|
};
|
|
388
528
|
await this.setState(next);
|
|
389
529
|
return {
|
|
@@ -393,21 +533,24 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
393
533
|
});
|
|
394
534
|
}
|
|
395
535
|
/**
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
536
|
+
* 按宿主权威归档集合一次恢复全部、一个工作区或未分组的归档会话。
|
|
537
|
+
* 目标全部来自已归档集合,因此即使日志已被外部移除,也会清掉陈旧归档标记。
|
|
538
|
+
*/
|
|
399
539
|
async unarchiveSessions(target) {
|
|
400
540
|
return this.enqueueOperation(async () => {
|
|
401
541
|
const unarchivedSessionIds = this.archivedSessionIdsForTarget(target);
|
|
402
|
-
if (unarchivedSessionIds.length === 0)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
542
|
+
if (unarchivedSessionIds.length === 0)
|
|
543
|
+
return {
|
|
544
|
+
archivedSessionIds: [...this.requireState().archivedSessionIds],
|
|
545
|
+
unarchivedSessionIds: []
|
|
546
|
+
};
|
|
406
547
|
const restored = new Set(unarchivedSessionIds);
|
|
407
548
|
const state = this.requireState();
|
|
408
549
|
const next = {
|
|
409
550
|
...state,
|
|
410
|
-
archivedSessionIds: state.archivedSessionIds.filter(
|
|
551
|
+
archivedSessionIds: state.archivedSessionIds.filter(
|
|
552
|
+
(id) => !restored.has(id)
|
|
553
|
+
)
|
|
411
554
|
};
|
|
412
555
|
await this.setState(next);
|
|
413
556
|
return {
|
|
@@ -417,9 +560,9 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
417
560
|
});
|
|
418
561
|
}
|
|
419
562
|
/**
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
563
|
+
* 按作用域永久删除归档会话。跨会话文件删除无法组成事务,因此继续处理
|
|
564
|
+
* 后续目标并把成功、并发消失和失败分别返回给客户端。
|
|
565
|
+
*/
|
|
423
566
|
async deleteArchivedSessions(target) {
|
|
424
567
|
return this.enqueueOperation(async () => {
|
|
425
568
|
const requestedSessionIds = this.archivedSessionIdsForTarget(target);
|
|
@@ -443,14 +586,19 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
443
586
|
failures.push({ sessionId, message: String(error) });
|
|
444
587
|
}
|
|
445
588
|
}
|
|
446
|
-
return {
|
|
589
|
+
return {
|
|
590
|
+
requestedSessionIds,
|
|
591
|
+
deletedSessionIds,
|
|
592
|
+
skippedSessionIds,
|
|
593
|
+
failures
|
|
594
|
+
};
|
|
447
595
|
});
|
|
448
596
|
}
|
|
449
597
|
/**
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
598
|
+
* 清理已无转录的陈旧归档项。缓存墓碑只在清除在途写入期间短暂持有:
|
|
599
|
+
* workspace 没有可记录的旧 header 身份,永久保留它会挡住未来的冷复用。
|
|
600
|
+
* 归档标记最后清除,前序可失败步骤出错时批量入口仍能再次命中。
|
|
601
|
+
*/
|
|
454
602
|
async cleanupUnknownArchivedSession(sessionId) {
|
|
455
603
|
const projCache = this.ctx.get("sessionProjectionCache");
|
|
456
604
|
await projCache?.whenIdle?.();
|
|
@@ -465,7 +613,9 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
465
613
|
if (state.archivedSessionIds.includes(sessionId)) {
|
|
466
614
|
await this.setState({
|
|
467
615
|
...state,
|
|
468
|
-
archivedSessionIds: state.archivedSessionIds.filter(
|
|
616
|
+
archivedSessionIds: state.archivedSessionIds.filter(
|
|
617
|
+
(id) => id !== sessionId
|
|
618
|
+
)
|
|
469
619
|
});
|
|
470
620
|
}
|
|
471
621
|
}
|
|
@@ -481,30 +631,33 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
481
631
|
}
|
|
482
632
|
if (target.scope === "workspace") {
|
|
483
633
|
const workspace = this.requireTable().get(target.workspaceId);
|
|
484
|
-
if (workspace === void 0)
|
|
634
|
+
if (workspace === void 0)
|
|
635
|
+
throw new Error(`unknown workspace "${target.workspaceId}"`);
|
|
485
636
|
const accounted2 = new Set(workspace.sessionIds);
|
|
486
637
|
return archivedSessionIds.filter((id) => accounted2.has(id));
|
|
487
638
|
}
|
|
488
639
|
const accounted = /* @__PURE__ */ new Set();
|
|
489
640
|
const table = this.requireTable();
|
|
490
641
|
for (const workspaceId of state.workspaceIds) {
|
|
491
|
-
for (const sessionId of table.get(workspaceId)?.sessionIds ?? [])
|
|
642
|
+
for (const sessionId of table.get(workspaceId)?.sessionIds ?? [])
|
|
643
|
+
accounted.add(sessionId);
|
|
492
644
|
}
|
|
493
645
|
return archivedSessionIds.filter((id) => !accounted.has(id));
|
|
494
646
|
}
|
|
495
647
|
/**
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
648
|
+
* 永久删除一个会话及其全部痕迹(转录目录、工作区记账、归档标记、
|
|
649
|
+
* 投影缓存行)。
|
|
650
|
+
* @param sessionId - 要删除的会话。
|
|
651
|
+
* @returns 持久化完成后的 `{ deleted: true }`。
|
|
652
|
+
* @throws {@link ArchiveUnknownSessionError} 会话未知时抛出。
|
|
653
|
+
*/
|
|
502
654
|
async deleteSession(sessionId) {
|
|
503
655
|
return this.enqueueOperation(() => this.deleteSessionCore(sessionId));
|
|
504
656
|
}
|
|
505
657
|
/** 串行化后的删除主体(级联路径复用:它已持有操作链,绝不能再入队)。 */
|
|
506
658
|
async deleteSessionCore(sessionId) {
|
|
507
|
-
if (!await this.sessionKnown(sessionId))
|
|
659
|
+
if (!await this.sessionKnown(sessionId))
|
|
660
|
+
throw new ArchiveUnknownSessionError(sessionId);
|
|
508
661
|
const sessions = this.ctx.get("sessions");
|
|
509
662
|
const live = sessions?.get(sessionId);
|
|
510
663
|
const deletedHeader = this.headers.get(sessionId) ?? live?.header;
|
|
@@ -512,7 +665,8 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
512
665
|
await sessions.flush(live);
|
|
513
666
|
const entry = sessions.liveEntryFor(live);
|
|
514
667
|
sessions.detachEntered(entry);
|
|
515
|
-
} else if (sessions !== void 0)
|
|
668
|
+
} else if (sessions !== void 0)
|
|
669
|
+
await this.publishColdSessionRemoval(sessionId, sessions);
|
|
516
670
|
const projCache = this.ctx.get("sessionProjectionCache");
|
|
517
671
|
await projCache?.whenIdle?.();
|
|
518
672
|
if (projCache !== void 0) await projCache.delete(sessionId);
|
|
@@ -523,12 +677,15 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
523
677
|
if (state.archivedSessionIds.includes(sessionId)) {
|
|
524
678
|
await this.setState({
|
|
525
679
|
...state,
|
|
526
|
-
archivedSessionIds: state.archivedSessionIds.filter(
|
|
680
|
+
archivedSessionIds: state.archivedSessionIds.filter(
|
|
681
|
+
(id) => id !== sessionId
|
|
682
|
+
)
|
|
527
683
|
});
|
|
528
684
|
}
|
|
529
685
|
await this.removeFromWorkspaceAccounts(sessionId);
|
|
530
686
|
this.forgetIndexedSession(sessionId);
|
|
531
|
-
if (deletedHeader !== void 0)
|
|
687
|
+
if (deletedHeader !== void 0)
|
|
688
|
+
this.deletedIdentities.set(sessionId, headerIdentity(deletedHeader));
|
|
532
689
|
this.publishDeletedSession(sessionId);
|
|
533
690
|
return { deleted: true };
|
|
534
691
|
}
|
|
@@ -537,13 +694,15 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
537
694
|
try {
|
|
538
695
|
this.ctx.emit("api-session/removed", sessionId);
|
|
539
696
|
} catch (error) {
|
|
540
|
-
this.ctx.logger.warn(
|
|
697
|
+
this.ctx.logger.warn(
|
|
698
|
+
`archive-manager: session "${sessionId}" deleted but removal notification failed: ${String(error)}`
|
|
699
|
+
);
|
|
541
700
|
}
|
|
542
701
|
}
|
|
543
702
|
/**
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
703
|
+
* 从父类内存索引中遗忘已删除会话,并阻止后续 indexHeaders 把它加回。
|
|
704
|
+
* 实时会话以同 id 重新出现时(自定义 id 复用)会撤掉墓碑。
|
|
705
|
+
*/
|
|
547
706
|
clearTombstone(sessionId) {
|
|
548
707
|
this.deletedSessionIds.delete(sessionId);
|
|
549
708
|
this.deletedIdentities.delete(sessionId);
|
|
@@ -552,15 +711,21 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
552
711
|
this.ctx.get("sessionProjectionCache")?.clearTombstone?.(sessionId);
|
|
553
712
|
}
|
|
554
713
|
forgetIndexedSession(sessionId) {
|
|
555
|
-
for (const evicted of trackTombstone(
|
|
714
|
+
for (const evicted of trackTombstone(
|
|
715
|
+
this.deletedSessionIds,
|
|
716
|
+
this.deletedSessionOrder,
|
|
717
|
+
sessionId,
|
|
718
|
+
this.deletedSessionTombstoneLimit
|
|
719
|
+
))
|
|
720
|
+
this.deletedIdentities.delete(evicted);
|
|
556
721
|
this.headers.delete(sessionId);
|
|
557
722
|
this.sessionPaths.delete(sessionId);
|
|
558
723
|
this.invalidSessionPaths.delete(sessionId);
|
|
559
724
|
}
|
|
560
725
|
/**
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
726
|
+
* 已删除会话对归档/删除入口都视为未知。实时复用同一 id 时撤墓碑,
|
|
727
|
+
* 避免挡住新会话。
|
|
728
|
+
*/
|
|
564
729
|
async sessionKnown(id) {
|
|
565
730
|
if (this.ctx.get("sessions")?.get(id) !== void 0) {
|
|
566
731
|
this.clearTombstone(id);
|
|
@@ -570,40 +735,46 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
570
735
|
return super.sessionKnown(id);
|
|
571
736
|
}
|
|
572
737
|
/**
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
738
|
+
* 墓碑分支的冷复用探针:其他进程以同 id 重建并落盘的新会话(日志身份
|
|
739
|
+
* 不同)撤墓碑放行并重新编入索引;stale list() 里同生命周期的旧头部
|
|
740
|
+
* 仍视为未知。身份不可考(删除时未取到头部)时保守维持未知。
|
|
741
|
+
*/
|
|
577
742
|
async coldReuseKnown(id) {
|
|
578
743
|
const deletedIdentity = this.deletedIdentities.get(id);
|
|
579
744
|
if (deletedIdentity === void 0) return false;
|
|
580
745
|
const persistence = this.ctx.get("sessionPersistence");
|
|
581
|
-
if (persistence === void 0 || typeof persistence.list !== "function")
|
|
746
|
+
if (persistence === void 0 || typeof persistence.list !== "function")
|
|
747
|
+
return false;
|
|
582
748
|
let header;
|
|
583
749
|
try {
|
|
584
750
|
header = (await this.listStoredHeaders()).find((item) => item.id === id);
|
|
585
751
|
} catch (error) {
|
|
586
|
-
this.ctx.logger.warn(
|
|
752
|
+
this.ctx.logger.warn(
|
|
753
|
+
`archive-manager: cold-reuse probe for "${id}" failed: ${String(error)}`
|
|
754
|
+
);
|
|
587
755
|
return false;
|
|
588
756
|
}
|
|
589
757
|
if (header === void 0) return false;
|
|
590
758
|
const listed = headerIdentity(header);
|
|
591
|
-
if (listed.createdAt === deletedIdentity.createdAt && listed.cwd === deletedIdentity.cwd)
|
|
759
|
+
if (listed.createdAt === deletedIdentity.createdAt && listed.cwd === deletedIdentity.cwd)
|
|
760
|
+
return false;
|
|
592
761
|
this.clearTombstone(id);
|
|
593
762
|
await this.indexHeader(header);
|
|
594
763
|
return true;
|
|
595
764
|
}
|
|
596
765
|
/**
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
766
|
+
* 父类 indexHeaders 只增不减;跳过墓碑 id,避免 stale persistence.list()
|
|
767
|
+
* 把已删除会话重新编入 headers。
|
|
768
|
+
*/
|
|
600
769
|
async indexHeader(header) {
|
|
601
770
|
if (this.deletedSessionIds.has(header.id)) return;
|
|
602
771
|
return super.indexHeader(header);
|
|
603
772
|
}
|
|
604
773
|
/** 统一旧版头部数组与 0.1.3 的持久化快照,供父类索引和本插件枚举共用。 */
|
|
605
774
|
async listStoredHeaders() {
|
|
606
|
-
return (await this.ctx.sessionPersistence.list()).map(
|
|
775
|
+
return (await this.ctx.sessionPersistence.list()).map(
|
|
776
|
+
(item) => item.header ?? item
|
|
777
|
+
);
|
|
607
778
|
}
|
|
608
779
|
async indexHeaders(items) {
|
|
609
780
|
for (const item of items) await this.indexHeader(item.header ?? item);
|
|
@@ -611,7 +782,8 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
611
782
|
/** 为未处于实时状态的持久化会话发布相同的移除事件。 */
|
|
612
783
|
async publishColdSessionRemoval(sessionId, sessions) {
|
|
613
784
|
const persistence = this.ctx.get("sessionPersistence");
|
|
614
|
-
if (persistence === void 0 || typeof persistence.prepare !== "function")
|
|
785
|
+
if (persistence === void 0 || typeof persistence.prepare !== "function")
|
|
786
|
+
return;
|
|
615
787
|
try {
|
|
616
788
|
const preparation = await persistence.prepare(sessionId);
|
|
617
789
|
const detach = sessions.enter(preparation.session);
|
|
@@ -622,19 +794,25 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
622
794
|
preparation[Symbol.dispose]();
|
|
623
795
|
}
|
|
624
796
|
} catch (error) {
|
|
625
|
-
this.ctx.logger.warn(
|
|
797
|
+
this.ctx.logger.warn(
|
|
798
|
+
`archive-manager: could not publish removal for stored session "${sessionId}": ${String(error)}`
|
|
799
|
+
);
|
|
626
800
|
}
|
|
627
801
|
}
|
|
628
802
|
/** 官方 JSONL 已知布局清理会话专属目录;其他后端只删除定位到的工件。 */
|
|
629
803
|
async removeTranscriptDirectory(sessionId) {
|
|
630
804
|
const persistence = this.ctx.get("sessionPersistence");
|
|
631
805
|
if (persistence === void 0 || typeof persistence.locate !== "function") {
|
|
632
|
-
throw new Error(
|
|
806
|
+
throw new Error(
|
|
807
|
+
`cannot delete session "${sessionId}": the session persistence backend does not expose locate() to resolve its transcript artifact`
|
|
808
|
+
);
|
|
633
809
|
}
|
|
634
810
|
const header = await this.readSessionHeader(sessionId);
|
|
635
811
|
const location = persistence.locate(header);
|
|
636
812
|
if (location === void 0 || typeof location.path !== "string") {
|
|
637
|
-
throw new Error(
|
|
813
|
+
throw new Error(
|
|
814
|
+
`cannot delete session "${sessionId}": the session persistence backend could not resolve its transcript artifact`
|
|
815
|
+
);
|
|
638
816
|
}
|
|
639
817
|
let target = { path: location.path, kind: "transcript artifact" };
|
|
640
818
|
try {
|
|
@@ -649,15 +827,21 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
649
827
|
if (error?.code === "ENOENT") continue;
|
|
650
828
|
throw error;
|
|
651
829
|
}
|
|
652
|
-
if (stat.isSymbolicLink())
|
|
653
|
-
|
|
830
|
+
if (stat.isSymbolicLink())
|
|
831
|
+
throw new Error(`refusing to delete through symbolic link "${path}"`);
|
|
832
|
+
if (!stat.isDirectory())
|
|
833
|
+
throw new Error(`expected session storage directory "${path}"`);
|
|
654
834
|
}
|
|
655
835
|
} else if (persistence.name === "session-persistence-jsonl") {
|
|
656
|
-
this.ctx.logger.warn(
|
|
836
|
+
this.ctx.logger.warn(
|
|
837
|
+
`archive-manager: session "${sessionId}": JSONL directory ownership could not be verified; falling back to artifact-only deletion at "${location.path}" (parent directory retained)`
|
|
838
|
+
);
|
|
657
839
|
}
|
|
658
840
|
await rm(target.path, { recursive: true, force: true });
|
|
659
841
|
if (typeof persistence.stat === "function" && await persistence.stat(sessionId) !== void 0) {
|
|
660
|
-
throw new Error(
|
|
842
|
+
throw new Error(
|
|
843
|
+
`session "${sessionId}" is still present in persistence after artifact removal`
|
|
844
|
+
);
|
|
661
845
|
}
|
|
662
846
|
} catch (error) {
|
|
663
847
|
const message = `cannot delete session "${sessionId}": cleanup of ${target.kind} "${target.path}" failed; bookkeeping retained for retry`;
|
|
@@ -676,24 +860,27 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
676
860
|
const next = await table.update(workspaceId, (current) => ({
|
|
677
861
|
...current,
|
|
678
862
|
sessionIds: current.sessionIds.filter((id) => id !== sessionId),
|
|
679
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
863
|
+
updatedAt: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString()
|
|
680
864
|
}));
|
|
681
865
|
const entity = this.entities.get(workspaceId);
|
|
682
866
|
if (entity !== void 0) entity.record = next;
|
|
683
867
|
}
|
|
684
868
|
}
|
|
685
869
|
/** 尽力而为的级联删除:删除 `sessionId` 的 SUBAGENT 子会话。
|
|
686
|
-
|
|
687
|
-
|
|
870
|
+
* 仅头部标记 `origin: "subagent"` 的会话参与:单凭 `parentSession` 有歧义
|
|
871
|
+
*(fork 分支也携带它),而 fork 分支是独立的用户会话,绝不能被级联删除。 */
|
|
688
872
|
async deleteDescendants(sessionId) {
|
|
689
873
|
try {
|
|
690
874
|
const descendants = [];
|
|
691
875
|
const sessions = this.ctx.get("sessions");
|
|
692
|
-
if (sessions !== void 0)
|
|
693
|
-
|
|
694
|
-
|
|
876
|
+
if (sessions !== void 0)
|
|
877
|
+
for (const session of sessions.list()) {
|
|
878
|
+
if (session.header.parentSession === sessionId && session.header.origin === "subagent")
|
|
879
|
+
descendants.push(session.id);
|
|
880
|
+
}
|
|
695
881
|
for (const header of await this.listStoredHeaders()) {
|
|
696
|
-
if (header.parentSession === sessionId && header.origin === "subagent" && !descendants.includes(header.id))
|
|
882
|
+
if (header.parentSession === sessionId && header.origin === "subagent" && !descendants.includes(header.id))
|
|
883
|
+
descendants.push(header.id);
|
|
697
884
|
}
|
|
698
885
|
for (const childId of descendants) {
|
|
699
886
|
try {
|
|
@@ -701,11 +888,15 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
701
888
|
await this.deleteSessionCore(childId);
|
|
702
889
|
} catch (error) {
|
|
703
890
|
if (error instanceof ArchiveUnknownSessionError) continue;
|
|
704
|
-
this.ctx.logger.warn(
|
|
891
|
+
this.ctx.logger.warn(
|
|
892
|
+
`archive-manager: cascade delete of subagent session "${childId}" (child of "${sessionId}") failed: ${String(error)}`
|
|
893
|
+
);
|
|
705
894
|
}
|
|
706
895
|
}
|
|
707
896
|
} catch (error) {
|
|
708
|
-
this.ctx.logger.warn(
|
|
897
|
+
this.ctx.logger.warn(
|
|
898
|
+
`archive-manager: descendant enumeration for deleted session "${sessionId}" failed: ${String(error)}`
|
|
899
|
+
);
|
|
709
900
|
}
|
|
710
901
|
}
|
|
711
902
|
/** 尽力而为的 spill 清理:移除该会话作用域的 spill 目录。 */
|
|
@@ -713,9 +904,14 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
713
904
|
try {
|
|
714
905
|
const spill = this.ctx.get("spillStore");
|
|
715
906
|
if (spill === void 0 || typeof spill.root !== "string") return;
|
|
716
|
-
await rm(sessionDir(spill.root, sessionId), {
|
|
907
|
+
await rm(sessionDir(spill.root, sessionId), {
|
|
908
|
+
recursive: true,
|
|
909
|
+
force: true
|
|
910
|
+
});
|
|
717
911
|
} catch (error) {
|
|
718
|
-
this.ctx.logger.warn(
|
|
912
|
+
this.ctx.logger.warn(
|
|
913
|
+
`archive-manager: spill cleanup for deleted session "${sessionId}" failed: ${String(error)}`
|
|
914
|
+
);
|
|
719
915
|
}
|
|
720
916
|
}
|
|
721
917
|
};
|