dsh-plugin-tool-management 0.9.1 → 0.11.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +119 -1
  2. package/README.md +227 -201
  3. package/README_EN.md +227 -199
  4. package/docs/images/1-EN.png +0 -0
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2-EN.png +0 -0
  7. package/docs/images/2.png +0 -0
  8. package/docs/images/3-EN.png +0 -0
  9. package/docs/images/3.png +0 -0
  10. package/docs/images/4-EN.png +0 -0
  11. package/docs/images/4.png +0 -0
  12. package/docs/images/5-EN.png +0 -0
  13. package/docs/images/5.png +0 -0
  14. package/docs/images/6-EN.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/images/7-EN.png +0 -0
  17. package/docs/images/7.png +0 -0
  18. package/docs/images/8-EN.png +0 -0
  19. package/docs/images/8.png +0 -0
  20. package/docs/update.md +144 -12
  21. package/lib/client.js +8437 -5929
  22. package/lib/compat/preset-reach.js +1 -10
  23. package/lib/compat/probe.js +158 -25
  24. package/lib/context-inject.js +396 -53
  25. package/lib/host-names.js +12 -0
  26. package/lib/http-fence.js +35 -15
  27. package/lib/hub.js +31 -3
  28. package/lib/imports/parsers.js +15 -9
  29. package/lib/imports/upload.js +43 -4
  30. package/lib/index.js +940 -3765
  31. package/lib/mcp/loader-token.js +238 -0
  32. package/lib/mcp/manager.js +1681 -0
  33. package/lib/mcp/override-blocks.js +20 -9
  34. package/lib/mcp/patch-yaml.js +351 -0
  35. package/lib/mcp/secret-guard.js +145 -0
  36. package/lib/mcp/state-section.js +64 -21
  37. package/lib/{rules → memories}/archive-engine.js +65 -10
  38. package/lib/{rules → memories}/archive.js +6 -7
  39. package/lib/memories/constants.js +128 -0
  40. package/lib/memories/index-io.js +330 -0
  41. package/lib/memories/projection.js +280 -0
  42. package/lib/memories/service.js +686 -0
  43. package/lib/memories/snapshot.js +672 -0
  44. package/lib/ops/candidates.js +64 -0
  45. package/lib/ops/compat.js +136 -0
  46. package/lib/ops/ctx.js +9 -0
  47. package/lib/ops/memory.js +678 -0
  48. package/lib/ops/prompts.js +107 -0
  49. package/lib/ops/scene-records.js +460 -0
  50. package/lib/ops/scene-sync.js +17 -0
  51. package/lib/ops/sessions.js +603 -0
  52. package/lib/ops/trash.js +140 -0
  53. package/lib/paths.js +103 -0
  54. package/lib/prompts/preset-id.js +49 -0
  55. package/lib/{agents-md → prompts}/service.js +1 -1
  56. package/lib/request-gate.js +320 -0
  57. package/lib/scene-prompt-sync.js +4 -4
  58. package/lib/scenes/candidates.js +344 -0
  59. package/lib/{history → sessions}/bridge.js +22 -5
  60. package/lib/sessions/history.js +323 -0
  61. package/lib/{history → sessions}/tombstone.js +1 -2
  62. package/lib/{history → sessions}/workspace.js +92 -24
  63. package/lib/skills/catalog.js +6 -9
  64. package/lib/skills/core.js +79 -43
  65. package/lib/skills/readonly-discovery.js +4 -1
  66. package/lib/skills/service.js +98 -13
  67. package/lib/subagents/catalog.js +31 -15
  68. package/lib/subagents/service.js +506 -89
  69. package/lib/subagents/tools.js +29 -4
  70. package/lib/tools/deps.js +8 -0
  71. package/lib/tools/mcp.js +110 -0
  72. package/lib/tools/memory.js +87 -0
  73. package/lib/tools/prompt.js +70 -0
  74. package/lib/tools/skills.js +139 -0
  75. package/lib/tools/subagent.js +40 -0
  76. package/package.json +105 -102
  77. package/lib/agents-md/preset-id.js +0 -49
  78. package/lib/history/projcache.js +0 -335
  79. package/lib/rules/service.js +0 -2971
@@ -1,5 +1,5 @@
1
1
  import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
- import { CapabilityRefusalError, createHistoryBridge, requiredSessionCapabilities } from "./bridge.js";
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";
@@ -13,7 +13,7 @@ import { trackTombstone } from "./tombstone.js";
13
13
  * ——全量 grep `archivedSessionDetails` / `deleteArchivedSessions` / `unarchiveSessions`
14
14
  * 均不存在,`@michengai/dsh-archive-manager` 也未安装。因此下面所有
15
15
  * `typeof this.registry.X === "function"` 的委托分支在当前环境**都是死代码**,
16
- * 实际执行路径永远是 history/bridge.js 的私有适配(依赖官方声明为 private 的方法,
16
+ * 实际执行路径永远是 sessions/bridge.ts 的私有适配(依赖官方声明为 private 的方法,
17
17
  * 并直接改 registry.headers / sessionPaths / invalidSessionPaths)。
18
18
  * 所以「不再替换服务、不再与外部插件争注册」成立,但**不能由此推出耦合下降或零侵入**。
19
19
  * 另:改为门面后新增的墓碑 indexHeader 守卫只覆盖本门面的调用;宿主自身的
@@ -128,6 +128,7 @@ const deletedSchema = {
128
128
  },
129
129
  };
130
130
  const archivedBatchTargetSchema = {
131
+ // 校验通过后原样返回记录,调用方按 scope 判别式收窄(宿主返回值校验器,故返回 any)。
131
132
  parse(value) {
132
133
  if (typeof value !== "object" || value === null || Array.isArray(value))
133
134
  throw new TypeError("target must be an object");
@@ -229,7 +230,14 @@ function defaultWorkspaceSnapshotFile() {
229
230
  /**
230
231
  * 路径比较键:工作区的唯一性在宿主侧是 realpath 字符串相等(dsh-workspace
231
232
  * `realpathNormalize`),Windows 盘符大小写与分隔符拼写却可能不同,因此比较前
232
- * 先归一化。仅用于**分组与快照索引**,不参与任何写入或归属判定。
233
+ * 先归一化。
234
+ *
235
+ * ⚠ 它不是"只用于分组":它决定**哪些会话被挂回哪条登记** —— `registerWorkspace` 会调
236
+ * `attachKnownSessions`,后者按这个键挑选会话并写宿主记账(`sessionIds`);
237
+ * `ensureWorkspaceAccounting` 也拿它当去重键后 `host.create` + 挂回。
238
+ * 而且它比宿主更宽:宿主 `realpathNormalize` 只是 `fs.realpath`(**不做大小写归一**),
239
+ * 本函数在 Windows 上 `toLowerCase()` ⇒ `C:\Proj` 与 `c:\proj` 在插件里同组、在宿主里
240
+ * 可以是两条登记。挂回前应以宿主 `resolveByPath` 实际返回的 id 复核,而不是按归一字符串匹配。
233
241
  * @param {string} path - 原始路径。
234
242
  * @returns {string} 归一化后的比较键(空路径返回空串)。
235
243
  */
@@ -263,6 +271,16 @@ function workspaceBaseName(path) {
263
271
  // Standalone facade over the existing registry. Never instantiate/register a
264
272
  // second WorkspaceRegistry, and never open another copy of its storage domain.
265
273
  var ArchiveWorkspaceRegistry = class {
274
+ /** 宿主注入的上下文(服务读取按动态面处理,见 `HostContext`)。 */
275
+ ctx;
276
+ /** 宿主注册表实例;构造期由入参收窄,随后被 bridge 收窄后的实例覆盖。 */
277
+ registry;
278
+ /** sessions/bridge.ts 的适配句柄(能力门禁 + 私有接口包装)。 */
279
+ bridge;
280
+ /** 账本写入串行链(保证读-改-写不互相覆盖)。 */
281
+ ledgerTail;
282
+ /** 删除中的会话 id 集合(防 SUBAGENT 级联自环),首次用到才建。 */
283
+ deleting;
266
284
  /** 本进程内已物理删除的会话;阻止父类把 stale list() 重新编入索引。 */
267
285
  deletedSessionIds = /* @__PURE__ */ new Set();
268
286
  /** 墓碑插入顺序,用于在上限处淘汰最旧项。 */
@@ -272,8 +290,8 @@ var ArchiveWorkspaceRegistry = class {
272
290
  /** 被删生命周期的日志身份(createdAt/cwd):冷复用探针区分“同 id 新会话”与 stale list() 的依据。 */
273
291
  deletedIdentities = /* @__PURE__ */ new Map();
274
292
  /** 会话 → 归档时刻(epoch ms);保留期自动删除的基线。 */
275
- archivedAtMap = /* @__PURE__ */ new Map();
276
- archivedAtFile = null;
293
+ archivedAtMap = new Map();
294
+ archivedAtFile;
277
295
  archivedAtLoaded = false;
278
296
  /**
279
297
  * 「见过的登记」快照:归一化路径 → { path, title, id, at }。
@@ -282,8 +300,8 @@ var ArchiveWorkspaceRegistry = class {
282
300
  * 使按目录重建的分组仍能显示用户命名,并能区分「曾经登记过、现在被移除」
283
301
  * 与「从未登记过的目录」。只读辅助信息,不参与任何归属判定或写入。
284
302
  */
285
- workspaceSnapshotMap = /* @__PURE__ */ new Map();
286
- workspaceSnapshotFile = null;
303
+ workspaceSnapshotMap = new Map();
304
+ workspaceSnapshotFile;
287
305
  workspaceSnapshotLoaded = false;
288
306
  /** 快照上限:足够覆盖常见工作区数,又避免长驻进程无限增长。 */
289
307
  workspaceSnapshotLimit = 200;
@@ -293,7 +311,11 @@ var ArchiveWorkspaceRegistry = class {
293
311
  */
294
312
  ready = Promise.resolve();
295
313
  constructor(ctx, registry, config = {}) {
314
+ // 注入的 `sessionPersistence` 不在官方 `Context` 声明里(index.ts 的 inject 列表里有它),
315
+ // 因此这里按本文件的使用面收窄(其余成员仍走官方 `Context`)。
296
316
  this.ctx = ctx;
317
+ // 入参按「官方声明可见的公开面」声明(四个索引 Map 在官方 `WorkspaceRegistry` 里是 private,
318
+ // 运行时存在且由 bridge 的形状检查兜底),因此此处收窄为本文件实际使用的注册表面。
297
319
  this.registry = registry;
298
320
  this.archivedAtFile = config.archivedAtFile || defaultArchivedAtFile();
299
321
  this.workspaceSnapshotFile = config.workspaceSnapshotFile || defaultWorkspaceSnapshotFile();
@@ -301,7 +323,8 @@ var ArchiveWorkspaceRegistry = class {
301
323
  this.ready = Promise.resolve(config.ready).then(() => undefined, () => undefined);
302
324
  }
303
325
  this.ledgerTail = Promise.resolve();
304
- this.bridge = createHistoryBridge(ctx, registry, (id, at) => this.recordArchive(id, at));
326
+ this.bridge = createSessionsBridge(ctx, registry, (id, at) => this.recordArchive(id, at));
327
+ // bridge 收窄后的注册表同样只声明运行时形状,这里还原为本文件的注册表面。
305
328
  this.registry = this.bridge.registry;
306
329
  this.bridge.observe();
307
330
  }
@@ -366,6 +389,7 @@ var ArchiveWorkspaceRegistry = class {
366
389
  this.archivedAtLoaded = true;
367
390
  try {
368
391
  const raw = await readFile(this.archivedAtFile, "utf8");
392
+ // 账本文件由本文件写出(id → epoch ms);非有限值在下面逐个跳过。
369
393
  const obj = JSON.parse(raw);
370
394
  if (obj && typeof obj === "object") {
371
395
  for (const [k, v] of Object.entries(obj)) {
@@ -514,6 +538,7 @@ var ArchiveWorkspaceRegistry = class {
514
538
  cwd = header && typeof header.cwd === "string" && header.cwd ? header.cwd : undefined;
515
539
  }
516
540
  catch (error) {
541
+ // 宿主/文件系统抛出的错误按动态形状读 message(strict 下 catch 变量为 unknown)。
517
542
  result.skipped.push({ sessionId, reason: `读不到会话头部:${String(error?.message ?? error)}` });
518
543
  continue;
519
544
  }
@@ -541,6 +566,7 @@ var ArchiveWorkspaceRegistry = class {
541
566
  result.registered.push(record);
542
567
  }
543
568
  catch (error) {
569
+ // 宿主 realpath 拒绝的原始错误按动态形状读 message。
544
570
  entry.failure = `无法解析工作区目录「${cwd}」:${String(error?.message ?? error)}`;
545
571
  }
546
572
  }
@@ -560,6 +586,7 @@ var ArchiveWorkspaceRegistry = class {
560
586
  result.attached.push(sessionId);
561
587
  }
562
588
  catch (error) {
589
+ // 宿主 attachSession 拒绝原因按动态形状读 message。
563
590
  result.skipped.push({ sessionId, reason: String(error?.message ?? error) });
564
591
  }
565
592
  }
@@ -596,6 +623,7 @@ var ArchiveWorkspaceRegistry = class {
596
623
  const created = entity === undefined;
597
624
  if (created)
598
625
  entity = await host.create(target, typeof title === "string" && title.trim() ? title.trim() : undefined);
626
+ // 解析不到即走上面的 create 分支,此后 entity 必定已定义(类型层无法表达该不变式)。
599
627
  const record = {
600
628
  id: String(entity.id),
601
629
  path: String(entity.path ?? target),
@@ -633,6 +661,7 @@ var ArchiveWorkspaceRegistry = class {
633
661
  attached.push(sessionId);
634
662
  }
635
663
  catch (error) {
664
+ // 宿主 attachSession 拒绝原因按动态形状读 message。
636
665
  attachSkipped.push({ sessionId, reason: String(error?.message ?? error) });
637
666
  }
638
667
  }
@@ -917,12 +946,15 @@ var ArchiveWorkspaceRegistry = class {
917
946
  }
918
947
  return this.enqueueOperation(async () => {
919
948
  const requestedSessionIds = this.archivedSessionIdsForTarget(target);
949
+ // 批首取一次权威快照:批内每一条删除都要一次全库 `listStoredHeaders()`(含逐会话
950
+ // 读首行),这批复用同一份。取不到就退回逐条取 —— 快照失败不该废掉整批。
951
+ const storedIndex = await this.storedHeaderIndex().catch(() => undefined);
920
952
  const deletedSessionIds = [];
921
953
  const skippedSessionIds = [];
922
954
  const failures = [];
923
955
  for (const sessionId of requestedSessionIds) {
924
956
  try {
925
- await this.deleteSessionCore(sessionId);
957
+ await this.deleteSessionCore(sessionId, storedIndex);
926
958
  deletedSessionIds.push(sessionId);
927
959
  }
928
960
  catch (error) {
@@ -963,6 +995,11 @@ var ArchiveWorkspaceRegistry = class {
963
995
  // 等待删除期间可能已进入的写回观察到墓碑并完成补删,再允许
964
996
  // 同 id 的未来新生命周期写入缓存。
965
997
  await projCache.whenIdle?.();
998
+ // 预留位:宿主**没有** `clearTombstone` 入口(全官方树 grep 零命中),
999
+ // 所以这里命中的是本插件 `bridge` guard 的墓碑(blocked.delete)——
1000
+ // "已删会话会不会被误复活" 100% 取决于插件自己的两份内存墓碑
1001
+ //(workspace 侧 FIFO-4096 + guard.blocked 无上限),插件重载即全清。
1002
+ // 将来宿主补了 delete+whenIdle 时 bridge 会提前 return,这行才退化成对宿主对象的空转。
966
1003
  projCache.clearTombstone?.(sessionId);
967
1004
  }
968
1005
  await this.cleanSpill(sessionId);
@@ -1017,8 +1054,9 @@ var ArchiveWorkspaceRegistry = class {
1017
1054
  }
1018
1055
  return this.enqueueOperation(() => this.deleteSessionCore(sessionId));
1019
1056
  }
1020
- /** 串行化后的删除主体(级联路径复用:它已持有操作链,绝不能再入队)。 */
1021
- async deleteSessionCore(sessionId) {
1057
+ /** 串行化后的删除主体(级联路径复用:它已持有操作链,绝不能再入队)。
1058
+ * @param storedIndex - 可选的批首 `id → header` 快照,见 {@link storedHeaderIndex}。 */
1059
+ async deleteSessionCore(sessionId, storedIndex) {
1022
1060
  if (typeof this.registry.deleteSession === "function")
1023
1061
  return this.registry.deleteSession(sessionId);
1024
1062
  if (this.deleting?.has(sessionId))
@@ -1026,10 +1064,12 @@ var ArchiveWorkspaceRegistry = class {
1026
1064
  this.deleting ??= new Set();
1027
1065
  this.deleting.add(sessionId);
1028
1066
  try {
1029
- return await this.deleteLocalSession(sessionId);
1067
+ return await this.deleteLocalSession(sessionId, storedIndex);
1030
1068
  }
1031
1069
  catch (error) {
1032
1070
  // Failed deletion must not permanently suppress a still-live session.
1071
+ // (`clearTombstone` 是**预留位**:宿主无此入口,实际命中的是本插件 bridge guard
1072
+ // 的墓碑 —— 说明见 `cleanupUnknownArchivedSession` 里那一处。)
1033
1073
  this.getProjectionCache()?.clearTombstone?.(sessionId);
1034
1074
  throw error;
1035
1075
  }
@@ -1037,8 +1077,8 @@ var ArchiveWorkspaceRegistry = class {
1037
1077
  this.deleting.delete(sessionId);
1038
1078
  }
1039
1079
  }
1040
- async deleteLocalSession(sessionId) {
1041
- if (!(await this.sessionKnown(sessionId)))
1080
+ async deleteLocalSession(sessionId, storedIndex) {
1081
+ if (!(await this.sessionKnown(sessionId, storedIndex)))
1042
1082
  throw new ArchiveUnknownSessionError(sessionId);
1043
1083
  const sessions = this.ctx.get("sessions");
1044
1084
  // 快速失败:删掉的顺序(flush → detach → 清缓存 → 删目录 → 清记账)里,
@@ -1061,8 +1101,13 @@ var ArchiveWorkspaceRegistry = class {
1061
1101
  // 持久化屏障先行:不能有未落盘的转录写入与目录删除竞争
1062
1102
  //(持久化后端按批关闭句柄,flush 过的会话不再持有打开的文件)。
1063
1103
  await sessions.flush(live);
1064
- // 从存储分离;`session/disposed` 同步触发,驱动浏览器端的
1065
- // `host/session-removed` 帧并启动投影缓存的最终写后落盘。
1104
+ // 从存储分离;`session/disposed` 同步触发(payload 是 **Session 对象**),
1105
+ // 由官方 `dsh-api-session-controller` 降级成 `api-session/removed`(payload 已降为
1106
+ // **SessionId 字符串**)再转发到浏览器,并启动投影缓存的最终写后落盘。
1107
+ // 名字别写成 `host/session-removed` —— 那个帧名在整个已安装树里不存在
1108
+ //(`host/…` 是 cordis 服务/槽位名的前缀,不是线上帧命名空间)。
1109
+ // 注意 `api-session/removed` 也是"发布回滚"与"作用域剪枝"发出的同一事件
1110
+ //(官方 `dsh-session/lib/types/index.d.ts`),所以收到它**不能**等同于"用户删了会话"。
1066
1111
  const entry = sessions.liveEntryFor(live);
1067
1112
  sessions.detachEntered(entry);
1068
1113
  }
@@ -1074,7 +1119,7 @@ var ArchiveWorkspaceRegistry = class {
1074
1119
  await projCache?.whenIdle?.();
1075
1120
  if (projCache !== void 0)
1076
1121
  await projCache.delete(sessionId);
1077
- await this.deleteDescendants(sessionId);
1122
+ await this.deleteDescendants(sessionId, storedIndex);
1078
1123
  await this.cleanSpill(sessionId);
1079
1124
  await this.removeTranscriptDirectory(sessionId);
1080
1125
  // 只有物理工件删除成功后才能提交记账清理;否则批量目标会因归档标记
@@ -1116,6 +1161,7 @@ var ArchiveWorkspaceRegistry = class {
1116
1161
  this.deletedSessionOrder.splice(idx, 1);
1117
1162
  // workspace 与 projection-cache 共同描述同一删除生命周期;新生命周期
1118
1163
  // 被接纳时必须同步撤销两处墓碑,否则缓存写入仍会永久被拦截。
1164
+ // 右侧那处同样是**预留位**(宿主无 `clearTombstone`,命中的是 bridge guard 的墓碑)。
1119
1165
  this.getProjectionCache()?.clearTombstone?.(sessionId);
1120
1166
  }
1121
1167
  forgetIndexedSession(sessionId) {
@@ -1129,7 +1175,7 @@ var ArchiveWorkspaceRegistry = class {
1129
1175
  * 已删除会话对归档/删除入口都视为未知。实时复用同一 id 时撤墓碑,
1130
1176
  * 避免挡住新会话。
1131
1177
  */
1132
- async sessionKnown(id) {
1178
+ async sessionKnown(id, storedIndex) {
1133
1179
  if (this.ctx.get("sessions")?.get(id) !== void 0) {
1134
1180
  this.clearTombstone(id);
1135
1181
  return true;
@@ -1137,7 +1183,10 @@ var ArchiveWorkspaceRegistry = class {
1137
1183
  if (this.deletedSessionIds.has(id))
1138
1184
  return this.coldReuseKnown(id);
1139
1185
  // Refresh from authoritative persistence, not the host's stale header cache.
1140
- const header = (await this.listStoredHeaders()).find((item) => item.id === id);
1186
+ // 批量删除会传入**批首快照**(同一批里 k 次全库 list() 是纯浪费);不传就是逐条取新鲜数据。
1187
+ const header = storedIndex !== void 0
1188
+ ? storedIndex.get(id)
1189
+ : (await this.listStoredHeaders()).find((item) => item.id === id);
1141
1190
  if (!header)
1142
1191
  return false;
1143
1192
  await this.indexHeader(header);
@@ -1186,6 +1235,19 @@ var ArchiveWorkspaceRegistry = class {
1186
1235
  async listStoredHeaders() {
1187
1236
  return (await this.ctx.sessionPersistence.list()).map((item) => item.header ?? item);
1188
1237
  }
1238
+ /**
1239
+ * `id → header` 索引,供**一次批量操作内部**复用:`listStoredHeaders()` 每次都是
1240
+ * 全库遍历 + 逐会话读首行,批量里逐条调用就是 O(k·n)。
1241
+ *
1242
+ * 只在批内有效 —— 它不随删除自动剔除,也不代表"此刻磁盘上的最新";需要后者做判据的
1243
+ * 地方(`coldReuseKnown` 的同 id 新生命周期比对)仍然自己取新鲜数据。
1244
+ */
1245
+ async storedHeaderIndex() {
1246
+ const index = new Map();
1247
+ for (const header of await this.listStoredHeaders())
1248
+ index.set(header.id, header);
1249
+ return index;
1250
+ }
1189
1251
  async indexHeaders(items) {
1190
1252
  for (const item of items)
1191
1253
  await this.indexHeader(item.header ?? item);
@@ -1236,6 +1298,7 @@ var ArchiveWorkspaceRegistry = class {
1236
1298
  stat = await lstat(path);
1237
1299
  }
1238
1300
  catch (error) {
1301
+ // Node 文件系统错误的 code 按动态形状读取(此处只关心 ENOENT)。
1239
1302
  if (error?.code === "ENOENT")
1240
1303
  continue; // Already removed on an earlier attempt.
1241
1304
  throw error;
@@ -1277,6 +1340,7 @@ var ArchiveWorkspaceRegistry = class {
1277
1340
  sessionIds: current.sessionIds.filter((id) => id !== sessionId),
1278
1341
  updatedAt: /* @__PURE__ */ new Date().toISOString(),
1279
1342
  }));
1343
+ // 实体快照的形状由宿主 `WorkspaceEntity` 决定(`record` 是私有字段,运行时存在)。
1280
1344
  const entity = this.entities.get(workspaceId);
1281
1345
  if (entity !== void 0)
1282
1346
  entity.record = next;
@@ -1284,8 +1348,9 @@ var ArchiveWorkspaceRegistry = class {
1284
1348
  }
1285
1349
  /** 尽力而为的级联删除:删除 `sessionId` 的 SUBAGENT 子会话。
1286
1350
  * 仅头部标记 `origin: "subagent"` 的会话参与:单凭 `parentSession` 有歧义
1287
- *(fork 分支也携带它),而 fork 分支是独立的用户会话,绝不能被级联删除。 */
1288
- async deleteDescendants(sessionId) {
1351
+ *(fork 分支也携带它),而 fork 分支是独立的用户会话,绝不能被级联删除。
1352
+ * @param storedIndex - 可选的批首 `id → header` 快照,见 {@link storedHeaderIndex}。 */
1353
+ async deleteDescendants(sessionId, storedIndex) {
1289
1354
  try {
1290
1355
  const descendants = [];
1291
1356
  const sessions = this.ctx.get("sessions");
@@ -1295,7 +1360,10 @@ var ArchiveWorkspaceRegistry = class {
1295
1360
  session.header.origin === "subagent")
1296
1361
  descendants.push(session.id);
1297
1362
  }
1298
- for (const header of await this.listStoredHeaders()) {
1363
+ const headers = storedIndex !== void 0
1364
+ ? [...storedIndex.values()]
1365
+ : await this.listStoredHeaders();
1366
+ for (const header of headers) {
1299
1367
  if (header.parentSession === sessionId &&
1300
1368
  header.origin === "subagent" &&
1301
1369
  !descendants.includes(header.id))
@@ -1303,9 +1371,9 @@ var ArchiveWorkspaceRegistry = class {
1303
1371
  }
1304
1372
  for (const childId of descendants) {
1305
1373
  try {
1306
- if (!(await this.sessionKnown(childId)))
1374
+ if (!(await this.sessionKnown(childId, storedIndex)))
1307
1375
  continue;
1308
- await this.deleteSessionCore(childId);
1376
+ await this.deleteSessionCore(childId, storedIndex);
1309
1377
  }
1310
1378
  catch (error) {
1311
1379
  if (error instanceof ArchiveUnknownSessionError)
@@ -11,8 +11,9 @@
11
11
  //
12
12
  // 与官方目录的两处有意差别:
13
13
  // - 只列**当前启用且可被模型调用**的(官方只过滤可调用性,不管用户在插件页关掉了谁)。
14
- // - 结尾多一行:本预设没有官方 `skill` 加载工具(目录与工具是一起挂的),需要正文时得先
15
- // `skill_manager_list` 取源文件路径再读那个文件 —— 不写这句,模型会以为有 `skill` 工具可调。
14
+ // - 「本预设没有官方 `skill` 加载工具,要正文先用 `skill_manager_list` 取源文件路径」那句
15
+ // 2026-09-18 起挪进注入框架的 `how` 行(context-inject.ts 的 DOMAIN_FRAME.skills)——
16
+ // 本段只排版清单,标题与"怎么用"一处一个出处。
16
17
  //
17
18
  // 同步性:`text()` 必须同步返回(注入通道每个 step 同步取文本),而技能清单是异步读盘 →
18
19
  // 与子智能体目录同构的 stale-while-revalidate:`text()` 返回缓存值并在超龄时后台重算,
@@ -72,13 +73,9 @@ export function renderSkillCatalog(data, maxEntries = SKILL_CATALOG_MAX_ENTRIES,
72
73
  const sorted = [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
73
74
  const shown = sorted.slice(0, Math.max(0, maxEntries));
74
75
  const lines = shown.map((row) => (row.description ? '- `' + row.name + '`: ' + row.description : '- `' + row.name + '`'));
75
- const out = [
76
- '## 技能',
77
- '',
78
- '**本预设没有官方 `skill` 加载工具;要技能正文时用 `skill_manager_list` 取源文件路径再读。**',
79
- '',
80
- ...lines,
81
- ];
76
+ // 只有清单:标题、工具名与"只给摘要、读完再用"的纪律由注入通道的框架承担
77
+ // (2026-09-18 起,见 context-inject.ts 的 DOMAIN_FRAME 的 `how` 行)—— 一处内容一个出处。
78
+ const out = [...lines];
82
79
  const hidden = sorted.length - shown.length;
83
80
  if (hidden > 0)
84
81
  out.push('', `(另有 ${hidden} 个未列出,用 \`skill_manager_list\` 查。)`);