@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/CHANGELOG.md +10 -0
- package/CHANGELOG.zh-CN.md +10 -0
- package/README.md +9 -12
- package/README.zh-CN.md +9 -12
- package/lib/client.js +3777 -4117
- package/lib/index.js +5 -16
- package/lib/projcache.js +234 -290
- package/lib/tombstone.js +16 -30
- package/lib/workspace.js +618 -663
- package/package.json +7 -5
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
21
|
+
return `unknown session "${sessionId}" (UNKNOWN_SESSION)`;
|
|
46
22
|
}
|
|
47
23
|
var ArchiveUnknownSessionError = class extends Error {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
32
|
+
return {
|
|
33
|
+
createdAt: header.createdAt,
|
|
34
|
+
cwd: header.cwd ?? null
|
|
35
|
+
};
|
|
61
36
|
}
|
|
62
37
|
const sessionIdSchema = {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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 };
|