dsh-plugin-tool-management 0.11.0 → 0.12.1

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/lib/ops/compat.js CHANGED
@@ -6,8 +6,10 @@
6
6
  //
7
7
  // 边界纪律:本文件只负责「op 的实现」,不负责 ops 表之外的接线(MCP 写后刷新、场景锁定
8
8
  // 守卫仍作用在组装后的整张表上,见 index.ts)。
9
- import { EXPECTED_MIN_HOST_VERSION, EXPECTED_PEER_RANGE, VERIFIED_HOST_VERSION, summarize, } from '../compat/probe.js';
9
+ import { EXPECTED_MIN_HOST_VERSION, EXPECTED_PEER_RANGE, VERIFIED_HOST_VERSION, routeFor, summarize, } from '../compat/probe.js';
10
+ import { INJECT_DOMAIN_KEYS } from '../context-inject.js';
10
11
  import { assessPresetReach } from '../compat/preset-reach.js';
12
+ import { runtimeNotes } from '../compat/runtime-notes.js';
11
13
  import { pluginLog } from '../skills/service.js';
12
14
  export function buildCompatOps(deps) {
13
15
  return {
@@ -29,13 +31,30 @@ export function buildCompatOps(deps) {
29
31
  };
30
32
  }
31
33
  const summary = summarize(assessment);
34
+ // 运行时降级(A1 的补丁校验、A2 的投影缓存适配、B3 的装配层…)统一在这里并进
35
+ // findings/degraded:它们不是宿主能力,但必须各占一行 —— 否则用户只能在事故之后
36
+ // 才知道某道保险没生效。上报口径见 compat/runtime-notes.ts。
37
+ const extraFindings = runtimeNotes().map((note) => ({
38
+ id: note.id,
39
+ label: note.label,
40
+ kind: note.kind,
41
+ owner: 'plugin',
42
+ fallback: note.fallback,
43
+ state: 'not-available',
44
+ missing: [],
45
+ detail: note.detail,
46
+ }));
47
+ const findings = [...assessment.findings, ...extraFindings];
48
+ const degraded = [...assessment.degraded, ...extraFindings];
49
+ const merged = { ...assessment, findings, degraded };
50
+ const summaryMerged = extraFindings.length === 0 ? summary : summarize(merged);
32
51
  // 每次刷新都留一条结构化日志:升级当天就能在日志里看到降级发生。
33
52
  if (force || !deps.compatLog.version || deps.compatLog.version !== assessment.identity.version) {
34
53
  deps.compatLog.version = assessment.identity.version;
35
54
  void pluginLog()('compat/probe', {
36
55
  hostVersion: assessment.identity.version,
37
- summary,
38
- degraded: assessment.degraded.map((item) => ({ id: item.id, state: item.state, detail: item.detail })),
56
+ summary: summaryMerged,
57
+ degraded: degraded.map((item) => ({ id: item.id, state: item.state, detail: item.detail })),
39
58
  blockers: assessment.identity.blockers,
40
59
  sameAsHost: assessment.identity.sameAsHost,
41
60
  }).catch(() => { });
@@ -45,15 +64,15 @@ export function buildCompatOps(deps) {
45
64
  host: { version: assessment.identity.version, modules: assessment.identity.modules },
46
65
  sameAsHost: assessment.identity.sameAsHost,
47
66
  unverified: assessment.identity.unverified,
48
- findings: assessment.findings,
49
- degraded: assessment.degraded,
67
+ findings,
68
+ degraded,
50
69
  blockers: assessment.identity.blockers,
51
70
  mayDelete: assessment.mayDelete,
52
71
  verifiedVersion: VERIFIED_HOST_VERSION,
53
72
  expectedPeerRange: EXPECTED_PEER_RANGE,
54
73
  minHostVersion: EXPECTED_MIN_HOST_VERSION,
55
74
  generatedAt: assessment.generatedAt,
56
- summary,
75
+ summary: summaryMerged,
57
76
  };
58
77
  }
59
78
  catch (e) {
@@ -91,6 +110,77 @@ export function buildCompatOps(deps) {
91
110
  },
92
111
  // 注入设置(读 / 写):压制型预设下是否仍然注入 + 各域开关。界面在「兼容」页。
93
112
  'inject-settings': (args) => deps.injectSettingsOp(args),
113
+ // 功能总览(B6):**按功能点**回答"现在每一项到底能不能用",三层合成 ——
114
+ // ① 装配层(listener / provider / 适配是否真的挂上,来自运行时上报通道)
115
+ // ② 宿主能力层(compat 探测的路由判定)
116
+ // ③ 用户配置层(开关 / 令牌 / 场景锁定)
117
+ // 数据全部来自已有的缓存状态与运行时上报,**不新增宿主探测**(避免哨兵真调一类的开销)。
118
+ // 行内 `tab` 供界面跳转到对应页签;`state` 取值见下方约定。
119
+ 'feature-overview': async () => {
120
+ try {
121
+ const registry = deps.getSessionsRegistry();
122
+ const assessment = registry && typeof registry.capabilities === 'function' ? registry.capabilities() : undefined;
123
+ const notes = new Map(runtimeNotes().map((note) => [note.id, note]));
124
+ const settings = await deps.readInjectSettings();
125
+ const domainsOn = INJECT_DOMAIN_KEYS.filter((key) => settings.domains[key] !== false);
126
+ const token = await deps.readTokenState();
127
+ const lockedScenes = await deps.lockedSceneNames();
128
+ const disabledTools = deps.disabledToolCount();
129
+ const routeOf = (operation) => assessment === undefined ? undefined : routeFor(assessment, operation);
130
+ /** 一行一个功能点:状态三层合成,`detail` 写明依据。 */
131
+ const rows = [];
132
+ const push = (key, label, tab, state, detail) => rows.push({ key, label, tab, state, detail });
133
+ // 前置项:插件挂载(这一行本身说明 12 个 inject 都解析了 —— 否则 apply 根本不会跑);
134
+ // 启动期那段 `!!js` 表达式若抛错,插件同样不会挂上(它的 try/catch 就是为了不抛)。
135
+ const mountNote = notes.get('cordis-original-symbol');
136
+ push('mount', '插件挂载', 'compat', mountNote === undefined ? 'ok' : 'degraded', mountNote === undefined ? '12 个 inject 服务全部解析,插件已挂载(详见 doctor 的挂载心跳)' : mountNote.detail);
137
+ // 装配层:有上报就是降级,附上报里的原因;没有就是正常。
138
+ const assemblyRows = [
139
+ ['patch-write-guard', '宿主配置写入(补丁校验)', 'mcp'],
140
+ ['context-injection', '上下文注入通道', 'compat'],
141
+ ['official-suppression', '官方注入的关域拦截', 'compat'],
142
+ ['skills-provider', '技能 provider 装配', 'skills'],
143
+ ['projection-cache-adapter', '投影缓存删除屏障', 'sessions'],
144
+ ['mcp-tool-visibility', '停用工具的可见性', 'mcp'],
145
+ ];
146
+ for (const [noteId, label, tab] of assemblyRows) {
147
+ const note = notes.get(noteId);
148
+ push(noteId, label, tab, note === undefined ? 'ok' : 'degraded', note === undefined ? '装配正常,无降级上报' : note.detail);
149
+ }
150
+ // 宿主能力层:删除 / 归档 / 恢复 / 批量各自由路由判定回答(与服务端执行同源)。
151
+ const deleteRoute = routeOf('delete');
152
+ push('session-delete', '会话删除', 'sessions', deleteRoute === undefined ? 'unknown' : deleteRoute.via === 'none' ? 'unavailable' : 'ok', deleteRoute === undefined ? '能力探测不可用(归档服务未挂载)'
153
+ : deleteRoute.via === 'none' ? '宿主缺少该路径所需能力:' + deleteRoute.refusals.map((item) => item.label).join('、')
154
+ : `可用(路径:${deleteRoute.via})`);
155
+ for (const [operation, label] of [['archive', '归档'], ['unarchive', '恢复'], ['batch', '批量操作'], ['list', '历史列表']]) {
156
+ const decision = routeOf(operation);
157
+ push('session-' + operation, `会话${label}`, 'sessions', decision === undefined ? 'unknown' : decision.via === 'none' ? 'unavailable' : 'ok', decision === undefined ? '能力探测不可用(归档服务未挂载)'
158
+ : decision.via === 'none' ? '宿主缺少该路径所需能力:' + decision.refusals.map((item) => item.label).join('、')
159
+ : `可用(路径:${decision.via})`);
160
+ }
161
+ // 用户配置层:注入域 / 令牌 / 场景锁定 / 停用工具数。
162
+ push('injection-domains', '注入域开关', 'compat', domainsOn.length === 0 ? 'disabled' : 'ok', domainsOn.length === 0 ? '五个注入域全部关闭(用户设置)' : `${domainsOn.length}/5 个域开启:${domainsOn.join('、')}`);
163
+ push('token', '访问令牌', 'compat', !token.active ? 'disabled' : token.accepted ? 'ok' : 'locked', !token.active ? '未启用(宿主没配令牌,或令牌功能被关掉)'
164
+ : token.accepted ? '已生效且本次启动已通过验证' : '已生效,本次启动尚未验证:写操作与对话会被拦住,到本页下方填写令牌');
165
+ push('scene-lock', '场景锁定', 'scenes', lockedScenes.length === 0 ? 'ok' : 'locked', lockedScenes.length === 0 ? '无锁定场景' : `锁定中:${lockedScenes.join('、')}(写门禁按锁定场景生效)`);
166
+ push('mcp-tools', 'MCP 工具停用', 'mcp', disabledTools === 0 ? 'ok' : 'partial', disabledTools === 0 ? '没有停用的工具' : `${disabledTools} 个工具处于停用态(执行拦截 + 可见性摘除)`);
167
+ push('native-delete', '宿主原生删除入口', 'compat', notes.has('workspace.delete-native') ? 'partial' : 'ok', notes.get('workspace.delete-native')?.detail ?? '宿主未提供原生删除入口(本插件自有完整序列)');
168
+ // 身份 / 版本:与 compat-status 同一份数据。
169
+ const identity = assessment?.identity;
170
+ const identityIssue = identity !== undefined && (identity.blockers.length > 0 || Object.values(identity.sameAsHost).some((same) => same === false));
171
+ push('host-identity', '宿主身份与模块', 'compat', assessment === undefined ? 'unknown' : identityIssue ? 'degraded' : 'ok', assessment === undefined ? '能力探测不可用(归档服务未挂载)'
172
+ : identityIssue ? '存在阻塞项:' + (identity?.blockers ?? []).join(';')
173
+ : `模块与宿主同源(宿主 DSH ${identity?.version ?? '?'})`);
174
+ // 无独立装配点的域:没有上报就是正常(如实说明依据是"没有降级上报")。
175
+ for (const [key, label, tab] of [['memory', '记忆', 'memory'], ['prompts', '提示词', 'prompts'], ['subagents', '子智能体', 'subagents'], ['scenes', '场景档案', 'scenes']]) {
176
+ push(key, label, tab, 'ok', '无降级上报(装配失败会出现在这里)');
177
+ }
178
+ return { ok: true, rows, generatedAt: Date.now() };
179
+ }
180
+ catch (e) {
181
+ return { ok: false, error: deps.message(e) };
182
+ }
183
+ },
94
184
  // patch 备份(改宿主配置前的整份副本):列清单只读,清理按写门禁。
95
185
  // 每份备份里都是**明文**凭据,所以给一个显式出口让人能删掉多余的副本(理由见
96
186
  // cleanPatchBackups 的注释)。清单不带文件内容,只给名字 / 层级 / 时间 / 大小。
@@ -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 子会话。
@@ -21,7 +21,7 @@
21
21
  /** 描述截断长度(与官方目录的 `catalogDescriptionMaxLength` 默认值一致)。 */
22
22
  export const SKILL_CATALOG_DESCRIPTION_MAX_LENGTH = 500;
23
23
  /** 目录最多列几条;超出只报数量(让模型去调 `skill_manager_list`)。 */
24
- export const SKILL_CATALOG_MAX_ENTRIES = 50;
24
+ export const SKILL_CATALOG_MAX_ENTRIES = 60;
25
25
  /** 描述归一化 + 截断(与官方 `catalogDescription` 同款:折叠空白、超出补省略号)。 */
26
26
  function catalogDescription(value, maxLength) {
27
27
  const normalized = String(value ?? '').replaceAll(/\s+/g, ' ').trim();
@@ -11,7 +11,7 @@ import { filterBySceneBinding } from './tools.js';
11
11
  */
12
12
  export const DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH = 500;
13
13
  /** 段里最多列几个人设;超出部分只报数量,让模型自己去调 subagent_manager_list。 */
14
- export const DEFAULT_CATALOG_MAX_ENTRIES = 40;
14
+ export const DEFAULT_CATALOG_MAX_ENTRIES = 60;
15
15
  /** 无描述时的占位,与 `subagent_manager_list` 工具的输出保持同一口径。 */
16
16
  const NO_DESCRIPTION = '(无描述)';
17
17
  /**
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.1",
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",
@@ -33,14 +33,14 @@
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsc -p tsconfig.json && node scripts/sync-client.mjs && node scripts/sync-profile.mjs",
36
- "prepublishOnly": "npm run build && npm run lint && node --test test/client-exports.test.mjs",
36
+ "prepublishOnly": "npm run build && npm run lint && npm run typecheck:client && node --test test/client-exports.test.mjs",
37
37
  "lint": "node --check lib/client.js && node --check lib/index.js",
38
38
  "typecheck:client": "node scripts/sync-client.mjs && tsc -p tsconfig.client.json",
39
39
  "check:i18n": "node scripts/check-i18n.mjs lib/client.js",
40
40
  "check:host": "node scripts/host-deps.mjs && node scripts/doctor.mjs",
41
41
  "host-deps": "node scripts/host-deps.mjs --fix",
42
42
  "doctor": "node scripts/doctor.mjs",
43
- "test": "npm run build && npm run check:i18n && node --test test/contracts.test.mjs test/client-exports.test.mjs",
43
+ "test": "npm run build && npm run check:i18n && npm run typecheck:client && node --test test/contracts.test.mjs test/client-exports.test.mjs",
44
44
  "sync:profile": "node scripts/sync-profile.mjs"
45
45
  },
46
46
  "keywords": [
@@ -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
  }