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
@@ -127,7 +127,7 @@ export function createScenePromptSync(deps) {
127
127
  warn(`scene-prompt: refs probe (scenes) failed: ${String(e)}`);
128
128
  }
129
129
  try {
130
- const cur = await deps.agentsMd.getCurrent();
130
+ const cur = await deps.prompts.getCurrent();
131
131
  if (!cur || cur.ok !== true)
132
132
  complete = false;
133
133
  else if (cur.presetId)
@@ -159,7 +159,7 @@ export function createScenePromptSync(deps) {
159
159
  // 基线始终是"进场景之前"那一份)。
160
160
  const saved = await readBaseline();
161
161
  if (!saved || saved.content === null) {
162
- const cur = await deps.agentsMd.getCurrent();
162
+ const cur = await deps.prompts.getCurrent();
163
163
  await writeBaseline({
164
164
  v: 1,
165
165
  at: Date.now(),
@@ -167,7 +167,7 @@ export function createScenePromptSync(deps) {
167
167
  content: cur && cur.ok && cur.exists ? String(cur.content) : null,
168
168
  });
169
169
  }
170
- const res = await deps.agentsMd.apply(current.presetId);
170
+ const res = await deps.prompts.apply(current.presetId);
171
171
  if (res && res.ok === false)
172
172
  return { error: String(res.error || '写入 AGENTS.md 失败') };
173
173
  return { applied: current.presetId };
@@ -176,7 +176,7 @@ export function createScenePromptSync(deps) {
176
176
  const saved = await readBaseline();
177
177
  if (!saved || saved.content === null)
178
178
  return { unchanged: true };
179
- const res = await deps.agentsMd.restore(saved.content);
179
+ const res = await deps.prompts.restore(saved.content);
180
180
  if (res && res.ok === false)
181
181
  return { error: String(res.error || '恢复 AGENTS.md 失败') };
182
182
  await writeBaseline({ v: 1, at: Date.now(), presetId: null, content: null });
@@ -0,0 +1,344 @@
1
+ // 场景 / 表单的**候选源** —— 界面要"选一个"的地方,数据从哪来。
2
+ //
3
+ // 2026-09-19 从 index.ts 的 apply 闭包原样抽出(10 段,约 260 行)。为什么单独一个文件:
4
+ // 这些函数是**只读的取数**,彼此只有缓存关系,与"写补丁 / 改场景"完全无关;此前散在
5
+ // apply 闭包的四个角落(286 / 425 / 532 / 833 / 1924-2133),改一处要在 3000 行里找。
6
+ //
7
+ // 三组候选,各自的失效模式不同:
8
+ // * **技能 / 记忆 / 场景**(skillRows / memoryCandidates / memorySceneCandidates)——
9
+ // 直接读各自 service 的 op,无缓存;
10
+ // * **预设工具**(presetToolCandidates / presetToolNames)—— 枚举会为预设建立 standing
11
+ // mount(官方语义:每进程一次),所以按 PRESET_ENUM_CACHE_MS 缓存,不能每次重枚举;
12
+ // * **模型**(modelCandidates)—— 只读宿主 LLM 目录,**不发网络请求**。
13
+ //
14
+ // 依赖全部经 deps 显式传入。注意 \`toolKeyParts\` 留在 index.ts:场景档案引擎也在用它,
15
+ // 放进本文件会让两个域互相引用。
16
+ import { presetRosterOf } from '../compat/preset-reach.js';
17
+ import { injectionFactsOf, readCompositionFacts } from '../compat/preset-reach.js';
18
+ export function createCandidates(deps) {
19
+ // 外部能力一次解构成局部名:块内代码逐字搬来,保持原样最不容易出错。
20
+ const { get, tools, mcp, skillsService, memoriesService, toolKeyParts } = deps;
21
+ const ctx = { get };
22
+ // Agent 预设名单(@deepseek-ai/dsh-agent-presets):同样是可选服务,按需取用。
23
+ // 读它只为了回答"当前预设下本插件注入的东西到不到得了模型"——只读,不挂载任何预设。
24
+ const presetRoster = () => presetRosterOf(ctx);
25
+ async function toolStates() {
26
+ const disabled = await mcp.readDisabledTools();
27
+ const states = {};
28
+ let schemas = [];
29
+ try {
30
+ schemas = await tools.schemas();
31
+ }
32
+ catch { /* 无 live 工具 → 仅启停表 */ }
33
+ const liveByServer = {};
34
+ for (const s of schemas) {
35
+ const p = toolKeyParts(String((s && s.name) || ''));
36
+ if (!p)
37
+ continue;
38
+ const list = liveByServer[p.server] || (liveByServer[p.server] = []);
39
+ if (list.indexOf(p.tool) < 0)
40
+ list.push(p.tool);
41
+ }
42
+ for (const [server, list] of Object.entries(disabled)) {
43
+ // 整台停用(['*']):在 live 工具上展开成具体键,不产出 `server/*` 伪键(那会污染勾选器)。
44
+ if (list.indexOf('*') >= 0) {
45
+ for (const t of (liveByServer[server] || []))
46
+ states[server + '/' + t] = false;
47
+ continue;
48
+ }
49
+ for (const t of list)
50
+ states[server + '/' + t] = false;
51
+ }
52
+ for (const [server, list] of Object.entries(liveByServer)) {
53
+ for (const t of list)
54
+ if (!(server + '/' + t in states))
55
+ states[server + '/' + t] = true;
56
+ }
57
+ return states;
58
+ }
59
+ /**
60
+ * 场景档案「技能集」的技能行 —— 技能名与来源名**分开**给出。
61
+ *
62
+ * 界面早先直接渲染 `key`(形如 `<来源 key>/<技能名>`),而自定义目录的来源 key 是
63
+ * `custom-<sha256-16>`,于是导入的技能在场景档案里显示成一串哈希(用户 2026-09-15 报的)。
64
+ * 这里把来源的 label / localeKey 一并带上,界面就能显示「技能名 · 来源名」。
65
+ *
66
+ * 含被覆盖的副本(`shadowed`):场景允许勾选任意一条技能,用生效集校验会把合法勾选判成 stale。
67
+ */
68
+ async function skillRows() {
69
+ const rows = [];
70
+ try {
71
+ const r = await skillsService.ops['skill-state']({});
72
+ for (const root of ((r && r.data && r.data.roots) || [])) {
73
+ const rootKey = String((root && root.key) || '');
74
+ if (!rootKey)
75
+ continue;
76
+ for (const sk of ((root && root.skills) || [])) {
77
+ const name = String((sk && (sk.declaredName || sk.name)) || '');
78
+ if (!name)
79
+ continue;
80
+ rows.push({
81
+ key: rootKey + '/' + name,
82
+ name,
83
+ enabled: sk.enabled === true && !sk.shadowedBy,
84
+ shadowed: !!sk.shadowedBy,
85
+ loadable: sk.loadable !== false,
86
+ rootKey,
87
+ rootLabel: String((root && root.label) || rootKey),
88
+ ...(root && root.localeKey ? { rootLocaleKey: String(root.localeKey) } : {}),
89
+ ...(root && root.kind ? { rootKind: String(root.kind) } : {}),
90
+ });
91
+ }
92
+ }
93
+ }
94
+ catch { /* 技能状态读不到 → 空列表(界面如实显示"没有可选项") */ }
95
+ // 平铺列表按技能名排(同名再按来源),比按来源聚在一起更好找。
96
+ return rows.sort((a, b) => a.name.localeCompare(b.name) || a.rootKey.localeCompare(b.rootKey));
97
+ }
98
+ // 注入判定要的两条预设事实(压制型?挂得到 AGENTS.md 通道?)。读取走 TTL 缓存:
99
+ // 组合文件可以随时被编辑(patchReload: live),但每个 step 读一次盘没有必要。
100
+ const PRESET_FACTS_TTL_MS = 15_000;
101
+ const presetFactsCache = new Map();
102
+ async function presetFactsForAgent(agent) {
103
+ const roster = presetRoster();
104
+ if (!roster || typeof roster.composedPreset !== 'function' || typeof roster.read !== 'function')
105
+ return undefined;
106
+ let presetId = '';
107
+ try {
108
+ presetId = String(roster.composedPreset(agent?.ctx) ?? '');
109
+ }
110
+ catch {
111
+ return undefined;
112
+ }
113
+ if (presetId === '')
114
+ return undefined;
115
+ const hit = presetFactsCache.get(presetId);
116
+ if (hit && Date.now() - hit.at < PRESET_FACTS_TTL_MS)
117
+ return hit.value;
118
+ try {
119
+ const text = String((await roster.read(presetId)) ?? '');
120
+ const value = injectionFactsOf(readCompositionFacts(text));
121
+ presetFactsCache.set(presetId, { at: Date.now(), value });
122
+ return value;
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ }
128
+ /**
129
+ * 记忆候选(档案编辑器第 4 段「记忆」):`[{ id, scene, name, description }]`。
130
+ * 只读、不读正文(勾选集只存 id);任何异常降级为空列表,不让档案弹窗崩掉。
131
+ */
132
+ async function memoryCandidates() {
133
+ try {
134
+ const r = await memoriesService.ops['rules-list']({});
135
+ if (!r || r.ok === false)
136
+ return [];
137
+ return (r.rules || [])
138
+ .filter((x) => !x.shadowed)
139
+ .map((x) => ({
140
+ id: String(x.id),
141
+ scene: String(x.group || ''),
142
+ name: String(x.name || ''),
143
+ description: String(x.description || ''),
144
+ }));
145
+ }
146
+ catch {
147
+ return [];
148
+ }
149
+ }
150
+ /** 场景候选(档案编辑器第 4 段的分组维度):`[{ name, label, description, count, global }]`。 */
151
+ async function memorySceneCandidates() {
152
+ try {
153
+ const r = await memoriesService.ops['rules-list']({});
154
+ if (!r || r.ok === false)
155
+ return [];
156
+ return (r.scenes || []).map((s) => ({
157
+ name: String(s.name),
158
+ label: String(s.label || s.name),
159
+ description: String(s.description || ''),
160
+ count: Number(s.count || 0),
161
+ global: s.global === true,
162
+ }));
163
+ }
164
+ catch {
165
+ return [];
166
+ }
167
+ }
168
+ /**
169
+ * 工具候选(人设的「工具白名单 / 黑名单」选择器):
170
+ * 取**全体 Agent 预设工具名的并集**——人设可能在任意预设下被子代理复用,
171
+ * 只列当前会话的工具会让换预设后的子代理启动失败(官方 `toolFilter` 对未知名直接拒绝)。
172
+ * 同时标注 `current`:当前会话可见的工具(其余只是「本预设可用,当前会话看不到」)。
173
+ *
174
+ * 每次调用都会为尚未挂载的预设建立 standing mount(官方语义:一个预设在本进程内只挂一次,
175
+ * 正常创建会话时同样会挂),因此结果会按需缓存 PRESET_ENUM_CACHE_MS,避免频繁枚举。
176
+ */
177
+ // 两个预设枚举缓存(全体候选 / 单个预设)共用这个 TTL:枚举的代价是"给预设建立
178
+ // standing mount",与内容变化无关,所以只要别刷得太勤就行。
179
+ const PRESET_ENUM_CACHE_MS = 60_000;
180
+ let presetToolsCache = null;
181
+ async function presetToolCandidates() {
182
+ if (presetToolsCache && Date.now() - presetToolsCache.at < PRESET_ENUM_CACHE_MS)
183
+ return presetToolsCache.value;
184
+ const byName = new Map();
185
+ const presets = [];
186
+ // 当前会话的可见工具(用于标 current);拿不到就全部按「非当前」处理。
187
+ const currentNames = new Set();
188
+ try {
189
+ const schemas = await tools.schemas();
190
+ for (const s of schemas || [])
191
+ currentNames.add(String(s.name));
192
+ }
193
+ catch { /* 无 live 工具 → current 全 false */ }
194
+ const agentPresets = (typeof ctx.get === 'function' ? ctx.get('agentPresets') : undefined);
195
+ if (agentPresets && typeof agentPresets.list === 'function') {
196
+ try {
197
+ // roster 顺序 = 官方的展示顺序(preset.yml 的 order:标准 1 / PTC 2 / 极简 3 / 创造 4,
198
+ // 自建预设排在其后)。人设编辑器的四行照抄这个顺序,**不要**再按 id 排序。
199
+ const roster = await agentPresets.list();
200
+ for (const p of roster || []) {
201
+ const id = String((p && p.id) || '');
202
+ if (!id)
203
+ continue;
204
+ // MCP 工具名形如 `mcp__<server>__<tool>`:面向上百个条目,噪声大于价值 → 不进候选。
205
+ // 子代理照样能用当前在跑的 MCP —— 那份名单在 decideToolFilter 里运行时并进白名单。
206
+ const names = (await presetToolNames(id)).filter((name) => !name.startsWith('mcp__'));
207
+ presets.push({
208
+ id,
209
+ name: String((p && (p.name || p.id)) || id),
210
+ trust: String((p && p.trust) || 'user'),
211
+ broken: typeof (p && p.broken) === 'string',
212
+ tools: names,
213
+ });
214
+ for (const name of names) {
215
+ const rec = byName.get(name) || { name, presets: new Set(), current: false };
216
+ rec.presets.add(id);
217
+ byName.set(name, rec);
218
+ }
219
+ }
220
+ }
221
+ catch { /* 预设服务不可用 → 退回「仅当前会话工具」 */ }
222
+ }
223
+ // 当前会话的工具即便没有任何预设可枚举,也要出现在候选里(否则选择器是空的)。
224
+ for (const name of currentNames) {
225
+ if (name.startsWith('mcp__'))
226
+ continue;
227
+ const rec = byName.get(name) || { name, presets: new Set(), current: false };
228
+ byName.set(name, rec);
229
+ }
230
+ for (const rec of byName.values())
231
+ rec.current = currentNames.has(rec.name);
232
+ const value = {
233
+ tools: [...byName.values()]
234
+ .map((r) => ({ name: r.name, presets: [...r.presets].sort(), current: r.current }))
235
+ .sort((a, b) => a.name.localeCompare(b.name)),
236
+ // 保持 roster 顺序(官方的展示顺序),前端四行直接照用。
237
+ presets,
238
+ };
239
+ presetToolsCache = { at: Date.now(), value };
240
+ return value;
241
+ }
242
+ /**
243
+ * 单个预设的工具名(列表里含 MCP 与宿主平面的工具,因为子代理的可见集合是
244
+ * "宿主平面 ∪ 该预设"的并集)。枚举会为该预设建立 standing mount —— 官方语义:
245
+ * 一个预设每进程只挂一次,正常创建会话时同样会挂,所以这里按 id 缓存 PRESET_ENUM_CACHE_MS;
246
+ * `subagent_manager_run` 每次委派都要用它校验名单,不能每次都重新枚举。
247
+ */
248
+ const presetNamesCache = new Map();
249
+ async function presetToolNames(id) {
250
+ const hit = presetNamesCache.get(id);
251
+ if (hit && Date.now() - hit.at < PRESET_ENUM_CACHE_MS)
252
+ return hit.names;
253
+ const agentPresets = (typeof ctx.get === 'function' ? ctx.get('agentPresets') : undefined);
254
+ let scopeKey;
255
+ try {
256
+ scopeKey = agentPresets && typeof agentPresets.standingKeyFor === 'function'
257
+ ? await agentPresets.standingKeyFor(id)
258
+ : undefined;
259
+ }
260
+ catch {
261
+ scopeKey = undefined;
262
+ }
263
+ let names = [];
264
+ try {
265
+ names = ((await tools.schemas(scopeKey)) || []).map((s) => String(s.name)).filter(Boolean);
266
+ }
267
+ catch {
268
+ names = [];
269
+ }
270
+ presetNamesCache.set(id, { at: Date.now(), names });
271
+ return names;
272
+ }
273
+ /** 预设名单的名字投影(只读 roster,不挂载任何预设):场景页显示模式名用。 */
274
+ async function presetNames() {
275
+ const agentPresets = (typeof ctx.get === 'function' ? ctx.get('agentPresets') : undefined);
276
+ if (!agentPresets || typeof agentPresets.list !== 'function')
277
+ return [];
278
+ try {
279
+ const roster = await agentPresets.list();
280
+ return (roster || [])
281
+ .map((p) => ({ id: String((p && p.id) || ''), name: String((p && (p.name || p.id)) || ''), trust: String((p && p.trust) || 'user') }))
282
+ .filter((p) => p.id !== '');
283
+ }
284
+ catch {
285
+ return [];
286
+ }
287
+ }
288
+ /**
289
+ * 模型候选(人设的「模型」下拉):宿主已注册的 provider + 各 provider 能宣告的模型。
290
+ * 只读宿主 LLM 目录,**不发起网络请求**(`discoverModels` 会打端点,这里不用);
291
+ * 拿不到就返回空列表,UI 退回手填(跨来源模型如 sensenova 的手工条目仍需手填兜底)。
292
+ * 返回扁平列表:DSH 的模型路由是 (provider, model) 一对,两个键必须同时给。
293
+ */
294
+ async function modelCandidates() {
295
+ const llm = (typeof ctx.get === 'function' ? ctx.get('llm') : undefined);
296
+ if (!llm || typeof llm.listProviders !== 'function')
297
+ return { models: [] };
298
+ let list = [];
299
+ try {
300
+ list = llm.listProviders() || [];
301
+ }
302
+ catch {
303
+ return { models: [] };
304
+ }
305
+ const models = [];
306
+ const seen = new Set();
307
+ for (const p of list) {
308
+ const provider = String((p && (p.id || p.provider)) || '');
309
+ if (!provider)
310
+ continue;
311
+ const providerName = String((p && (p.name || p.displayName)) || provider);
312
+ let discovered = [];
313
+ try {
314
+ // 适配器可选提供 listModels(官方 LlmAdapter 契约);未提供时该 provider 只报名字。
315
+ if (typeof llm.listModels === 'function')
316
+ discovered = (await llm.listModels(provider)) || [];
317
+ }
318
+ catch { /* 该 provider 的目录读失败 → 只报 provider 本身 */ }
319
+ for (const m of discovered) {
320
+ const id = String((m && (m.id || m.model)) || '');
321
+ if (!id)
322
+ continue;
323
+ const key = provider + '\u0000' + id;
324
+ if (seen.has(key))
325
+ continue;
326
+ seen.add(key);
327
+ models.push({ provider, providerName, id, name: String((m && m.name) || id) });
328
+ }
329
+ }
330
+ return { models };
331
+ }
332
+ return {
333
+ presetRoster,
334
+ presetFactsForAgent,
335
+ toolStates,
336
+ skillRows,
337
+ presetToolCandidates,
338
+ presetToolNames,
339
+ presetNames,
340
+ modelCandidates,
341
+ memoryCandidates,
342
+ memorySceneCandidates,
343
+ };
344
+ }
@@ -9,8 +9,19 @@ export { CapabilityRefusalError };
9
9
  // presence, shapes, read-only behaviour), never from comparing source text:
10
10
  // text equality only ever proved "same release", and it turned every harmless
11
11
  // upstream refactor into a total feature outage.
12
+ //
13
+ // 纪律边界(对工作区「官方包与宿主机制只读」一条的解释,2026-09-18 复核确认):
14
+ // 这里的包装是**运行时实例**上的临时、可恢复、能力门控的适配(仅当宿主自己缺
15
+ // delete/whenIdle 时才装;按原 descriptor 恢复;serial 队列防复活),不修改任何
16
+ // 官方包文件、不持久化改动、不改变宿主机制的对外行为 —— 与「不 patch 官方代码」
17
+ // 禁令针对的对象(包文件与宿主机制的永久改写)不同层。若用户裁定该解释不成立,
18
+ // 撤掉 acquire/releaseCacheGuard 即可整体退回「无删除屏障」的降级形态。
12
19
  const cacheGuards = new WeakMap();
13
- const raw = (value) => value?.[symbols.original] ?? value;
20
+ const raw = (value) => {
21
+ // cordis 的 traceable 代理在 [symbols.original] 上挂原始对象;无代理时原样返回
22
+ const holder = value;
23
+ return holder?.[symbols.original] ?? value;
24
+ };
14
25
  const sameLifecycle = (a, b) => a && b && a.createdAt === b.createdAt && (a.cwd ?? null) === (b.cwd ?? null);
15
26
  /**
16
27
  * Install the write barrier around the host's projection cache when (and only
@@ -161,10 +172,12 @@ export function requiredSessionCapabilities(hasLiveSession) {
161
172
  ? ["sessions.detach-live"]
162
173
  : ["sessions.cold-announce"];
163
174
  }
164
- export function createHistoryBridge(ctx, suppliedRegistry, onArchive) {
175
+ export function createSessionsBridge(ctx, suppliedRegistry, onArchive) {
165
176
  // Cordis returns a fresh traceable method proxy on access. Compare and adapt
166
177
  // the original object, not proxies; calls retain the service owner's context.
167
178
  const registry = raw(suppliedRegistry);
179
+ // 官方声明把 put/requireTable 标为 private(运行时存在,见文件头「纪律边界」),
180
+ // 因此按本文件实际调用面收窄,不改动宿主对象本身。
168
181
  const cache = raw(ctx.get("sessionProjectionCache"));
169
182
  let guard;
170
183
  let stopObserving;
@@ -209,7 +222,9 @@ export function createHistoryBridge(ctx, suppliedRegistry, onArchive) {
209
222
  const method = (name, ...args) => {
210
223
  if (typeof registry[name] !== "function")
211
224
  throw new Error(`宿主缺少归档兼容接口 ${name},操作已停止`);
212
- return registry[name](...args);
225
+ // 动态成员名:先收窄为可调用类型,再以 registry 为 receiver 调用,
226
+ // 与 registry[name](...args) 的 this 绑定完全一致。
227
+ return registry[name].call(registry, ...args);
213
228
  };
214
229
  /**
215
230
  * The adapter route needs the registry's read/write/delegate members. This is
@@ -249,9 +264,11 @@ export function createHistoryBridge(ctx, suppliedRegistry, onArchive) {
249
264
  // from the official sidebar or an external archive manager. No monkey
250
265
  // patch of archiveSession and no competing writer of its ledger.
251
266
  stopObserving = ctx.on("domain/changed", (change) => {
252
- if (change?.domain !== workspaceDomainSpec.name || change.table !== "" || !Array.isArray(change.value?.archivedSessionIds))
267
+ // 官方事件类型把 value 声明为 unknown(载荷形状由 workspace 域规范决定)。
268
+ const payload = change.value;
269
+ if (change?.domain !== workspaceDomainSpec.name || change.table !== "" || !Array.isArray(payload?.archivedSessionIds))
253
270
  return;
254
- const next = new Set(change.value.archivedSessionIds);
271
+ const next = new Set(payload.archivedSessionIds);
255
272
  const at = Date.now();
256
273
  for (const id of next)
257
274
  if (!previous.has(id))