dsh-plugin-tool-management 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -1
- package/README.md +67 -50
- package/README_EN.md +61 -38
- package/cordis.patch.yml +10 -1
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +132 -12
- package/lib/client.js +2826 -547
- package/lib/compat/patch-dialect.js +173 -0
- package/lib/compat/preset-reach.js +1 -10
- package/lib/compat/probe.js +205 -24
- package/lib/compat/runtime-notes.js +25 -0
- package/lib/context-inject.js +83 -9
- package/lib/host-names.js +12 -0
- package/lib/http-fence.js +35 -15
- package/lib/hub.js +28 -2
- package/lib/imports/parsers.js +15 -9
- package/lib/imports/upload.js +43 -4
- package/lib/index.js +927 -3897
- package/lib/mcp/loader-token.js +238 -0
- package/lib/mcp/manager.js +1769 -0
- package/lib/mcp/override-blocks.js +10 -3
- package/lib/mcp/patch-yaml.js +351 -0
- package/lib/mcp/secret-guard.js +145 -0
- package/lib/{rules → memories}/archive-engine.js +1 -1
- package/lib/{rules → memories}/archive.js +1 -1
- package/lib/memories/constants.js +128 -0
- package/lib/memories/index-io.js +330 -0
- package/lib/memories/projection.js +280 -0
- package/lib/memories/service.js +686 -0
- package/lib/memories/snapshot.js +672 -0
- package/lib/ops/candidates.js +64 -0
- package/lib/ops/compat.js +226 -0
- package/lib/ops/ctx.js +9 -0
- package/lib/ops/memory.js +678 -0
- package/lib/ops/prompts.js +107 -0
- package/lib/ops/scene-records.js +460 -0
- package/lib/ops/scene-sync.js +17 -0
- package/lib/ops/sessions.js +603 -0
- package/lib/ops/trash.js +140 -0
- package/lib/paths.js +103 -0
- package/lib/prompts/preset-id.js +49 -0
- package/lib/{agents-md → prompts}/service.js +1 -1
- package/lib/request-gate.js +320 -0
- package/lib/scene-prompt-sync.js +4 -4
- package/lib/scenes/candidates.js +344 -0
- package/lib/{history → sessions}/bridge.js +124 -36
- package/lib/sessions/history.js +323 -0
- package/lib/{history → sessions}/tombstone.js +1 -1
- package/lib/{history → sessions}/workspace.js +151 -50
- package/lib/skills/core.js +74 -39
- package/lib/skills/readonly-discovery.js +4 -1
- package/lib/skills/service.js +98 -13
- package/lib/subagents/service.js +199 -55
- package/lib/tools/deps.js +8 -0
- package/lib/tools/mcp.js +110 -0
- package/lib/tools/memory.js +87 -0
- package/lib/tools/prompt.js +70 -0
- package/lib/tools/skills.js +139 -0
- package/lib/tools/subagent.js +40 -0
- package/package.json +13 -10
- package/lib/agents-md/preset-id.js +0 -49
- package/lib/rules/service.js +0 -3078
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { CapabilityRefusalError,
|
|
2
|
+
import { CapabilityRefusalError, createSessionsBridge, requiredSessionCapabilities } from "./bridge.js";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { hubPath } from "../hub.js";
|
|
5
5
|
import { sessionDir } from "@deepseek-ai/dsh-spill-local";
|
|
6
6
|
import { trackTombstone } from "./tombstone.js";
|
|
7
|
+
import { noteRuntime } from "../compat/runtime-notes.js";
|
|
8
|
+
/**
|
|
9
|
+
* A3-1:宿主补上了原生删除入口时上报一次(**不切换**,理由见 deleteSessionCore)。
|
|
10
|
+
*
|
|
11
|
+
* 上报本身就够用:它说明「本插件适配的那个缺口已经被官方补上了」,人工核对过原生语义
|
|
12
|
+
* (级联 / spill / 记账 / 缓存行是否都覆盖)之后才谈切换。
|
|
13
|
+
*
|
|
14
|
+
* @param entry - 实际看到的是哪个原生入口(单删 / 批删),写进详情便于判断。
|
|
15
|
+
*/
|
|
16
|
+
function reportNativeDeleteAvailable(entry) {
|
|
17
|
+
noteRuntime({
|
|
18
|
+
id: "workspace.delete-native",
|
|
19
|
+
label: "宿主原生删除入口",
|
|
20
|
+
kind: "delete",
|
|
21
|
+
fallback: "inform-only",
|
|
22
|
+
detail: `宿主已提供原生 ${entry}:本插件仍走自有完整序列(级联子会话 + spill + 记账 + 缓存行),尚未切换到原生入口。`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
7
25
|
//#region lib/types/index.js
|
|
8
26
|
/**
|
|
9
27
|
* 独立归档门面:消费当前宿主服务,不注册或替换 workspaceRegistry。
|
|
@@ -13,7 +31,7 @@ import { trackTombstone } from "./tombstone.js";
|
|
|
13
31
|
* ——全量 grep `archivedSessionDetails` / `deleteArchivedSessions` / `unarchiveSessions`
|
|
14
32
|
* 均不存在,`@michengai/dsh-archive-manager` 也未安装。因此下面所有
|
|
15
33
|
* `typeof this.registry.X === "function"` 的委托分支在当前环境**都是死代码**,
|
|
16
|
-
* 实际执行路径永远是
|
|
34
|
+
* 实际执行路径永远是 sessions/bridge.ts 的私有适配(依赖官方声明为 private 的方法,
|
|
17
35
|
* 并直接改 registry.headers / sessionPaths / invalidSessionPaths)。
|
|
18
36
|
* 所以「不再替换服务、不再与外部插件争注册」成立,但**不能由此推出耦合下降或零侵入**。
|
|
19
37
|
* 另:改为门面后新增的墓碑 indexHeader 守卫只覆盖本门面的调用;宿主自身的
|
|
@@ -128,6 +146,7 @@ const deletedSchema = {
|
|
|
128
146
|
},
|
|
129
147
|
};
|
|
130
148
|
const archivedBatchTargetSchema = {
|
|
149
|
+
// 校验通过后原样返回记录,调用方按 scope 判别式收窄(宿主返回值校验器,故返回 any)。
|
|
131
150
|
parse(value) {
|
|
132
151
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
133
152
|
throw new TypeError("target must be an object");
|
|
@@ -229,7 +248,14 @@ function defaultWorkspaceSnapshotFile() {
|
|
|
229
248
|
/**
|
|
230
249
|
* 路径比较键:工作区的唯一性在宿主侧是 realpath 字符串相等(dsh-workspace
|
|
231
250
|
* `realpathNormalize`),Windows 盘符大小写与分隔符拼写却可能不同,因此比较前
|
|
232
|
-
*
|
|
251
|
+
* 先归一化。
|
|
252
|
+
*
|
|
253
|
+
* ⚠ 它不是"只用于分组":它决定**哪些会话被挂回哪条登记** —— `registerWorkspace` 会调
|
|
254
|
+
* `attachKnownSessions`,后者按这个键挑选会话并写宿主记账(`sessionIds`);
|
|
255
|
+
* `ensureWorkspaceAccounting` 也拿它当去重键后 `host.create` + 挂回。
|
|
256
|
+
* 而且它比宿主更宽:宿主 `realpathNormalize` 只是 `fs.realpath`(**不做大小写归一**),
|
|
257
|
+
* 本函数在 Windows 上 `toLowerCase()` ⇒ `C:\Proj` 与 `c:\proj` 在插件里同组、在宿主里
|
|
258
|
+
* 可以是两条登记。挂回前应以宿主 `resolveByPath` 实际返回的 id 复核,而不是按归一字符串匹配。
|
|
233
259
|
* @param {string} path - 原始路径。
|
|
234
260
|
* @returns {string} 归一化后的比较键(空路径返回空串)。
|
|
235
261
|
*/
|
|
@@ -263,6 +289,16 @@ function workspaceBaseName(path) {
|
|
|
263
289
|
// Standalone facade over the existing registry. Never instantiate/register a
|
|
264
290
|
// second WorkspaceRegistry, and never open another copy of its storage domain.
|
|
265
291
|
var ArchiveWorkspaceRegistry = class {
|
|
292
|
+
/** 宿主注入的上下文(服务读取按动态面处理,见 `HostContext`)。 */
|
|
293
|
+
ctx;
|
|
294
|
+
/** 宿主注册表实例;构造期由入参收窄,随后被 bridge 收窄后的实例覆盖。 */
|
|
295
|
+
registry;
|
|
296
|
+
/** sessions/bridge.ts 的适配句柄(能力门禁 + 私有接口包装)。 */
|
|
297
|
+
bridge;
|
|
298
|
+
/** 账本写入串行链(保证读-改-写不互相覆盖)。 */
|
|
299
|
+
ledgerTail;
|
|
300
|
+
/** 删除中的会话 id 集合(防 SUBAGENT 级联自环),首次用到才建。 */
|
|
301
|
+
deleting;
|
|
266
302
|
/** 本进程内已物理删除的会话;阻止父类把 stale list() 重新编入索引。 */
|
|
267
303
|
deletedSessionIds = /* @__PURE__ */ new Set();
|
|
268
304
|
/** 墓碑插入顺序,用于在上限处淘汰最旧项。 */
|
|
@@ -272,8 +308,8 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
272
308
|
/** 被删生命周期的日志身份(createdAt/cwd):冷复用探针区分“同 id 新会话”与 stale list() 的依据。 */
|
|
273
309
|
deletedIdentities = /* @__PURE__ */ new Map();
|
|
274
310
|
/** 会话 → 归档时刻(epoch ms);保留期自动删除的基线。 */
|
|
275
|
-
archivedAtMap =
|
|
276
|
-
archivedAtFile
|
|
311
|
+
archivedAtMap = new Map();
|
|
312
|
+
archivedAtFile;
|
|
277
313
|
archivedAtLoaded = false;
|
|
278
314
|
/**
|
|
279
315
|
* 「见过的登记」快照:归一化路径 → { path, title, id, at }。
|
|
@@ -282,8 +318,8 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
282
318
|
* 使按目录重建的分组仍能显示用户命名,并能区分「曾经登记过、现在被移除」
|
|
283
319
|
* 与「从未登记过的目录」。只读辅助信息,不参与任何归属判定或写入。
|
|
284
320
|
*/
|
|
285
|
-
workspaceSnapshotMap =
|
|
286
|
-
workspaceSnapshotFile
|
|
321
|
+
workspaceSnapshotMap = new Map();
|
|
322
|
+
workspaceSnapshotFile;
|
|
287
323
|
workspaceSnapshotLoaded = false;
|
|
288
324
|
/** 快照上限:足够覆盖常见工作区数,又避免长驻进程无限增长。 */
|
|
289
325
|
workspaceSnapshotLimit = 200;
|
|
@@ -293,7 +329,11 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
293
329
|
*/
|
|
294
330
|
ready = Promise.resolve();
|
|
295
331
|
constructor(ctx, registry, config = {}) {
|
|
332
|
+
// 注入的 `sessionPersistence` 不在官方 `Context` 声明里(index.ts 的 inject 列表里有它),
|
|
333
|
+
// 因此这里按本文件的使用面收窄(其余成员仍走官方 `Context`)。
|
|
296
334
|
this.ctx = ctx;
|
|
335
|
+
// 入参按「官方声明可见的公开面」声明(四个索引 Map 在官方 `WorkspaceRegistry` 里是 private,
|
|
336
|
+
// 运行时存在且由 bridge 的形状检查兜底),因此此处收窄为本文件实际使用的注册表面。
|
|
297
337
|
this.registry = registry;
|
|
298
338
|
this.archivedAtFile = config.archivedAtFile || defaultArchivedAtFile();
|
|
299
339
|
this.workspaceSnapshotFile = config.workspaceSnapshotFile || defaultWorkspaceSnapshotFile();
|
|
@@ -301,7 +341,8 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
301
341
|
this.ready = Promise.resolve(config.ready).then(() => undefined, () => undefined);
|
|
302
342
|
}
|
|
303
343
|
this.ledgerTail = Promise.resolve();
|
|
304
|
-
this.bridge =
|
|
344
|
+
this.bridge = createSessionsBridge(ctx, registry, (id, at) => this.recordArchive(id, at));
|
|
345
|
+
// bridge 收窄后的注册表同样只声明运行时形状,这里还原为本文件的注册表面。
|
|
305
346
|
this.registry = this.bridge.registry;
|
|
306
347
|
this.bridge.observe();
|
|
307
348
|
}
|
|
@@ -366,6 +407,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
366
407
|
this.archivedAtLoaded = true;
|
|
367
408
|
try {
|
|
368
409
|
const raw = await readFile(this.archivedAtFile, "utf8");
|
|
410
|
+
// 账本文件由本文件写出(id → epoch ms);非有限值在下面逐个跳过。
|
|
369
411
|
const obj = JSON.parse(raw);
|
|
370
412
|
if (obj && typeof obj === "object") {
|
|
371
413
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -514,6 +556,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
514
556
|
cwd = header && typeof header.cwd === "string" && header.cwd ? header.cwd : undefined;
|
|
515
557
|
}
|
|
516
558
|
catch (error) {
|
|
559
|
+
// 宿主/文件系统抛出的错误按动态形状读 message(strict 下 catch 变量为 unknown)。
|
|
517
560
|
result.skipped.push({ sessionId, reason: `读不到会话头部:${String(error?.message ?? error)}` });
|
|
518
561
|
continue;
|
|
519
562
|
}
|
|
@@ -541,6 +584,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
541
584
|
result.registered.push(record);
|
|
542
585
|
}
|
|
543
586
|
catch (error) {
|
|
587
|
+
// 宿主 realpath 拒绝的原始错误按动态形状读 message。
|
|
544
588
|
entry.failure = `无法解析工作区目录「${cwd}」:${String(error?.message ?? error)}`;
|
|
545
589
|
}
|
|
546
590
|
}
|
|
@@ -560,6 +604,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
560
604
|
result.attached.push(sessionId);
|
|
561
605
|
}
|
|
562
606
|
catch (error) {
|
|
607
|
+
// 宿主 attachSession 拒绝原因按动态形状读 message。
|
|
563
608
|
result.skipped.push({ sessionId, reason: String(error?.message ?? error) });
|
|
564
609
|
}
|
|
565
610
|
}
|
|
@@ -596,6 +641,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
596
641
|
const created = entity === undefined;
|
|
597
642
|
if (created)
|
|
598
643
|
entity = await host.create(target, typeof title === "string" && title.trim() ? title.trim() : undefined);
|
|
644
|
+
// 解析不到即走上面的 create 分支,此后 entity 必定已定义(类型层无法表达该不变式)。
|
|
599
645
|
const record = {
|
|
600
646
|
id: String(entity.id),
|
|
601
647
|
path: String(entity.path ?? target),
|
|
@@ -633,6 +679,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
633
679
|
attached.push(sessionId);
|
|
634
680
|
}
|
|
635
681
|
catch (error) {
|
|
682
|
+
// 宿主 attachSession 拒绝原因按动态形状读 message。
|
|
636
683
|
attachSkipped.push({ sessionId, reason: String(error?.message ?? error) });
|
|
637
684
|
}
|
|
638
685
|
}
|
|
@@ -894,35 +941,22 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
894
941
|
* 后续目标并把成功、并发消失和失败分别返回给客户端。
|
|
895
942
|
*/
|
|
896
943
|
async deleteArchivedSessions(target) {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
return result;
|
|
901
|
-
}
|
|
944
|
+
// 与单删同一条纪律(A3-1):宿主补上原生批量入口也**先不切换** —— 只上报。
|
|
945
|
+
if (typeof this.registry.deleteArchivedSessions === "function")
|
|
946
|
+
reportNativeDeleteAvailable("deleteArchivedSessions");
|
|
902
947
|
// 适配路径(无宿主原生批量入口):在任何会话被删除之前确认整条链路可用。
|
|
903
948
|
this.bridge.checkWorkspace("delete");
|
|
904
|
-
if (typeof this.registry.deleteSession === "function") {
|
|
905
|
-
const requestedSessionIds = this.archivedSessionIdsForTarget(target);
|
|
906
|
-
const result = { requestedSessionIds, deletedSessionIds: [], skippedSessionIds: [], failures: [] };
|
|
907
|
-
for (const id of requestedSessionIds) {
|
|
908
|
-
try {
|
|
909
|
-
await this.deleteSession(id);
|
|
910
|
-
result.deletedSessionIds.push(id);
|
|
911
|
-
}
|
|
912
|
-
catch (error) {
|
|
913
|
-
result.failures.push({ sessionId: id, message: String(error) });
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
return result;
|
|
917
|
-
}
|
|
918
949
|
return this.enqueueOperation(async () => {
|
|
919
950
|
const requestedSessionIds = this.archivedSessionIdsForTarget(target);
|
|
951
|
+
// 批首取一次权威快照:批内每一条删除都要一次全库 `listStoredHeaders()`(含逐会话
|
|
952
|
+
// 读首行),这批复用同一份。取不到就退回逐条取 —— 快照失败不该废掉整批。
|
|
953
|
+
const storedIndex = await this.storedHeaderIndex().catch(() => undefined);
|
|
920
954
|
const deletedSessionIds = [];
|
|
921
955
|
const skippedSessionIds = [];
|
|
922
956
|
const failures = [];
|
|
923
957
|
for (const sessionId of requestedSessionIds) {
|
|
924
958
|
try {
|
|
925
|
-
await this.deleteSessionCore(sessionId);
|
|
959
|
+
await this.deleteSessionCore(sessionId, storedIndex);
|
|
926
960
|
deletedSessionIds.push(sessionId);
|
|
927
961
|
}
|
|
928
962
|
catch (error) {
|
|
@@ -963,6 +997,11 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
963
997
|
// 等待删除期间可能已进入的写回观察到墓碑并完成补删,再允许
|
|
964
998
|
// 同 id 的未来新生命周期写入缓存。
|
|
965
999
|
await projCache.whenIdle?.();
|
|
1000
|
+
// 预留位:宿主**没有** `clearTombstone` 入口(全官方树 grep 零命中),
|
|
1001
|
+
// 所以这里命中的是本插件 `bridge` guard 的墓碑(blocked.delete)——
|
|
1002
|
+
// "已删会话会不会被误复活" 100% 取决于插件自己的两份内存墓碑
|
|
1003
|
+
//(workspace 侧 FIFO-4096 + guard.blocked 无上限),插件重载即全清。
|
|
1004
|
+
// 将来宿主补了 delete+whenIdle 时 bridge 会提前 return,这行才退化成对宿主对象的空转。
|
|
966
1005
|
projCache.clearTombstone?.(sessionId);
|
|
967
1006
|
}
|
|
968
1007
|
await this.cleanSpill(sessionId);
|
|
@@ -1010,26 +1049,30 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1010
1049
|
* @throws {@link ArchiveUnknownSessionError} 会话未知时抛出。
|
|
1011
1050
|
*/
|
|
1012
1051
|
async deleteSession(sessionId) {
|
|
1013
|
-
if (typeof this.registry.deleteSession === "function")
|
|
1014
|
-
|
|
1015
|
-
await this.reconcileArchiveLedger();
|
|
1016
|
-
return result;
|
|
1017
|
-
}
|
|
1052
|
+
if (typeof this.registry.deleteSession === "function")
|
|
1053
|
+
reportNativeDeleteAvailable("deleteSession");
|
|
1018
1054
|
return this.enqueueOperation(() => this.deleteSessionCore(sessionId));
|
|
1019
1055
|
}
|
|
1020
|
-
/** 串行化后的删除主体(级联路径复用:它已持有操作链,绝不能再入队)。
|
|
1021
|
-
|
|
1056
|
+
/** 串行化后的删除主体(级联路径复用:它已持有操作链,绝不能再入队)。
|
|
1057
|
+
* @param storedIndex - 可选的批首 `id → header` 快照,见 {@link storedHeaderIndex}。 */
|
|
1058
|
+
async deleteSessionCore(sessionId, storedIndex) {
|
|
1059
|
+
// A3-1(第 3 版改向):宿主补上原生 `deleteSession` 时**先不切换** —— 原生分支跳过本插件
|
|
1060
|
+
// 的级联删子会话 / spill 清理 / 记账清理,等于行为静默缩水;而"切过去的代码"今天不可达,
|
|
1061
|
+
// 写成即等于它会在官方补上入口的那次升级上首次运行,且无从事前验证。保持自有完整序列
|
|
1062
|
+
//(=今天的能力),只上报一次「宿主已提供原生入口」,切换留到人工核对过原生语义之后。
|
|
1022
1063
|
if (typeof this.registry.deleteSession === "function")
|
|
1023
|
-
|
|
1064
|
+
reportNativeDeleteAvailable("deleteSession");
|
|
1024
1065
|
if (this.deleting?.has(sessionId))
|
|
1025
1066
|
throw new Error(`cyclic subagent lineage at "${sessionId}"`);
|
|
1026
1067
|
this.deleting ??= new Set();
|
|
1027
1068
|
this.deleting.add(sessionId);
|
|
1028
1069
|
try {
|
|
1029
|
-
return await this.deleteLocalSession(sessionId);
|
|
1070
|
+
return await this.deleteLocalSession(sessionId, storedIndex);
|
|
1030
1071
|
}
|
|
1031
1072
|
catch (error) {
|
|
1032
1073
|
// Failed deletion must not permanently suppress a still-live session.
|
|
1074
|
+
// (`clearTombstone` 是**预留位**:宿主无此入口,实际命中的是本插件 bridge guard
|
|
1075
|
+
// 的墓碑 —— 说明见 `cleanupUnknownArchivedSession` 里那一处。)
|
|
1033
1076
|
this.getProjectionCache()?.clearTombstone?.(sessionId);
|
|
1034
1077
|
throw error;
|
|
1035
1078
|
}
|
|
@@ -1037,8 +1080,8 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1037
1080
|
this.deleting.delete(sessionId);
|
|
1038
1081
|
}
|
|
1039
1082
|
}
|
|
1040
|
-
async deleteLocalSession(sessionId) {
|
|
1041
|
-
if (!(await this.sessionKnown(sessionId)))
|
|
1083
|
+
async deleteLocalSession(sessionId, storedIndex) {
|
|
1084
|
+
if (!(await this.sessionKnown(sessionId, storedIndex)))
|
|
1042
1085
|
throw new ArchiveUnknownSessionError(sessionId);
|
|
1043
1086
|
const sessions = this.ctx.get("sessions");
|
|
1044
1087
|
// 快速失败:删掉的顺序(flush → detach → 清缓存 → 删目录 → 清记账)里,
|
|
@@ -1052,6 +1095,16 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1052
1095
|
const sessionRefusals = this.bridge.refusalsFor("delete", requiredSessionCapabilities(live !== void 0));
|
|
1053
1096
|
if (sessionRefusals.length > 0)
|
|
1054
1097
|
throw new CapabilityRefusalError("delete", sessionRefusals);
|
|
1098
|
+
// A3-2:转录清理(`removeTranscriptDirectory`)还要 `readSessionHeader` 与
|
|
1099
|
+
// `persistence.locate()`,而它们排在「装屏障 → flush → detach → 清缓存行」**之后**。
|
|
1100
|
+
// 缺任何一个,失败点都落在中段,留下"转录还在、记账已清"的半删态 —— 与上面
|
|
1101
|
+
// `sessions.get` 同一条纪律:破坏性步骤之前先把实现检查完。
|
|
1102
|
+
const headerRefusals = this.bridge.refusalsFor("delete", ["workspace.read-header"]);
|
|
1103
|
+
if (headerRefusals.length > 0)
|
|
1104
|
+
throw new CapabilityRefusalError("delete", headerRefusals);
|
|
1105
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
1106
|
+
if (persistence === void 0 || typeof persistence.locate !== "function")
|
|
1107
|
+
throw new Error("宿主持久化后端未暴露 locate()(官方接口已变动);操作在改动任何数据前停止,请更新本插件");
|
|
1055
1108
|
// 先记录被删生命周期的日志身份:目录删除后头部不可再读,
|
|
1056
1109
|
// 冷复用探针(sessionKnown 墓碑分支)靠它区分同 id 的新生命周期。
|
|
1057
1110
|
const deletedHeader = this.headers.get(sessionId) ?? live?.header;
|
|
@@ -1061,8 +1114,13 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1061
1114
|
// 持久化屏障先行:不能有未落盘的转录写入与目录删除竞争
|
|
1062
1115
|
//(持久化后端按批关闭句柄,flush 过的会话不再持有打开的文件)。
|
|
1063
1116
|
await sessions.flush(live);
|
|
1064
|
-
// 从存储分离;`session/disposed`
|
|
1065
|
-
// `
|
|
1117
|
+
// 从存储分离;`session/disposed` 同步触发(payload 是 **Session 对象**),
|
|
1118
|
+
// 由官方 `dsh-api-session-controller` 降级成 `api-session/removed`(payload 已降为
|
|
1119
|
+
// **SessionId 字符串**)再转发到浏览器,并启动投影缓存的最终写后落盘。
|
|
1120
|
+
// 名字别写成 `host/session-removed` —— 那个帧名在整个已安装树里不存在
|
|
1121
|
+
//(`host/…` 是 cordis 服务/槽位名的前缀,不是线上帧命名空间)。
|
|
1122
|
+
// 注意 `api-session/removed` 也是"发布回滚"与"作用域剪枝"发出的同一事件
|
|
1123
|
+
//(官方 `dsh-session/lib/types/index.d.ts`),所以收到它**不能**等同于"用户删了会话"。
|
|
1066
1124
|
const entry = sessions.liveEntryFor(live);
|
|
1067
1125
|
sessions.detachEntered(entry);
|
|
1068
1126
|
}
|
|
@@ -1074,7 +1132,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1074
1132
|
await projCache?.whenIdle?.();
|
|
1075
1133
|
if (projCache !== void 0)
|
|
1076
1134
|
await projCache.delete(sessionId);
|
|
1077
|
-
await this.deleteDescendants(sessionId);
|
|
1135
|
+
await this.deleteDescendants(sessionId, storedIndex);
|
|
1078
1136
|
await this.cleanSpill(sessionId);
|
|
1079
1137
|
await this.removeTranscriptDirectory(sessionId);
|
|
1080
1138
|
// 只有物理工件删除成功后才能提交记账清理;否则批量目标会因归档标记
|
|
@@ -1116,6 +1174,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1116
1174
|
this.deletedSessionOrder.splice(idx, 1);
|
|
1117
1175
|
// workspace 与 projection-cache 共同描述同一删除生命周期;新生命周期
|
|
1118
1176
|
// 被接纳时必须同步撤销两处墓碑,否则缓存写入仍会永久被拦截。
|
|
1177
|
+
// 右侧那处同样是**预留位**(宿主无 `clearTombstone`,命中的是 bridge guard 的墓碑)。
|
|
1119
1178
|
this.getProjectionCache()?.clearTombstone?.(sessionId);
|
|
1120
1179
|
}
|
|
1121
1180
|
forgetIndexedSession(sessionId) {
|
|
@@ -1129,7 +1188,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1129
1188
|
* 已删除会话对归档/删除入口都视为未知。实时复用同一 id 时撤墓碑,
|
|
1130
1189
|
* 避免挡住新会话。
|
|
1131
1190
|
*/
|
|
1132
|
-
async sessionKnown(id) {
|
|
1191
|
+
async sessionKnown(id, storedIndex) {
|
|
1133
1192
|
if (this.ctx.get("sessions")?.get(id) !== void 0) {
|
|
1134
1193
|
this.clearTombstone(id);
|
|
1135
1194
|
return true;
|
|
@@ -1137,7 +1196,10 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1137
1196
|
if (this.deletedSessionIds.has(id))
|
|
1138
1197
|
return this.coldReuseKnown(id);
|
|
1139
1198
|
// Refresh from authoritative persistence, not the host's stale header cache.
|
|
1140
|
-
|
|
1199
|
+
// 批量删除会传入**批首快照**(同一批里 k 次全库 list() 是纯浪费);不传就是逐条取新鲜数据。
|
|
1200
|
+
const header = storedIndex !== void 0
|
|
1201
|
+
? storedIndex.get(id)
|
|
1202
|
+
: (await this.listStoredHeaders()).find((item) => item.id === id);
|
|
1141
1203
|
if (!header)
|
|
1142
1204
|
return false;
|
|
1143
1205
|
await this.indexHeader(header);
|
|
@@ -1186,6 +1248,19 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1186
1248
|
async listStoredHeaders() {
|
|
1187
1249
|
return (await this.ctx.sessionPersistence.list()).map((item) => item.header ?? item);
|
|
1188
1250
|
}
|
|
1251
|
+
/**
|
|
1252
|
+
* `id → header` 索引,供**一次批量操作内部**复用:`listStoredHeaders()` 每次都是
|
|
1253
|
+
* 全库遍历 + 逐会话读首行,批量里逐条调用就是 O(k·n)。
|
|
1254
|
+
*
|
|
1255
|
+
* 只在批内有效 —— 它不随删除自动剔除,也不代表"此刻磁盘上的最新";需要后者做判据的
|
|
1256
|
+
* 地方(`coldReuseKnown` 的同 id 新生命周期比对)仍然自己取新鲜数据。
|
|
1257
|
+
*/
|
|
1258
|
+
async storedHeaderIndex() {
|
|
1259
|
+
const index = new Map();
|
|
1260
|
+
for (const header of await this.listStoredHeaders())
|
|
1261
|
+
index.set(header.id, header);
|
|
1262
|
+
return index;
|
|
1263
|
+
}
|
|
1189
1264
|
async indexHeaders(items) {
|
|
1190
1265
|
for (const item of items)
|
|
1191
1266
|
await this.indexHeader(item.header ?? item);
|
|
@@ -1236,6 +1311,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1236
1311
|
stat = await lstat(path);
|
|
1237
1312
|
}
|
|
1238
1313
|
catch (error) {
|
|
1314
|
+
// Node 文件系统错误的 code 按动态形状读取(此处只关心 ENOENT)。
|
|
1239
1315
|
if (error?.code === "ENOENT")
|
|
1240
1316
|
continue; // Already removed on an earlier attempt.
|
|
1241
1317
|
throw error;
|
|
@@ -1277,15 +1353,37 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1277
1353
|
sessionIds: current.sessionIds.filter((id) => id !== sessionId),
|
|
1278
1354
|
updatedAt: /* @__PURE__ */ new Date().toISOString(),
|
|
1279
1355
|
}));
|
|
1356
|
+
// 实体快照的形状由宿主 `WorkspaceEntity` 决定(`record` 是私有字段,运行时存在)。
|
|
1280
1357
|
const entity = this.entities.get(workspaceId);
|
|
1281
|
-
if (entity
|
|
1358
|
+
if (entity === void 0)
|
|
1359
|
+
continue;
|
|
1360
|
+
// A3-3:官方把 `record` 改名或改成只读访问器时,这一行会**静默无效** —— 界面读到旧
|
|
1361
|
+
// 快照(会话计数不动),看起来却"什么都没发生"。写完立刻读回,没写进去就上报;
|
|
1362
|
+
// 仍然照写(不因为探测而少做一次更新)。
|
|
1363
|
+
let applied = false;
|
|
1364
|
+
try {
|
|
1282
1365
|
entity.record = next;
|
|
1366
|
+
applied = entity.record === next;
|
|
1367
|
+
}
|
|
1368
|
+
catch {
|
|
1369
|
+
applied = false;
|
|
1370
|
+
}
|
|
1371
|
+
if (!applied) {
|
|
1372
|
+
noteRuntime({
|
|
1373
|
+
id: "workspace.entity-record",
|
|
1374
|
+
label: "工作区实体快照更新",
|
|
1375
|
+
kind: "write",
|
|
1376
|
+
fallback: "inform-only",
|
|
1377
|
+
detail: "工作区实体的 record 字段写不进去(宿主 WorkspaceEntity 形状可能已变):实体快照可能停留在旧值,界面上的会话计数可能不刷新。",
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1283
1380
|
}
|
|
1284
1381
|
}
|
|
1285
1382
|
/** 尽力而为的级联删除:删除 `sessionId` 的 SUBAGENT 子会话。
|
|
1286
1383
|
* 仅头部标记 `origin: "subagent"` 的会话参与:单凭 `parentSession` 有歧义
|
|
1287
|
-
*(fork 分支也携带它),而 fork 分支是独立的用户会话,绝不能被级联删除。
|
|
1288
|
-
|
|
1384
|
+
*(fork 分支也携带它),而 fork 分支是独立的用户会话,绝不能被级联删除。
|
|
1385
|
+
* @param storedIndex - 可选的批首 `id → header` 快照,见 {@link storedHeaderIndex}。 */
|
|
1386
|
+
async deleteDescendants(sessionId, storedIndex) {
|
|
1289
1387
|
try {
|
|
1290
1388
|
const descendants = [];
|
|
1291
1389
|
const sessions = this.ctx.get("sessions");
|
|
@@ -1295,7 +1393,10 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1295
1393
|
session.header.origin === "subagent")
|
|
1296
1394
|
descendants.push(session.id);
|
|
1297
1395
|
}
|
|
1298
|
-
|
|
1396
|
+
const headers = storedIndex !== void 0
|
|
1397
|
+
? [...storedIndex.values()]
|
|
1398
|
+
: await this.listStoredHeaders();
|
|
1399
|
+
for (const header of headers) {
|
|
1299
1400
|
if (header.parentSession === sessionId &&
|
|
1300
1401
|
header.origin === "subagent" &&
|
|
1301
1402
|
!descendants.includes(header.id))
|
|
@@ -1303,9 +1404,9 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1303
1404
|
}
|
|
1304
1405
|
for (const childId of descendants) {
|
|
1305
1406
|
try {
|
|
1306
|
-
if (!(await this.sessionKnown(childId)))
|
|
1407
|
+
if (!(await this.sessionKnown(childId, storedIndex)))
|
|
1307
1408
|
continue;
|
|
1308
|
-
await this.deleteSessionCore(childId);
|
|
1409
|
+
await this.deleteSessionCore(childId, storedIndex);
|
|
1309
1410
|
}
|
|
1310
1411
|
catch (error) {
|
|
1311
1412
|
if (error instanceof ArchiveUnknownSessionError)
|