dsh-plugin-tool-management 0.9.0 → 0.10.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 +323 -245
- package/README.md +212 -201
- package/README_EN.md +206 -199
- 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 +171 -111
- package/lib/client.js +6220 -5451
- package/lib/compat/probe.js +0 -5
- package/lib/context-inject.js +468 -39
- package/lib/history/bridge.js +7 -0
- package/lib/history/tombstone.js +1 -2
- package/lib/hub.js +3 -1
- package/lib/index.js +735 -95
- package/lib/mcp/override-blocks.js +10 -6
- package/lib/mcp/state-section.js +64 -21
- package/lib/rules/archive-engine.js +93 -18
- package/lib/rules/archive.js +12 -7
- package/lib/rules/service.js +195 -44
- package/lib/scene-prompt-sync.js +4 -12
- package/lib/skills/catalog.js +6 -9
- package/lib/skills/core.js +5 -4
- package/lib/skills/service.js +56 -2
- package/lib/subagents/catalog.js +31 -15
- package/lib/subagents/service.js +307 -34
- package/lib/subagents/tools.js +29 -4
- package/package.json +105 -102
- package/lib/history/projcache.js +0 -335
package/lib/subagents/tools.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// 深度探针(`subagentDepthOf`)与目录判据(`catalogInjectedAt`)都不再需要:
|
|
2
|
+
// 2026-09-17 方案 A 之后,这两个工具都不再看会话深度 —— 委派由官方决定(默认能嵌套到 3 层),
|
|
3
|
+
// `catalogDepth` 只管常驻目录注入到哪些会话。
|
|
1
4
|
export function text(v) {
|
|
2
5
|
return [{ type: 'text', text: v }];
|
|
3
6
|
}
|
|
@@ -19,7 +22,7 @@ export async function filterBySceneBinding(docs, enabledSceneLists) {
|
|
|
19
22
|
export function defineSubagentManagerListTool(subagents) {
|
|
20
23
|
return {
|
|
21
24
|
name: 'subagent_manager_list',
|
|
22
|
-
description: 'List available personas (pre-configured subagent profiles) with their descriptions.
|
|
25
|
+
description: 'List available personas (pre-configured subagent profiles) with their descriptions. The same catalog is injected into your context each turn (the「可委派的子智能体」system-reminder); call this tool before subagent_manager_run for the full, always-current list.',
|
|
23
26
|
parameters: {},
|
|
24
27
|
output: {
|
|
25
28
|
schema: { type: 'string' },
|
|
@@ -27,6 +30,10 @@ export function defineSubagentManagerListTool(subagents) {
|
|
|
27
30
|
},
|
|
28
31
|
async execute(_args, exec) {
|
|
29
32
|
const { allowed, reason } = await filterBySceneBinding(await subagents.list(), await subagents.sceneLists());
|
|
33
|
+
// 这里**不再按深度过滤**(2026-09-17 用户裁定,方案 A):`catalogDepth` 只决定常驻目录
|
|
34
|
+
// 注入到哪些会话,与"能不能委派"无关 —— 委派由官方决定(`dsh-tool-subagent` 默认能嵌套
|
|
35
|
+
// 到 3 层)。此前按深度过滤等于把可用人设藏起来:目录不注入时模型只能靠这个工具查,
|
|
36
|
+
// 而工具又不列全,结果是"明明能委派却查不到人设"。
|
|
30
37
|
const lines = allowed.map((p) => '- ' + p.name + ' — ' + (p.description || '(无描述)'));
|
|
31
38
|
let notice = '';
|
|
32
39
|
try {
|
|
@@ -42,10 +49,21 @@ export function defineSubagentManagerListTool(subagents) {
|
|
|
42
49
|
export function defineSubagentManagerRunTool(subagents) {
|
|
43
50
|
return {
|
|
44
51
|
name: 'subagent_manager_run',
|
|
45
|
-
|
|
52
|
+
// 描述按「是什么 → 两种模式 → 何时用(含与官方两个委派工具的分界)→ 何时改用别的」组织。
|
|
53
|
+
// 2026-09-17 补了**分界规则**与 `inherit`:此前上下文里虽然注入了人设目录与「用
|
|
54
|
+
// subagent_manager_run 执行」,但没有任何一句话说明它和官方 `subagent` / `subagent_fork`
|
|
55
|
+
// 何时该用谁 —— 本机实测(session-ee722e23)模型读完 README 后选了官方 `subagent_fork`
|
|
56
|
+
// (fork 能继承已读内容、不必复述)。现在人设通道也有 fork(`inherit`),分界只剩
|
|
57
|
+
// 「要不要后台跑」一件事,所以规则能写成"贴合人设的一律走这里"。
|
|
58
|
+
//
|
|
59
|
+
// 「何时不用」那一段保留(2026-09-17,用户采纳的四条里的第 6 条):参照 Claude Code 的
|
|
60
|
+
// Agent 工具(`AgentTool/prompt.ts:232-240`),把"不该用"写成**带替代工具**的具体清单
|
|
61
|
+
// (具体路径→Read;找定义→Grep/Glob),比笼统说"这个很贵"有用得多。
|
|
62
|
+
description: 'Run a named persona as a subagent: it gets the persona as its own system prompt, works on `task`, and returns only its final output.\n\nTwo modes: by default the child starts fresh — it cannot see this conversation, so `task` must be self-contained. With `inherit: true` the child is seeded with this conversation\'s finished turns (the same mechanism as the host\'s `subagent_fork`), so `task` only states what is new — use it for follow-ups on work already completed. Only **finished** turns are inherited: a delegation made during the current turn inherits nothing from that turn, so a mid-turn hand-off still needs a self-contained `task`.\n\nWrite `task` as the goal plus the context it needs — do not prescribe method or output format: those belong to the persona.\n\nWhen to use: work that matches one of the personas in the「可委派的子智能体」system-reminder injected into your context (or from subagent_manager_list) — a review, a focused investigation, a piece of writing — where the detail does not belong in your own context. Work that matches a persona belongs here, not in the host\'s `subagent` / `subagent_fork`: those take no persona. Use them only when no persona fits, or when you need a background run (this tool waits for the result).\n\nWhen NOT to use: reading a specific file (use Read), finding a definition (use Grep/Glob), or touching two or three files (use Read directly).',
|
|
46
63
|
parameters: {
|
|
47
64
|
agent: { type: 'string', required: true, description: 'Persona name from subagent_manager_list.' },
|
|
48
|
-
task: { type: 'string', required: true, description: 'Self-contained
|
|
65
|
+
task: { type: 'string', required: true, description: 'The task for the subagent: the goal plus the context it needs. Self-contained by default; with inherit: true it only needs to state what is new. Leave method and output format to the persona.' },
|
|
66
|
+
inherit: { type: 'boolean', description: 'Let the subagent inherit this conversation\'s finished turns, like the host\'s subagent_fork (default false = a fresh child that cannot see this conversation). Only finished turns are inherited — a delegation made mid-turn cannot pass the current turn\'s content, so write `task` as if it were self-contained.' },
|
|
49
67
|
},
|
|
50
68
|
output: {
|
|
51
69
|
schema: { type: 'string' },
|
|
@@ -54,6 +72,7 @@ export function defineSubagentManagerRunTool(subagents) {
|
|
|
54
72
|
async execute(args, exec) {
|
|
55
73
|
const name = String((args && args.agent) || '').trim();
|
|
56
74
|
const task = String((args && args.task) || '').trim();
|
|
75
|
+
const inherit = args && args.inherit === true;
|
|
57
76
|
if (!task)
|
|
58
77
|
throw new Error('task 不能为空');
|
|
59
78
|
// 官方 SubagentStartRequest.parent 必填:调用方 agent 缺失时给结构化错误,不把 undefined 透传下去。
|
|
@@ -66,12 +85,18 @@ export function defineSubagentManagerRunTool(subagents) {
|
|
|
66
85
|
const available = allowed.map((p) => p.name).join('、') || '(无)';
|
|
67
86
|
throw new Error('人设不可用: ' + name + (reason ? '(' + reason + ')' : '(可用: ' + available + ')'));
|
|
68
87
|
}
|
|
88
|
+
// 这里**刻意没有**"深度不够就拒绝"的检查(2026-09-17 用户裁定,方案 A)。
|
|
89
|
+
// 它此前基于一个错误假设:以为传 `maxDepth: 1` 会让子会话再委派必然失败。实际上官方
|
|
90
|
+
// `dsh-tool-subagent` 的默认是 **3**、provider 只在**传了值**时才校验,所以子代理本来
|
|
91
|
+
// 就能继续嵌套。那个检查的唯一净效果是让**我们的**工具比官方严 —— 同一个子会话里官方
|
|
92
|
+
// 工具能委派、我们的被自己拦下 —— 而用户真想禁止嵌套也禁止不了(官方工具照样能)。
|
|
93
|
+
// `catalogDepth` 现在只管"目录注入到哪些会话",不参与委派可行性判断。
|
|
69
94
|
// 按当前会话的 Agent 预设算工具限制:判断不了当前预设、或名单里的工具已经不存在时,
|
|
70
95
|
// 结论里会带一句实话,跟着结果一起返回——不静默改变限制的强度。
|
|
71
96
|
const decision = typeof subagents.toolFilterFor === 'function'
|
|
72
97
|
? await subagents.toolFilterFor(persona, exec.agent && exec.agent.ctx)
|
|
73
98
|
: undefined;
|
|
74
|
-
const r = await subagents.runSerial(exec.agent, persona, task, exec.signal, decision === undefined ? undefined : decision.filter);
|
|
99
|
+
const r = await subagents.runSerial(exec.agent, persona, task, exec.signal, decision === undefined ? undefined : decision.filter, inherit);
|
|
75
100
|
const prefix = r.stopReason && r.stopReason !== 'completed' ? `[stopReason: ${r.stopReason}]\n` : '';
|
|
76
101
|
const note = decision && decision.note ? `⚠ ${decision.note}\n\n` : '';
|
|
77
102
|
return note + prefix + r.text;
|
package/package.json
CHANGED
|
@@ -1,102 +1,105 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-plugin-tool-management",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
|
|
8
|
-
},
|
|
9
|
-
"type": "module",
|
|
10
|
-
"main": "lib/index.js",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": "./lib/index.js",
|
|
13
|
-
"./client": "./lib/client.js",
|
|
14
|
-
"./workspace": "./lib/history/workspace.js",
|
|
15
|
-
"./
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"build": "
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"check:
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"mcp",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"@deepseek-ai/
|
|
70
|
-
"@deepseek-ai/dsh-
|
|
71
|
-
"@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
|
|
72
|
-
"@deepseek-ai/dsh-
|
|
73
|
-
"@deepseek-ai/dsh-
|
|
74
|
-
"@deepseek-ai/dsh-
|
|
75
|
-
"@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2",
|
|
76
|
-
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
|
|
77
|
-
"@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2",
|
|
78
|
-
"react": "^18.2.0"
|
|
79
|
-
},
|
|
80
|
-
"peerDependenciesMeta": {
|
|
81
|
-
"@deepseek-ai/dsh-subagent-
|
|
82
|
-
"optional": true
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"@deepseek-ai/
|
|
90
|
-
"@deepseek-ai/dsh-
|
|
91
|
-
"@deepseek-ai/dsh-
|
|
92
|
-
"@deepseek-ai/dsh-
|
|
93
|
-
"@deepseek-ai/dsh-
|
|
94
|
-
"@deepseek-ai/dsh-
|
|
95
|
-
"@
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-tool-management",
|
|
3
|
+
"version": "0.10.0",
|
|
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
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "lib/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./lib/index.js",
|
|
13
|
+
"./client": "./lib/client.js",
|
|
14
|
+
"./workspace": "./lib/history/workspace.js",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"lib",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"README.md",
|
|
21
|
+
"README_EN.md",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"screenshots.json",
|
|
25
|
+
"docs"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"registry": "https://registry.npmjs.org",
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json && node scripts/sync-client.mjs",
|
|
36
|
+
"build:client": "node scripts/sync-client.mjs",
|
|
37
|
+
"prepublishOnly": "npm run build",
|
|
38
|
+
"lint": "node --check lib/client.js && node --check lib/index.js",
|
|
39
|
+
"check:i18n": "node scripts/check-i18n.mjs src/client.js",
|
|
40
|
+
"check:host": "node scripts/host-deps.mjs && node scripts/doctor.mjs",
|
|
41
|
+
"host-deps": "node scripts/host-deps.mjs --fix",
|
|
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 test/client-render.test.mjs",
|
|
44
|
+
"sync:profile": "node scripts/sync-profile.mjs"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"dsh",
|
|
48
|
+
"deepseek-harness",
|
|
49
|
+
"skill",
|
|
50
|
+
"skills",
|
|
51
|
+
"mcp",
|
|
52
|
+
"mcp-server",
|
|
53
|
+
"plugin",
|
|
54
|
+
"cordis"
|
|
55
|
+
],
|
|
56
|
+
"dsh": {
|
|
57
|
+
"bundle": {
|
|
58
|
+
"patch": "./cordis.patch.yml"
|
|
59
|
+
},
|
|
60
|
+
"client": {
|
|
61
|
+
"platform": "web",
|
|
62
|
+
"inject": [
|
|
63
|
+
"@deepseek-ai/dsh-client-locale"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
69
|
+
"@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-storage-domain": ">=0.1.5-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-subagent-fork-in-process": ">=0.1.5-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2",
|
|
78
|
+
"react": "^18.2.0"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"@deepseek-ai/dsh-subagent-fork-in-process": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"@deepseek-ai/dsh-subagent-spawn-in-process": {
|
|
85
|
+
"optional": true
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
90
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
91
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
92
|
+
"@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
|
|
93
|
+
"@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
|
|
94
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
|
|
95
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
96
|
+
"@deepseek-ai/dsh-user-approval": "^0.1.5-rc.2",
|
|
97
|
+
"@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
|
|
98
|
+
"@types/node": "^24.0.0",
|
|
99
|
+
"typescript": "^6.0.0"
|
|
100
|
+
},
|
|
101
|
+
"license": "MIT",
|
|
102
|
+
"dependencies": {
|
|
103
|
+
"fflate": "^0.8.3"
|
|
104
|
+
}
|
|
105
|
+
}
|
package/lib/history/projcache.js
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
-
import { SessionProjectionCache, checkpointIdentity, checkpointRecord, projectionCacheDomainSpec } from "@deepseek-ai/dsh-session-projection-cache";
|
|
4
|
-
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
5
|
-
import { trackTombstone } from "./tombstone.js";
|
|
6
|
-
/**
|
|
7
|
-
* 当前投影缓存写入独立的 v2 域,避免 DSH alpha.2 将包含冒号的逻辑会话 ID
|
|
8
|
-
* 直接映射为 Windows 文件名。旧名 v1 与旧名 v2 域只在启动时读取并补缺迁入,
|
|
9
|
-
* 迁移不删除原始缓存,任意中断都可在后续启动继续。
|
|
10
|
-
*/
|
|
11
|
-
const storedCheckpointRecord = checkpointRecord.extend({
|
|
12
|
-
sessionId: checkpointIdentity.shape.cwd.unwrap()
|
|
13
|
-
});
|
|
14
|
-
const safeDomainInput = {
|
|
15
|
-
// 旧版 DSH 不支持同名域的版本迁移。v2 改用新域名,确保升级时不会在
|
|
16
|
-
// 打开目标域之前因旧 v1 元数据而终止启动。
|
|
17
|
-
name: "session_projcache_archive_manager_v2",
|
|
18
|
-
version: 2,
|
|
19
|
-
tables: { sessions: domainTable(storedCheckpointRecord) },
|
|
20
|
-
...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
|
|
21
|
-
};
|
|
22
|
-
const safeProjectionCacheDomainSpec = defineDomain(safeDomainInput);
|
|
23
|
-
/** The last official whole-file cache format used by DSH rc.2. */
|
|
24
|
-
const legacyOptionalIdentityFields = {};
|
|
25
|
-
for (const field of ["isSeeded", "inheritedEventCount"]) {
|
|
26
|
-
// rc.2 的身份 schema 尚未定义这些字段;向 Zod 传入不存在的字段会在
|
|
27
|
-
// 首次解析历史记录时抛错,因此只能按当前运行时实际的 schema 构造掩码。
|
|
28
|
-
if (Object.hasOwn(checkpointIdentity.shape, field))
|
|
29
|
-
legacyOptionalIdentityFields[field] = true;
|
|
30
|
-
}
|
|
31
|
-
const legacyCheckpointRecord = checkpointRecord.extend({
|
|
32
|
-
identity: checkpointIdentity.partial(legacyOptionalIdentityFields)
|
|
33
|
-
});
|
|
34
|
-
const legacyStoredCheckpointRecord = legacyCheckpointRecord.extend({
|
|
35
|
-
sessionId: checkpointIdentity.shape.cwd.unwrap()
|
|
36
|
-
});
|
|
37
|
-
/** v0.1.24 短暂使用过的旧名 v2 安全缓存域。 */
|
|
38
|
-
const legacySafeV2ProjectionCacheDomainSpec = defineDomain({
|
|
39
|
-
name: "session_projcache_archive_manager",
|
|
40
|
-
version: 2,
|
|
41
|
-
tables: { sessions: domainTable(storedCheckpointRecord) },
|
|
42
|
-
...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
|
|
43
|
-
});
|
|
44
|
-
/** v0.1.23 及以前归档管理器自身使用的安全缓存域。 */
|
|
45
|
-
const legacySafeProjectionCacheDomainSpec = defineDomain({
|
|
46
|
-
name: "session_projcache_archive_manager",
|
|
47
|
-
version: 1,
|
|
48
|
-
tables: { sessions: domainTable(legacyStoredCheckpointRecord) },
|
|
49
|
-
...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
|
|
50
|
-
});
|
|
51
|
-
const legacyProjectionCacheDomainSpec = defineDomain({
|
|
52
|
-
name: "session_projcache",
|
|
53
|
-
version: 3,
|
|
54
|
-
tables: { sessions: domainTable(legacyCheckpointRecord) }
|
|
55
|
-
});
|
|
56
|
-
function projectionCacheStorageKey(sessionId) {
|
|
57
|
-
return `session_${createHash("sha256").update(sessionId, "utf8").digest("base64url")}`;
|
|
58
|
-
}
|
|
59
|
-
function unwrapStoredCheckpoint(stored) {
|
|
60
|
-
return { identity: stored.identity, rows: stored.rows };
|
|
61
|
-
}
|
|
62
|
-
/** Map the Session-id table contract onto path-safe physical keys. */
|
|
63
|
-
class SafeSessionTable {
|
|
64
|
-
constructor(table) {
|
|
65
|
-
this.table = table;
|
|
66
|
-
}
|
|
67
|
-
get size() {
|
|
68
|
-
return [...this.keys()].length;
|
|
69
|
-
}
|
|
70
|
-
get(sessionId) {
|
|
71
|
-
const stored = this.table.get(projectionCacheStorageKey(sessionId));
|
|
72
|
-
if (stored === void 0 || stored.sessionId !== sessionId)
|
|
73
|
-
return void 0;
|
|
74
|
-
return unwrapStoredCheckpoint(stored);
|
|
75
|
-
}
|
|
76
|
-
has(sessionId) {
|
|
77
|
-
return this.get(sessionId) !== void 0;
|
|
78
|
-
}
|
|
79
|
-
*entries() {
|
|
80
|
-
for (const [key, stored] of this.table.entries()) {
|
|
81
|
-
if (projectionCacheStorageKey(stored.sessionId) !== key)
|
|
82
|
-
continue;
|
|
83
|
-
yield [stored.sessionId, unwrapStoredCheckpoint(stored)];
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
*keys() {
|
|
87
|
-
for (const [sessionId] of this.entries())
|
|
88
|
-
yield sessionId;
|
|
89
|
-
}
|
|
90
|
-
*values() {
|
|
91
|
-
for (const [, value] of this.entries())
|
|
92
|
-
yield value;
|
|
93
|
-
}
|
|
94
|
-
async put(sessionId, record) {
|
|
95
|
-
const key = projectionCacheStorageKey(sessionId);
|
|
96
|
-
const existing = this.table.get(key);
|
|
97
|
-
if (existing !== void 0 && existing.sessionId !== sessionId) {
|
|
98
|
-
throw new Error(`projection-cache storage-key collision for "${sessionId}"`);
|
|
99
|
-
}
|
|
100
|
-
await this.table.put(key, { sessionId, identity: record.identity, rows: record.rows });
|
|
101
|
-
}
|
|
102
|
-
async delete(sessionId) {
|
|
103
|
-
const key = projectionCacheStorageKey(sessionId);
|
|
104
|
-
const existing = this.table.get(key);
|
|
105
|
-
if (existing === void 0 || existing.sessionId !== sessionId)
|
|
106
|
-
return false;
|
|
107
|
-
return this.table.delete(key);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
async function readSourceRecords(ctx, spec, label) {
|
|
111
|
-
let domain;
|
|
112
|
-
try {
|
|
113
|
-
domain = await ctx.storageDomain.open(spec);
|
|
114
|
-
return { opened: true, records: [...domain.table("sessions").entries()] };
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
ctx.logger.warn(`archive-manager projcache: ${label} cache import skipped: ${String(error)}`);
|
|
118
|
-
return { opened: false, records: [] };
|
|
119
|
-
}
|
|
120
|
-
finally {
|
|
121
|
-
try {
|
|
122
|
-
await domain?.close();
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
ctx.logger.warn(`archive-manager projcache: ${label} cache close failed: ${String(error)}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
async function readLegacySafeRecords(ctx, spec, label) {
|
|
130
|
-
const source = await readSourceRecords(ctx, spec, label);
|
|
131
|
-
const records = [];
|
|
132
|
-
for (const [physicalKey, stored] of source.records) {
|
|
133
|
-
if (stored === null || typeof stored !== "object" || typeof stored.sessionId !== "string") {
|
|
134
|
-
ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its session id is invalid`);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (projectionCacheStorageKey(stored.sessionId) !== physicalKey) {
|
|
138
|
-
ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its storage key does not match the session id`);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
records.push([stored.sessionId, unwrapStoredCheckpoint(stored)]);
|
|
142
|
-
}
|
|
143
|
-
return { ...source, records };
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* 旧版记录没有完整身份时,只能安全补齐未播种会话的零继承切点。
|
|
147
|
-
* 播种会话无法从头部反推出精确切点,宁可跳过并按需重建。
|
|
148
|
-
*/
|
|
149
|
-
function normalizeLegacyRecord(record) {
|
|
150
|
-
if (record === null || typeof record !== "object" || record.identity === null || typeof record.identity !== "object")
|
|
151
|
-
return void 0;
|
|
152
|
-
const identity = record.identity;
|
|
153
|
-
const inheritedEventCount = identity.inheritedEventCount;
|
|
154
|
-
const validOffset = Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0;
|
|
155
|
-
if (identity.isSeeded === true) {
|
|
156
|
-
if (!validOffset)
|
|
157
|
-
return void 0;
|
|
158
|
-
return record;
|
|
159
|
-
}
|
|
160
|
-
if (identity.isSeeded !== void 0 && identity.isSeeded !== false)
|
|
161
|
-
return void 0;
|
|
162
|
-
if (inheritedEventCount !== void 0 && inheritedEventCount !== 0)
|
|
163
|
-
return void 0;
|
|
164
|
-
return {
|
|
165
|
-
...record,
|
|
166
|
-
identity: {
|
|
167
|
-
...identity,
|
|
168
|
-
isSeeded: false,
|
|
169
|
-
inheritedEventCount: 0
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
async function importMissingRecords(ctx, target, records, label) {
|
|
174
|
-
let imported = 0;
|
|
175
|
-
for (const [sessionId, record] of records) {
|
|
176
|
-
if (target.has(sessionId))
|
|
177
|
-
continue;
|
|
178
|
-
try {
|
|
179
|
-
const normalized = normalizeLegacyRecord(record);
|
|
180
|
-
if (normalized === void 0) {
|
|
181
|
-
ctx.logger.warn(`archive-manager projcache: ${label} row "${sessionId}" import skipped because its lifecycle identity is incomplete`);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
await target.put(sessionId, normalized);
|
|
185
|
-
imported += 1;
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
ctx.logger.warn(`archive-manager projcache: ${label} row "${sessionId}" import failed: ${String(error)}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return imported;
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* 合并归档管理器旧名 v2、旧名 v1、官方新版逐会话与旧版整文件缓存。目标域记录
|
|
195
|
-
* 最高优先,随后按新旧顺序只补缺失记录;任一来源失败时仍继续尝试另一个来源。
|
|
196
|
-
*/
|
|
197
|
-
async function importPreviousProjectionCache(ctx, target) {
|
|
198
|
-
const legacySafeV2 = await readLegacySafeRecords(ctx, legacySafeV2ProjectionCacheDomainSpec, "archive-manager v2");
|
|
199
|
-
const legacySafe = await readLegacySafeRecords(ctx, legacySafeProjectionCacheDomainSpec, "archive-manager v1");
|
|
200
|
-
const legacy = await readSourceRecords(ctx, legacyProjectionCacheDomainSpec, "legacy v3");
|
|
201
|
-
const sameSpec = projectionCacheDomainSpec.version === legacyProjectionCacheDomainSpec.version
|
|
202
|
-
&& projectionCacheDomainSpec.layout === legacyProjectionCacheDomainSpec.layout;
|
|
203
|
-
let imported = 0;
|
|
204
|
-
if (legacySafeV2.opened)
|
|
205
|
-
imported += await importMissingRecords(ctx, target, legacySafeV2.records, "archive-manager v2");
|
|
206
|
-
if (legacySafe.opened)
|
|
207
|
-
imported += await importMissingRecords(ctx, target, legacySafe.records, "archive-manager v1");
|
|
208
|
-
if (!sameSpec) {
|
|
209
|
-
const current = await readSourceRecords(ctx, projectionCacheDomainSpec, "current");
|
|
210
|
-
if (current.opened)
|
|
211
|
-
imported += await importMissingRecords(ctx, target, current.records, "current");
|
|
212
|
-
}
|
|
213
|
-
if (legacy.opened)
|
|
214
|
-
imported += await importMissingRecords(ctx, target, legacy.records, "legacy v3");
|
|
215
|
-
return imported;
|
|
216
|
-
}
|
|
217
|
-
//#region lib/types/index.js
|
|
218
|
-
/**
|
|
219
|
-
* dsh-archive-manager projcache 半边。
|
|
220
|
-
*
|
|
221
|
-
* `ArchiveProjectionCache` 继承上游 `SessionProjectionCache`,保留服务名
|
|
222
|
-
* `sessionProjectionCache` 与 fail-soft 写路径,但写入独立、路径安全的
|
|
223
|
-
* `session_projcache_archive_manager_v2` 域。旧名 v1 与 v2 域只在启动时
|
|
224
|
-
* 读取并补缺迁入;归档管理的
|
|
225
|
-
* `deleteSession` 增加三处守护:
|
|
226
|
-
*
|
|
227
|
-
* - `delete(id)` - 永久移除兼容域中该会话经 SHA-256 映射的缓存投影行,
|
|
228
|
-
* 写入前先登记墓碑。
|
|
229
|
-
* - `whenIdle()` - 全部在途公开写入落定后 resolve。
|
|
230
|
-
* - 墓碑挡住已删除会话的写入:迟到的写入要么被直接拦截,要么在
|
|
231
|
-
* 落定后补删自己的残留行,不会复活已删除的缓存条目。守护覆盖
|
|
232
|
-
* `write()`(dispose 写后路径)与 `put()`(rc.2 的 `putSoft` 最终调用
|
|
233
|
-
* 它,alpha.2 的 `coldSnapshot` 则直接调用它),两代落盘路径都纳入
|
|
234
|
-
* `whenIdle` 跟踪。
|
|
235
|
-
*
|
|
236
|
-
* 默认导出是 Service 子类(与上游 `@deepseek-ai/dsh-session-projection-cache`
|
|
237
|
-
* 包同形),profile 补丁可直接替换 `session-projection-cache` 服务行,
|
|
238
|
-
* 无需其他接线改动。
|
|
239
|
-
*/
|
|
240
|
-
var ArchiveProjectionCache = class extends SessionProjectionCache {
|
|
241
|
-
/** 已永久删除的会话:其投影缓存行不再允许写入。 */
|
|
242
|
-
deletedSessionIds = /* @__PURE__ */ new Set();
|
|
243
|
-
/** 墓碑插入顺序,用于在上限处淘汰最旧项。 */
|
|
244
|
-
deletedSessionOrder = [];
|
|
245
|
-
deletedSessionTombstoneLimit = 4096;
|
|
246
|
-
/** 在途写入的队尾(只含已落定的 promise)。 */
|
|
247
|
-
writeTail = Promise.resolve();
|
|
248
|
-
constructor(ctx, config) {
|
|
249
|
-
super(ctx, config);
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Replace the official cache domain with the path-safe compatibility
|
|
253
|
-
* domain, then preserve the upstream listener/write implementation.
|
|
254
|
-
*/
|
|
255
|
-
async [Service.init]() {
|
|
256
|
-
const domain = await this.ctx.storageDomain.open(safeProjectionCacheDomainSpec);
|
|
257
|
-
this.ctx.effect(() => () => domain.close(), "sessionProjectionCache.domainClose");
|
|
258
|
-
const table = new SafeSessionTable(domain.table("sessions"));
|
|
259
|
-
this.table = table;
|
|
260
|
-
await importPreviousProjectionCache(this.ctx, table);
|
|
261
|
-
this.installWritePath();
|
|
262
|
-
}
|
|
263
|
-
/** 跟踪公开写入路径,避免依赖上游私有 `flushSoft` 的实现细节。 */
|
|
264
|
-
write(session) {
|
|
265
|
-
const task = this.writeCore(session);
|
|
266
|
-
this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
|
|
267
|
-
return task;
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* 墓碑正确性依赖:super.write(session) 一次整体写入,返回后不再有后续异步落盘。
|
|
271
|
-
* 若上游改成多阶段异步,(C) 补删会漏掉后续写入,deletedSessionIds 挡不住复活。
|
|
272
|
-
*/
|
|
273
|
-
async writeCore(session) {
|
|
274
|
-
if (this.deletedSessionIds.has(session.id))
|
|
275
|
-
return;
|
|
276
|
-
await super.write(session);
|
|
277
|
-
if (this.deletedSessionIds.has(session.id))
|
|
278
|
-
await this.requireTable().delete(session.id);
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* 守住所有底层写入。rc.2 的冷读经 `putSoft -> put`,alpha.2 则
|
|
282
|
-
* 直接经 `put`;把墓碑与 whenIdle 跟踪放在这里可同时兼容两代实现。
|
|
283
|
-
*/
|
|
284
|
-
put(id, identity, rows) {
|
|
285
|
-
// alpha.2 coldSnapshot unconditionally chains `.catch()` onto put().
|
|
286
|
-
// A blocked write must therefore preserve the upstream Promise contract.
|
|
287
|
-
if (this.deletedSessionIds.has(id))
|
|
288
|
-
return Promise.resolve();
|
|
289
|
-
const task = this.putCore(id, identity, rows);
|
|
290
|
-
this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
|
|
291
|
-
return task;
|
|
292
|
-
}
|
|
293
|
-
async putCore(id, identity, rows) {
|
|
294
|
-
await super.put(id, identity, rows);
|
|
295
|
-
if (this.deletedSessionIds.has(id))
|
|
296
|
-
await this.requireTable().delete(id);
|
|
297
|
-
}
|
|
298
|
-
/** rc.2 公开的 fail-soft 辅助在 alpha.2 已移除;保留同形兼容入口。 */
|
|
299
|
-
async putSoft(id, identity, rows, what) {
|
|
300
|
-
const upstream = SessionProjectionCache.prototype.putSoft;
|
|
301
|
-
if (typeof upstream === "function")
|
|
302
|
-
return upstream.call(this, id, identity, rows, what);
|
|
303
|
-
try {
|
|
304
|
-
await this.put(id, identity, rows);
|
|
305
|
-
}
|
|
306
|
-
catch (error) {
|
|
307
|
-
this.ctx.logger.warn(`session projection cache: ${what} for "${id}" failed (cache stays stale): ${String(error)}`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* 全部被跟踪的在途写入落定后 resolve(含失败)。空闲缓存上调用立即返回。
|
|
312
|
-
* @returns 跟踪写入落定后的 resolution。
|
|
313
|
-
*/
|
|
314
|
-
whenIdle() {
|
|
315
|
-
return this.writeTail;
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* 永久移除一个会话的缓存投影行。
|
|
319
|
-
* @param id - 要删除缓存行的会话。
|
|
320
|
-
* @returns 行删除完成后的 resolution。
|
|
321
|
-
*/
|
|
322
|
-
async delete(id) {
|
|
323
|
-
trackTombstone(this.deletedSessionIds, this.deletedSessionOrder, id, this.deletedSessionTombstoneLimit);
|
|
324
|
-
await this.requireTable().delete(id);
|
|
325
|
-
}
|
|
326
|
-
/** 撤销墓碑(供测试与同 id 新生命周期复用路径使用)。 */
|
|
327
|
-
clearTombstone(id) {
|
|
328
|
-
this.deletedSessionIds.delete(id);
|
|
329
|
-
const idx = this.deletedSessionOrder.indexOf(id);
|
|
330
|
-
if (idx !== -1)
|
|
331
|
-
this.deletedSessionOrder.splice(idx, 1);
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
//#endregion
|
|
335
|
-
export { ArchiveProjectionCache, ArchiveProjectionCache as default, SafeSessionTable, importPreviousProjectionCache, legacyProjectionCacheDomainSpec, legacySafeProjectionCacheDomainSpec, legacySafeV2ProjectionCacheDomainSpec, projectionCacheStorageKey, safeProjectionCacheDomainSpec };
|