dsh-plugin-tool-management 0.11.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.
@@ -1,6 +1,7 @@
1
1
  import { symbols } from "@deepseek-ai/cordis";
2
2
  import { workspaceDomainSpec } from "@deepseek-ai/dsh-workspace";
3
3
  import { assessHost, routeFor, refusalsFor, CapabilityRefusalError } from "../compat/probe.js";
4
+ import { noteRuntime } from "../compat/runtime-notes.js";
4
5
  export { CapabilityRefusalError };
5
6
  // No Service subclass, no ctx.provide(), no second storage-domain owner.
6
7
  // The official API lacks unarchive and a cache deletion barrier, so this file
@@ -36,16 +37,39 @@ const sameLifecycle = (a, b) => a && b && a.createdAt === b.createdAt && (a.cwd
36
37
  * @throws {CapabilityRefusalError} when the cache cannot support a safe delete.
37
38
  */
38
39
  function acquireCacheGuard(cache) {
39
- let guard = cacheGuards.get(cache);
40
- if (guard) {
41
- guard.users += 1;
42
- return guard;
40
+ const existing = cacheGuards.get(cache);
41
+ // 复用前先验「装着的确实是我们的包装器」:release 时被第三方接管过的那条路会留下一个
42
+ // 「在册但没装」的 guard,复用它等于墓碑拦截静默失效(缓存行可能复活)。
43
+ if (existing && existing.users > 0 && existing.installed()) {
44
+ existing.users += 1;
45
+ return existing;
43
46
  }
47
+ if (existing)
48
+ cacheGuards.delete(cache);
44
49
  const missing = ["put", "write", "requireTable"].filter((name) => typeof cache[name] !== "function");
45
50
  if (missing.length > 0) {
46
51
  throw new CapabilityRefusalError("delete", refusalsFor({ findings: [{ id: "projection.write", label: "投影缓存写入路径", kind: "write", owner: "projectionCache", fallback: "disable-destructive", state: "missing-member", detail: `宿主投影缓存缺少 ${missing.join(", ")}`, missing }] }, ["projection.write"]));
47
52
  }
48
- const table = cache.requireTable();
53
+ let table;
54
+ try {
55
+ table = cache.requireTable();
56
+ }
57
+ catch (error) {
58
+ // 安装期就抛:此刻还没动 put/write(不可能有半状态),但必须上报 —— 删除屏障没在岗。
59
+ noteRuntime({
60
+ id: "projection-cache-adapter",
61
+ label: "投影缓存删除屏障",
62
+ kind: "delete",
63
+ fallback: "inform-only",
64
+ detail: `投影缓存适配未完成(requireTable 抛错):${String(error?.message ?? error)};宿主对象未被改动。`,
65
+ });
66
+ throw new CapabilityRefusalError("delete", [{
67
+ id: "projection.table-delete",
68
+ label: "投影缓存行删除",
69
+ detail: `宿主投影缓存的 requireTable() 抛错:${String(error?.message ?? error)}`,
70
+ recovery: "请更新本插件到与本机 DSH 匹配的版本(先运行 node scripts/doctor.mjs 查看差异)。",
71
+ }]);
72
+ }
49
73
  if (typeof table?.delete !== "function") {
50
74
  throw new CapabilityRefusalError("delete", [{
51
75
  id: "projection.table-delete",
@@ -90,29 +114,46 @@ function acquireCacheGuard(cache) {
90
114
  return serial(id, async () => {
91
115
  if (isBlocked(id, expected))
92
116
  return;
93
- await originals.put.call(cache, id, expected, detached);
94
- if (isBlocked(id, expected)) {
95
- const stored = table.get(id);
96
- if (stored && sameLifecycle(stored.identity, expected))
97
- await table.delete(id);
117
+ try {
118
+ await originals.put.call(cache, id, expected, detached);
119
+ if (isBlocked(id, expected)) {
120
+ const stored = table.get(id);
121
+ if (stored && sameLifecycle(stored.identity, expected))
122
+ await table.delete(id);
123
+ }
124
+ }
125
+ catch (error) {
126
+ // 形状变了才会是 TypeError(与本仓库探针同一条口径:受控 Error 不算适配失败)。
127
+ // 撤掉包装、绝不让宿主留在"半个包装器"状态下,并上报 —— 下一次删除会重装。
128
+ if (error instanceof TypeError)
129
+ retire("failed");
130
+ throw error;
98
131
  }
99
132
  });
100
133
  },
101
134
  write(session) {
102
135
  // Track the flush await before put as well as put itself. Otherwise a
103
136
  // delayed write could outlive disposal and bypass the restored method.
137
+ let task;
104
138
  try {
105
- return track(Promise.resolve(originals.write.call(cache, session)));
139
+ task = track(Promise.resolve(originals.write.call(cache, session)));
106
140
  }
107
141
  catch (error) {
142
+ if (error instanceof TypeError)
143
+ retire("failed");
108
144
  return Promise.reject(error);
109
145
  }
146
+ return task.catch((error) => { if (error instanceof TypeError)
147
+ retire("failed"); throw error; });
110
148
  },
111
149
  };
112
150
  cache.put = wrappers.put;
113
151
  cache.write = wrappers.write;
114
- guard = {
152
+ const self = {
115
153
  users: 1,
154
+ // 两个都得是我们的才算"在岗":只装着一个就等于另一半的写入不受墓碑约束(缓存行会复活)。
155
+ // 半个的情形由 acquire 的重建路径处理(覆盖掉那半个外来包装),并在退役时已上报过。
156
+ installed: () => cache.put === wrappers.put && cache.write === wrappers.write,
116
157
  begin(id, header) { blocked.set(id, header ?? null); },
117
158
  clearTombstone(id) { blocked.delete(id); },
118
159
  async whenIdle() { while (pending.size)
@@ -132,26 +173,55 @@ function acquireCacheGuard(cache) {
132
173
  await this.whenIdle();
133
174
  if (this.users > 0)
134
175
  return;
135
- let restored = true;
136
- for (const name of ["put", "write"]) {
137
- // Never overwrite a later third-party wrapper.
138
- if (cache[name] !== wrappers[name]) {
139
- restored = false;
140
- continue;
141
- }
142
- if (descriptors[name])
143
- Object.defineProperty(cache, name, descriptors[name]);
144
- else
145
- delete cache[name];
146
- }
147
- if (restored) {
148
- blocked.clear();
149
- cacheGuards.delete(cache);
150
- }
176
+ const clean = restoreWrappers();
177
+ retire(clean ? "idle" : "taken-over");
151
178
  },
152
179
  };
153
- cacheGuards.set(cache, guard);
154
- return guard;
180
+ let retired = false;
181
+ /** 撤包装:第三方装了的话**不动它**(返回 false 表示没撤干净)。 */
182
+ function restoreWrappers() {
183
+ let clean = true;
184
+ for (const name of ["put", "write"]) {
185
+ // Never overwrite a later third-party wrapper.
186
+ if (cache[name] !== wrappers[name]) {
187
+ clean = false;
188
+ continue;
189
+ }
190
+ if (descriptors[name])
191
+ Object.defineProperty(cache, name, descriptors[name]);
192
+ else
193
+ delete cache[name];
194
+ }
195
+ return clean;
196
+ }
197
+ /**
198
+ * 退役这个 guard:摘出 WeakMap(之后 acquire 会重建)、撤包装、清墓碑;非正常退役要上报。
199
+ *
200
+ * 三种退役理由对应三条真实路径:`idle` = 引用归零的正常释放(不上报);
201
+ * `taken-over` = 第三方在我们的包装器之上又包了一层(release 撤不干净,下次删除重建);
202
+ * `failed` = 首次使用抛 TypeError(宿主形状变了,已恢复原方法)。
203
+ */
204
+ function retire(reason) {
205
+ if (retired)
206
+ return;
207
+ retired = true;
208
+ if (cacheGuards.get(cache) === self)
209
+ cacheGuards.delete(cache);
210
+ blocked.clear();
211
+ if (reason === "idle")
212
+ return;
213
+ noteRuntime({
214
+ id: "projection-cache-adapter",
215
+ label: "投影缓存删除屏障",
216
+ kind: "delete",
217
+ fallback: "inform-only",
218
+ detail: reason === "taken-over"
219
+ ? "宿主缓存的 put/write 已被第三方包装接管:本插件的删除屏障这一轮已退役(缓存行删除少了墓碑拦截),下一次删除会重新安装。"
220
+ : "投影缓存适配在首次使用时抛错(宿主形状可能变了):已恢复宿主原方法,下一次删除会重新安装并再探一次。",
221
+ });
222
+ }
223
+ cacheGuards.set(cache, self);
224
+ return self;
155
225
  }
156
226
  /**
157
227
  * Live-session members the delete sequence needs, split by branch.
@@ -301,7 +371,15 @@ export function createSessionsBridge(ctx, suppliedRegistry, onArchive) {
301
371
  }]);
302
372
  if (typeof cache.delete === "function" && typeof cache.whenIdle === "function")
303
373
  return;
304
- guard ??= acquireCacheGuard(cache);
374
+ // 本地这份 guard 可能已经退役(第三方接管 / 首次使用抛错):退役时已从 WeakMap 摘掉自己,
375
+ // 这里也得换掉它 —— 拿一个不在岗的屏障去挡墓碑,正是"缓存行静默复活"那条路。
376
+ const next = acquireCacheGuard(cache);
377
+ if (next !== guard) {
378
+ const previous = guard;
379
+ guard = next;
380
+ if (previous)
381
+ await previous.release().catch(() => undefined);
382
+ }
305
383
  guard.begin(id, header);
306
384
  },
307
385
  cache() { return guard ?? cache; },
@@ -4,6 +4,24 @@ 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。
@@ -923,27 +941,11 @@ var ArchiveWorkspaceRegistry = class {
923
941
  * 后续目标并把成功、并发消失和失败分别返回给客户端。
924
942
  */
925
943
  async deleteArchivedSessions(target) {
926
- if (typeof this.registry.deleteArchivedSessions === "function") {
927
- const result = await this.registry.deleteArchivedSessions(target);
928
- await this.reconcileArchiveLedger();
929
- return result;
930
- }
944
+ // 与单删同一条纪律(A3-1):宿主补上原生批量入口也**先不切换** —— 只上报。
945
+ if (typeof this.registry.deleteArchivedSessions === "function")
946
+ reportNativeDeleteAvailable("deleteArchivedSessions");
931
947
  // 适配路径(无宿主原生批量入口):在任何会话被删除之前确认整条链路可用。
932
948
  this.bridge.checkWorkspace("delete");
933
- if (typeof this.registry.deleteSession === "function") {
934
- const requestedSessionIds = this.archivedSessionIdsForTarget(target);
935
- const result = { requestedSessionIds, deletedSessionIds: [], skippedSessionIds: [], failures: [] };
936
- for (const id of requestedSessionIds) {
937
- try {
938
- await this.deleteSession(id);
939
- result.deletedSessionIds.push(id);
940
- }
941
- catch (error) {
942
- result.failures.push({ sessionId: id, message: String(error) });
943
- }
944
- }
945
- return result;
946
- }
947
949
  return this.enqueueOperation(async () => {
948
950
  const requestedSessionIds = this.archivedSessionIdsForTarget(target);
949
951
  // 批首取一次权威快照:批内每一条删除都要一次全库 `listStoredHeaders()`(含逐会话
@@ -1047,18 +1049,19 @@ var ArchiveWorkspaceRegistry = class {
1047
1049
  * @throws {@link ArchiveUnknownSessionError} 会话未知时抛出。
1048
1050
  */
1049
1051
  async deleteSession(sessionId) {
1050
- if (typeof this.registry.deleteSession === "function") {
1051
- const result = await this.registry.deleteSession(sessionId);
1052
- await this.reconcileArchiveLedger();
1053
- return result;
1054
- }
1052
+ if (typeof this.registry.deleteSession === "function")
1053
+ reportNativeDeleteAvailable("deleteSession");
1055
1054
  return this.enqueueOperation(() => this.deleteSessionCore(sessionId));
1056
1055
  }
1057
1056
  /** 串行化后的删除主体(级联路径复用:它已持有操作链,绝不能再入队)。
1058
1057
  * @param storedIndex - 可选的批首 `id → header` 快照,见 {@link storedHeaderIndex}。 */
1059
1058
  async deleteSessionCore(sessionId, storedIndex) {
1059
+ // A3-1(第 3 版改向):宿主补上原生 `deleteSession` 时**先不切换** —— 原生分支跳过本插件
1060
+ // 的级联删子会话 / spill 清理 / 记账清理,等于行为静默缩水;而"切过去的代码"今天不可达,
1061
+ // 写成即等于它会在官方补上入口的那次升级上首次运行,且无从事前验证。保持自有完整序列
1062
+ //(=今天的能力),只上报一次「宿主已提供原生入口」,切换留到人工核对过原生语义之后。
1060
1063
  if (typeof this.registry.deleteSession === "function")
1061
- return this.registry.deleteSession(sessionId);
1064
+ reportNativeDeleteAvailable("deleteSession");
1062
1065
  if (this.deleting?.has(sessionId))
1063
1066
  throw new Error(`cyclic subagent lineage at "${sessionId}"`);
1064
1067
  this.deleting ??= new Set();
@@ -1092,6 +1095,16 @@ var ArchiveWorkspaceRegistry = class {
1092
1095
  const sessionRefusals = this.bridge.refusalsFor("delete", requiredSessionCapabilities(live !== void 0));
1093
1096
  if (sessionRefusals.length > 0)
1094
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()(官方接口已变动);操作在改动任何数据前停止,请更新本插件");
1095
1108
  // 先记录被删生命周期的日志身份:目录删除后头部不可再读,
1096
1109
  // 冷复用探针(sessionKnown 墓碑分支)靠它区分同 id 的新生命周期。
1097
1110
  const deletedHeader = this.headers.get(sessionId) ?? live?.header;
@@ -1342,8 +1355,28 @@ var ArchiveWorkspaceRegistry = class {
1342
1355
  }));
1343
1356
  // 实体快照的形状由宿主 `WorkspaceEntity` 决定(`record` 是私有字段,运行时存在)。
1344
1357
  const entity = this.entities.get(workspaceId);
1345
- if (entity !== void 0)
1358
+ if (entity === void 0)
1359
+ continue;
1360
+ // A3-3:官方把 `record` 改名或改成只读访问器时,这一行会**静默无效** —— 界面读到旧
1361
+ // 快照(会话计数不动),看起来却"什么都没发生"。写完立刻读回,没写进去就上报;
1362
+ // 仍然照写(不因为探测而少做一次更新)。
1363
+ let applied = false;
1364
+ try {
1346
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
+ }
1347
1380
  }
1348
1381
  }
1349
1382
  /** 尽力而为的级联删除:删除 `sessionId` 的 SUBAGENT 子会话。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-tool-management",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagents, AGENTS.md presets and archived sessions — scene memory injects into the system prompt, scene lock freezes all five domains, deleted workspaces re-register with one click. Settings UI + HTTP API + 14 model tools (mcp_manager_*, skill_manager_*, prompt_manager_*, memory_manager_*, subagent_*). Install with one command: dsh plugin --profile web add dsh-plugin-tool-management@latest",
5
5
  "repository": {
6
6
  "type": "git",
@@ -65,7 +65,7 @@
65
65
  }
66
66
  },
67
67
  "peerDependencies": {
68
- "@deepseek-ai/cordis": "^4.0.1",
68
+ "@deepseek-ai/cordis": "^4.0.2",
69
69
  "@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
70
70
  "@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
71
71
  "@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2",
@@ -92,14 +92,17 @@
92
92
  "@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
93
93
  "@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
94
94
  "@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
95
+ "@deepseek-ai/dsh-subagent-fork-in-process": "^0.1.5-rc.2",
96
+ "@deepseek-ai/dsh-subagent-spawn-in-process": "^0.1.5-rc.2",
95
97
  "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
96
- "@deepseek-ai/dsh-user-approval": "^0.1.5-rc.2",
97
98
  "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
99
+ "@types/js-yaml": "^4.0.9",
98
100
  "@types/node": "^24.0.0",
99
101
  "typescript": "^6.0.0"
100
102
  },
101
103
  "license": "MIT",
102
104
  "dependencies": {
103
- "fflate": "^0.8.3"
105
+ "fflate": "^0.8.3",
106
+ "js-yaml": "^4.3.2"
104
107
  }
105
108
  }