dsh-subagent-profile 0.3.2 → 0.3.4

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 (40) hide show
  1. package/README.md +77 -40
  2. package/README.zh.md +111 -74
  3. package/docs/screenshots/dispatch-card.png +0 -0
  4. package/docs/screenshots/settings-page1.png +0 -0
  5. package/docs/screenshots/settings-page2.png +0 -0
  6. package/index.mjs +276 -81
  7. package/lib/client.js +3218 -166
  8. package/lib/core/adoption-reminder.mjs +48 -0
  9. package/lib/core/adoption-tracker.mjs +430 -0
  10. package/lib/core/background-ledger.mjs +71 -0
  11. package/lib/core/catalog-cache.mjs +45 -7
  12. package/lib/core/catalog.mjs +6 -6
  13. package/lib/core/cost-evidence.mjs +145 -0
  14. package/lib/core/cost-guard.mjs +71 -44
  15. package/lib/core/decision-trace.mjs +413 -0
  16. package/lib/core/delegation.mjs +111 -50
  17. package/lib/core/dispatch-gates.mjs +153 -0
  18. package/lib/core/dispatch-guard.mjs +156 -0
  19. package/lib/core/dispatch-schema.mjs +103 -14
  20. package/lib/core/dispatch-tool.mjs +220 -204
  21. package/lib/core/draft-gates.mjs +45 -0
  22. package/lib/core/drafts-store.mjs +45 -0
  23. package/lib/core/escape.mjs +130 -0
  24. package/lib/core/evolution-advice.mjs +224 -0
  25. package/lib/core/evolution-ledger.mjs +300 -0
  26. package/lib/core/evolution-summary.mjs +255 -0
  27. package/lib/core/http-routes.mjs +256 -72
  28. package/lib/core/intersection.mjs +6 -9
  29. package/lib/core/presets-sync.mjs +161 -43
  30. package/lib/core/prices.mjs +46 -0
  31. package/lib/core/profile-directory.mjs +139 -0
  32. package/lib/core/profile-provider.mjs +42 -39
  33. package/lib/core/profiles-store.mjs +103 -76
  34. package/lib/core/pure.mjs +110 -66
  35. package/lib/core/reminder-store.mjs +172 -0
  36. package/lib/core/shims.mjs +67 -76
  37. package/lib/core/whitelist.mjs +23 -17
  38. package/package.json +82 -83
  39. package/presets/orchestrator/agent.cordis.yml +59 -87
  40. package/presets/orchestrator/NOTICE +0 -3
@@ -1,26 +1,25 @@
1
- // lib/core/profiles-store.mjs — profile registry store and enable/disable switch
2
- // (moved verbatim from index.mjs and refactored into a factory; import-free
3
- // node builtins + lib/core/pure.mjs only, no @deepseek-ai dependency).
1
+ // lib/core/profiles-store.mjs — profile 注册表 store 与启用/禁用开关
2
+ // (从 index.mjs 逐字移出并重构为工厂;import-free——
3
+ // node 内置 + lib/core/pure.mjs,无 @deepseek-ai 依赖)。
4
4
  //
5
- // `dshHome()` and `BUILTIN_SEEDS` are module-level exports shared by the
6
- // factory and by lib/core/http-routes.mjs (the settings HTTP handlers look up seeds). The
7
- // per-apply store is constructed by `createProfileStore({ dshHome, logger })`,
8
- // replacing the apply-closure singleton: every apply() call gets its own
9
- // profiles Map / deletedBuiltins Set / allowFailOpen flag, exactly like the
10
- // original closure state.
5
+ // `dshHome()` `BUILTIN_SEEDS` 是模块级导出,工厂与 lib/core/http-routes.mjs
6
+ // (设置 HTTP 处理器查 seeds)共享。per-apply store
7
+ // `createProfileStore({ dshHome, logger })` 构造,取代 apply 闭包单例:
8
+ // 每次 apply() 调用都有自己的 profiles Map / deletedBuiltins Set /
9
+ // allowFailOpen 标志,与原始闭包状态完全一致。
11
10
  //
12
- // 工厂内嵌的 loadProfiles/persistProfiles/loadEnabled/persistEnabled/
13
- // resolveProfile 按行门抽为模块级函数,per-apply 状态收集进 `state` 对象
14
- // 注入;返回面与拆分前逐字一致。
11
+ // 工厂内嵌的 loadProfiles/persistProfiles/resolveProfile 按行门抽为模块级函数,
12
+ // per-apply 状态收集进 `state` 对象注入;开关态(enabled / evolutionAdvice)
13
+ // 经 loadState/persistState 读写共享 state.json。
15
14
 
16
15
  import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
17
16
  import { homedir } from 'node:os';
18
17
  import { join } from 'node:path';
19
18
  import { sanitizeProfile } from './pure.mjs';
20
19
 
21
- // Resolve the DSH home directory (env override wins, platform fallback) — the
22
- // same policy as the dsh-persona-ref bundle: user profiles persist to
23
- // ~/.dsh/subagent-profiles.json, stable across harness working directories.
20
+ // 解析 DSH 主目录(env 覆盖优先,平台回退)——与 dsh-persona-ref bundle 相同
21
+ // 策略:用户 profile 持久化到 ~/.dsh/subagent-profiles.json,跨 harness 工作目录
22
+ // 稳定。
24
23
  export function dshHome() {
25
24
  const raw = process.env.DSH_HOME;
26
25
  if (typeof raw === 'string' && raw.trim() !== '') {
@@ -32,15 +31,13 @@ export function dshHome() {
32
31
  return join(homedir(), '.dsh');
33
32
  }
34
33
 
35
- // 1. Profile registry (per-instance state; a bundle row is process-level, so
36
- // this Map is the singleton store, exactly like the dynamic plugin's).
37
- // Builtin seeds carry `builtin: true` so reset/remove can identify them and
38
- // a modified builtin stays distinguishable from a pure user profile.
39
- // Descriptions are semantic: one-line positioning + when to use, to help
40
- // the model choose. reasoningEffort levels verified against the
41
- // llm-deepseek adapter (off/high/max; see resolveModel gating on
42
- // connection.defaults.thinking — this deployment leaves thinking unset,
43
- // so the full set is advertised for deepseek-v4-flash).
34
+ // 1. Profile 注册表(per-instance 状态;bundle 行是进程级,故本 Map 是单例
35
+ // store,与动态插件完全一致)。内置种子带 `builtin: true`,reset/remove
36
+ // 识别它们,改过的内置方案与纯用户 profile 可区分。
37
+ // description 是语义化的:一行定位 + 何时用,帮助模型选择。reasoningEffort
38
+ // 档位经 llm-deepseek adapter 核验(off/high/max;见 resolveModel
39
+ // connection.defaults.thinking 的门控——本部署未设 thinking,
40
+ // 故对 deepseek-v4-flash 全档位宣传)。
44
41
  export const BUILTIN_SEEDS = [
45
42
  { id: 'swap-standard', name: '标准编码', description: '切换到 standard 预设的完整编码工具集。当父会话不是 standard、但子任务需要完整编码能力时用。', preset: 'standard', tokenTier: 'balanced', builtin: true },
46
43
  { id: 'researcher', name: '调研检索', description: '关闭深度推理省 token,继承父工具。适合查资料、汇总、背景调研,不适合改代码。', reasoningEffort: 'off', persona: 'You are a research subagent: search, read, and summarize only. Do not modify code or files.', tokenTier: 'cheap', builtin: true }
@@ -50,14 +47,14 @@ export const BUILTIN_SEEDS = [
50
47
 
51
48
  function resolveProfile(profiles, id) {
52
49
  const found = profiles.get(id);
53
- if (found === undefined) throw new Error(`dispatch: unknown profile "${id}"`);
54
- if (found.enabled === false) throw new Error(`dispatch: profile "${id}" is disabled`);
50
+ if (found === undefined) throw new Error(`dispatch: 未知 profile "${id}"(可在设置页的 profile 列表确认可用的 id)`);
51
+ if (found.enabled === false) throw new Error(`dispatch: profile "${id}" 已禁用(可在设置页重新启用该 profile)`);
55
52
  return found;
56
53
  }
57
54
 
58
- // Two file shapes: v1 = a bare array; v2 = { version:2,
59
- // profiles:[...], allowFailOpen:<bool> }. Returns { entries, version } or
60
- // { entries: undefined } for an unrecognized shape.
55
+ // 两种文件形状:v1 = 裸数组;v2 = { version:2, profiles:[...],
56
+ // allowFailOpen:<bool> }。返回 { entries, version };无法识别的形状返回
57
+ // { entries: undefined }
61
58
  function classifyProfileFile(parsed) {
62
59
  if (Array.isArray(parsed)) return { entries: parsed, version: 1 };
63
60
  if (parsed !== null && typeof parsed === 'object' && Array.isArray(parsed.profiles)) {
@@ -66,10 +63,9 @@ function classifyProfileFile(parsed) {
66
63
  return { entries: undefined, version: undefined };
67
64
  }
68
65
 
69
- // Per-entry sanitize (strict=false): over-limit fields are dropped (field
70
- // removed) + warned; an over-length persona is KEPT + warned (never silently
71
- // truncated). A bad entry is skipped (fail-soft). Returns
72
- // 'loaded' | 'skipped' | 'deleted' so loadProfiles can keep its counters.
66
+ // 逐条净化(strict=false):超限字段被丢弃(字段移除)+ 告警;超长 persona
67
+ // 保留 + 告警(绝不静默截断)。坏条目跳过(fail-soft)。返回
68
+ // 'loaded' | 'skipped' | 'deleted',loadProfiles 据此维护计数。
73
69
  function applyProfileEntry(raw, profiles, deletedBuiltins, logger) {
74
70
  if (!raw || typeof raw !== 'object' || typeof raw.id !== 'string' || raw.id.length === 0) {
75
71
  logger.warn('[dsh-subagent-profile] skipping malformed profile entry:', raw === null ? String(raw) : typeof raw);
@@ -95,13 +91,11 @@ function applyProfileEntry(raw, profiles, deletedBuiltins, logger) {
95
91
  return 'loaded';
96
92
  }
97
93
 
98
- // 1b. User profile persistence. The settings service cannot serve this
99
- // plugin (its write path hard-requires register(ns, schema) + a schemastery
100
- // schema), so user profiles are persisted to ~/.dsh/subagent-profiles.json
101
- // through node:fs a bundle has node globals, unlike the dynamic-plugin
102
- // sandbox that needed the optional `fs` service. Loaded once at startup; the
103
- // add/remove HTTP routes rewrite the file. Persistence is an enhancement, not
104
- // a hard dependency: any failure only warns and the builtin seeds work.
94
+ // 1b. 用户 profile 持久化。设置服务服务不了本插件(其写路径硬性要求
95
+ // register(ns, schema) + schemastery schema),故用户 profile 经 node:fs
96
+ // 持久化到 ~/.dsh/subagent-profiles.json——bundle 有 node 全局,不像需要可选
97
+ // `fs` 服务的动态插件沙箱。启动时加载一次;add/remove HTTP 路由重写文件。
98
+ // 持久化是增强而非硬依赖:任何失败只告警,内置种子照常工作。
105
99
  function loadProfiles(state, logger) {
106
100
  if (!existsSync(state.profilesFile)) return;
107
101
  try {
@@ -112,7 +106,7 @@ function loadProfiles(state, logger) {
112
106
  return;
113
107
  }
114
108
  if (version !== 2) {
115
- // v1 (or an unversioned array): migrate in memory, keep fail-open compat.
109
+ // v1(或无版本数组):内存迁移,保持 fail-open 兼容。
116
110
  state.allowFailOpen = true;
117
111
  logger.warn('[dsh-subagent-profile] v1 数据:fail-open 兼容模式');
118
112
  } else {
@@ -127,8 +121,8 @@ function loadProfiles(state, logger) {
127
121
  if (status === 'loaded') loaded++;
128
122
  else if (status === 'skipped') skipped++;
129
123
  }
130
- // Silent success: report how many persisted profiles came in (skipped
131
- // when none — a missing/empty file is the normal first boot).
124
+ // 静默成功:报告载入了多少持久化 profile(没有时跳过——缺失/空文件是
125
+ // 正常首启)。
132
126
  if (loaded > 0) logger.info(`[dsh-subagent-profile] loaded ${loaded} persisted profile(s)`);
133
127
  if (skipped > 0) logger.warn(`[dsh-subagent-profile] skipped ${skipped} malformed profile entry(ies)`);
134
128
  } catch (error) {
@@ -137,13 +131,11 @@ function loadProfiles(state, logger) {
137
131
  }
138
132
 
139
133
  /**
140
- * Persist every `persisted: true` profile plus builtin-delete tombstones.
141
- * Atomic write: write `<profilesFile>.tmp` in the same directory,
142
- * then `renameSync` over the target (a crash leaves the old file intact, never
143
- * a truncated one). Fail-visible: a failure does NOT throw and does NOT
144
- * roll back the in-memory `profiles` Map — it returns `{ persisted: false }` so
145
- * the caller can signal "已保存但未持久化" while the in-memory state keeps
146
- * driving this process. Always writes the v2 envelope shape.
134
+ * 持久化每个 `persisted: true` profile 加内置删除墓碑。
135
+ * 原子写:同目录写 `<profilesFile>.tmp`,再 `renameSync` 覆盖目标
136
+ * (崩溃只留下旧文件完好,绝不留下截断文件)。失败可见:失败不抛、不
137
+ * 回滚内存 `profiles` Map——返回 `{ persisted: false }`,调用方可以提示
138
+ * 「已保存但未持久化」,内存态继续驱动本进程。恒写 v2 信封形状。
147
139
  */
148
140
  function persistProfiles(state, logger) {
149
141
  const entries = [];
@@ -166,42 +158,67 @@ function persistProfiles(state, logger) {
166
158
  renameSync(tmp, state.profilesFile);
167
159
  return { persisted: true };
168
160
  } catch (error) {
169
- // Best-effort cleanup of the partial tmp file (rename never ran).
161
+ // 尽力清理残留的 tmp 文件(rename 未执行)。
170
162
  try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
171
163
  logger.warn('[dsh-subagent-profile] persisted profile write failed:', error instanceof Error ? error.message : String(error));
172
164
  return { persisted: false, error: error instanceof Error ? error.message : String(error) };
173
165
  }
174
166
  }
175
167
 
176
- // The plugin's enable/disable switch. Persisted beside the profile list so a
177
- // user can turn the dispatch tool off without uninstalling the bundle. Default
178
- // is enabled; a missing/unreadable file falls back to enabled.
179
- function loadEnabled(state) {
168
+ // 插件开关态(state.json):enabled(默认开,禁用即关派发工具)+ evolutionAdvice
169
+ // (派发优化建议注入,默认关,需显式开)+ escapeEnabled(逃生舱放行,默认关——信任
170
+ // 底板,仅设置页显式 opt-in 可放开)。三开关共用同一 state.json,读写整份对象
171
+ // 以互不覆盖;缺失文件回退默认值(全新部署),损坏文件 fail-closed(三开关全关,
172
+ // 绝不静默回退 enabled:true —— 安全禁用会被撤销),内存态仍驱动本进程。
173
+ const STATE_DEFAULTS = { enabled: true, evolutionAdvice: false, escapeEnabled: false };
174
+ const STATE_FAIL_CLOSED = { enabled: false, evolutionAdvice: false, escapeEnabled: false };
175
+
176
+ function loadState(state) {
177
+ if (!existsSync(state.stateFile)) return { ...STATE_DEFAULTS };
180
178
  try {
181
- if (existsSync(state.stateFile)) {
182
- const parsed = JSON.parse(readFileSync(state.stateFile, 'utf8'));
183
- return parsed && parsed.enabled !== false;
179
+ const parsed = JSON.parse(readFileSync(state.stateFile, 'utf8'));
180
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('state 文件不是对象');
181
+ return {
182
+ enabled: parsed.enabled !== false,
183
+ evolutionAdvice: parsed.evolutionAdvice === true,
184
+ escapeEnabled: parsed.escapeEnabled === true,
185
+ };
186
+ } catch (error) {
187
+ // fail-closed(S2):损坏的 state.json 若回退 enabled:true,会把「安全禁用」静默撤销。
188
+ // 三开关一律关(派发/建议/逃生舱全停),显式 warn;下次持久化写入即自愈。
189
+ if (state.logger !== undefined && typeof state.logger.warn === 'function') {
190
+ state.logger.warn('[dsh-subagent-profile] state.json 损坏,按 fail-closed 处理(三开关全关):', error instanceof Error ? error.message : String(error));
184
191
  }
185
- } catch {
186
- // fall through to enabled
192
+ return { ...STATE_FAIL_CLOSED };
187
193
  }
188
- return true;
189
194
  }
190
195
 
191
- function persistEnabled(state, enabled) {
196
+ // 原子写 state.json(tmp+rename,与 persistProfiles 同口径,修复「全库唯一非原子写」)。
197
+ // 返回 { persisted: true } 或 { persisted: false, error },供开关路由回传 persisted 信号;
198
+ // 写失败仍触发治理审计钩子(不抛),内存态照常驱动本进程。
199
+ function persistState(state, patch, onGovernanceFailure) {
200
+ const next = { ...loadState(state), ...patch };
201
+ const tmp = `${state.stateFile}.tmp`;
192
202
  try {
193
- writeFileSync(state.stateFile, JSON.stringify({ enabled }, null, 2), 'utf8');
194
- } catch {
195
- // best effort the in-memory state still drives this process
203
+ writeFileSync(tmp, JSON.stringify(next, null, 2), 'utf8');
204
+ renameSync(tmp, state.stateFile);
205
+ return { persisted: true };
206
+ } catch (error) {
207
+ try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
208
+ // best effort——内存态仍驱动本进程;
209
+ // 开关态(含 persistEnabled)写失败也是治理审计丢失,通知审计钩子(不抛)。
210
+ if (typeof onGovernanceFailure === 'function') onGovernanceFailure();
211
+ return { persisted: false, error: error instanceof Error ? error.message : String(error) };
196
212
  }
197
213
  }
198
214
 
199
- // Build one store instance. `dshHome` is the already-resolved home directory
200
- // string (from the exported dshHome()); `logger` is the apply-time ctx.logger.
201
- // Per-apply state (profiles / deletedBuiltins / paths / allowFailOpen) collects
202
- // into `state` for the moved module-level functions. The returned surface is
203
- // identical to the pre-split factory.
204
- export function createProfileStore({ dshHome, logger }) {
215
+ // 构造一个 store 实例。`dshHome` 是已解析的主目录字符串(来自导出的
216
+ // dshHome());`logger` apply 时的 ctx.logger。`onGovernanceFailure`(可选)
217
+ // 是任何 profile/state 持久化写失败时触发的审计钩子——一次治理审计丢失事件。
218
+ // per-apply 状态(profiles / deletedBuiltins / paths / allowFailOpen)收进
219
+ // `state` 供移出的模块级函数使用。profile 注册表表面与拆分前工厂一致;两个
220
+ // 开关访问器(enabled / evolutionAdvice)读写共享 state.json。
221
+ export function createProfileStore({ dshHome, logger, onGovernanceFailure = () => {} }) {
205
222
  const profiles = new Map(BUILTIN_SEEDS.map((p) => [p.id, { ...p }]));
206
223
  const deletedBuiltins = new Set();
207
224
  const state = {
@@ -210,17 +227,27 @@ export function createProfileStore({ dshHome, logger }) {
210
227
  profilesFile: join(dshHome, 'subagent-profiles.json'),
211
228
  stateFile: join(dshHome, 'subagent-profiles.state.json'),
212
229
  allowFailOpen: false,
230
+ logger,
213
231
  };
214
232
  return {
215
233
  profiles,
216
234
  deletedBuiltins,
217
235
  resolveProfile: (id) => resolveProfile(profiles, id),
218
236
  loadProfiles: () => loadProfiles(state, logger),
219
- persistProfiles: () => persistProfiles(state, logger),
220
- loadEnabled: () => loadEnabled(state),
221
- persistEnabled: (enabled) => persistEnabled(state, enabled),
222
- // getAllowFailOpen: the cost guard must read the migration flag at dispatch
223
- // time; loadProfiles/persistProfiles own the mutation.
237
+ persistProfiles: () => {
238
+ const result = persistProfiles(state, logger);
239
+ // 治理审计:profile 变更持久化失败计一次治理丢失(写入不抛,仅为高可见标记)。
240
+ if (result.persisted === false) onGovernanceFailure();
241
+ return result;
242
+ },
243
+ loadEnabled: () => loadState(state).enabled,
244
+ persistEnabled: (enabled) => persistState(state, { enabled }, onGovernanceFailure),
245
+ loadEvolutionAdvice: () => loadState(state).evolutionAdvice,
246
+ persistEvolutionAdvice: (value) => persistState(state, { evolutionAdvice: value === true }, onGovernanceFailure),
247
+ loadEscapeEnabled: () => loadState(state).escapeEnabled,
248
+ persistEscapeEnabled: (value) => persistState(state, { escapeEnabled: value === true }, onGovernanceFailure),
249
+ // getAllowFailOpen:cost guard 必须在派发时读取迁移标志;
250
+ // loadProfiles/persistProfiles 持有该写入。
224
251
  getAllowFailOpen: () => state.allowFailOpen,
225
252
  };
226
253
  }
package/lib/core/pure.mjs CHANGED
@@ -1,10 +1,12 @@
1
- // lib/core/pure.mjs — import-free pure helpers (extracted from index.mjs and grown
2
- // by later refactors). No @deepseek-ai imports and no external dependencies:
3
- // @deepseek-ai symbols converge in lib/core/shims.mjs (the only such entry point),
4
- // and this module is safe to import from bare-CI tests without the junction
5
- // packages. Every function here is deterministic and free of service/fs access.
1
+ // lib/core/pure.mjs — 纯函数助手(从 index.mjs 拆出,经后续重构扩展)。
2
+ // @deepseek-ai import、无外部依赖:唯一 import node:crypto(dispatchLabel
3
+ // 用的 node 内置),@deepseek-ai 符号全部收敛在 lib/core/shims.mjs(唯一入口),
4
+ // 本模块可在 bare-CI 测试中无 junction 包安全导入。
5
+ // 这里每个函数都是确定性的,无服务/fs 访问。
6
6
 
7
- // Shipped toStopReason: map a turn-end reason to the seam's terminal vocabulary.
7
+ import { createHash } from 'node:crypto';
8
+
9
+ // 官方 toStopReason:把一轮结束原因映射到接缝的终态词汇表。
8
10
  export function toStopReason(reason) {
9
11
  switch (reason?.kind) {
10
12
  case 'completed': return 'completed';
@@ -15,7 +17,7 @@ export function toStopReason(reason) {
15
17
  }
16
18
  }
17
19
 
18
- // Shipped stopReasonError + withPartialText wording (dsh-tool-subagent L55-75).
20
+ // 官方 stopReasonError + withPartialText 措辞(dsh-tool-subagent L55-75)。
19
21
  export function stopReasonError(result) {
20
22
  switch (result.stopReason) {
21
23
  case 'completed': return;
@@ -43,34 +45,31 @@ export function textFrom(blocks) {
43
45
  }
44
46
 
45
47
  // --- 统一输入 schema ----------------------------------------------------------
46
- // sanitizeProfile is the single per-profile sampler shared by loadProfiles
47
- // (strict=false, migration-tolerant) and the HTTP /add write path (strict=true,
48
- // write-reject). It normalizes each field with a field-specific sampler and
49
- // returns `{ clean, warnings }`:
50
- // - clean the sanitized profile, minus any rejected field.
51
- // - warnings — array of `{ field, reason }` (reason is Chinese). Only
52
- // rejections / over-limit conditions land here; silent
53
- // normalizations (e.g. description newline-flattening, toolFilter
54
- // dedupe) do NOT produce a warning, so callers can treat a
55
- // non-empty warnings list as "this write would drop data".
48
+ // sanitizeProfile 是唯一的逐 profile 采样器,loadProfiles(strict=false,
49
+ // 迁移容忍)与 HTTP /add 写路径(strict=true,写拒绝)共用。它用字段专属
50
+ // 采样器规范化每个字段,返回 `{ clean, warnings }`:
51
+ // - clean 净化后的 profile,剔除被拒字段。
52
+ // - warnings `{ field, reason }` 数组(reason 为中文)。只有拒绝/超限
53
+ // 条件落到这里;静默规范化(如 description 换行压平、toolFilter
54
+ // 去重)不产生警告,调用方可把非空 warnings 当作「本次写入会
55
+ // 丢数据」。
56
56
 
57
- // persona length cap. 建议值 2048,待实测。The cap
58
- // applies to the INJECTED persona text, i.e. the guidance prefix + the raw text.
57
+ // persona 长度上限。建议值 2048,待实测。上限作用于**注入的** persona 文本,
58
+ // 即引导前缀 + 原文。
59
59
  export const PERSONA_MAX_CHARS = 2048;
60
60
 
61
- // The persona is injected as a shadow section; the guidance marker is prefixed
62
- // to make its non-authoritative nature explicit (双防线). 仅用于写入校验与提示。
61
+ // persona 以影子 section 注入;前缀引导标记显式声明其非权威性质(双防线)。
62
+ // 仅用于写入校验与提示。
63
63
  export const GUIDANCE_PREFIX = '[guidance, not authority] ';
64
64
 
65
- // Shared delegation caps single source of truth for sanitizeProfile and the
66
- // cost guard in lib/core/cost-guard.mjs (hard caps always-on).
65
+ // 共享委派上限——sanitizeProfile lib/core/cost-guard.mjs cost guard
66
+ // (硬上限恒开)的唯一事实来源。
67
67
  export const MAX_TOKENS = 65536;
68
68
  export const MAX_DEPTH = 3;
69
69
 
70
70
  // --- cost guard 硬上限 --------------------------------------------------------
71
- // assertHardLimits is the always-on budget-cap check, independent of the `llm`
72
- // service: maxTokens / maxDepth are hard delegation caps, so they must NOT
73
- // silently stop applying when the `llm` service is absent.
71
+ // assertHardLimits 是恒开的预算上限检查,与 `llm` 服务无关:maxTokens /
72
+ // maxDepth 是硬性委派上限,llm 缺失时绝不能静默停止生效。
74
73
  // 超限 throw(中文、可操作),与 sanitizeProfile 共用同一组常量。非数字/未设值
75
74
  // 不触发(sanitizeProfile 已在写路径拒绝非数字,这里仅兜底运行时竞态)。
76
75
  export function assertHardLimits(maxTokens, maxDepth) {
@@ -83,9 +82,9 @@ export function assertHardLimits(maxTokens, maxDepth) {
83
82
  }
84
83
 
85
84
  // --- continuable 工具门闭集 ---------------------------------------------------
86
- // computeContinuableAllow pre-computes the CLOSED tool `allow` set for the
87
- // continuable dispatch path: 父工具集 − run_code − (toolFilter.deny),并在存在
88
- // toolFilter.allow 时再 ∩ allow。空集 fail-loud(throw)—— 绝不静默派发零工具。
85
+ // computeContinuableAllow 预计算 continuable 派发路径的**闭合**工具 `allow` 集:
86
+ // 父工具集 − run_code − (toolFilter.deny),存在 toolFilter.allow 时再 ∩ allow。
87
+ // 空集 fail-loud(throw)——绝不静默派发零工具。
89
88
  //
90
89
  // 代码编辑者注意:这里写代码注释的「假设 / 失效条件」必须与 dispatch-tool.mjs
91
90
  // continuable 分支的注释保持一致(要求写入代码注释与 README)。
@@ -109,11 +108,10 @@ export function computeContinuableAllow(parentNames, toolFilter = {}) {
109
108
  return result;
110
109
  }
111
110
 
112
- // description/name sampler: `\n→空格`压平. These are display lines (the
113
- // dispatch:profiles section interpolates them into a single row), so a newline
114
- // would break the row. 压平仅作存储层;引号包裹由显示层负责——显示层在该行给
115
- // description 加引号(空时显示 '(无描述)' 不套引号),本纯函数只保证存储值
116
- // 是单行、无换行。
111
+ // description/name 采样器:`\n→空格` 压平。这些是显示行(dispatch:profiles
112
+ // section 把它们插进单行),换行会破坏该行。压平仅作存储层;引号包裹由显示层
113
+ // 负责——显示层在该行给 description 加引号(空时显示 '(无描述)' 不套引号),
114
+ // 本纯函数只保证存储值是单行、无换行。
117
115
  function sanitizeShortText(value) {
118
116
  return value.replace(/\r\n|\r|\n/g, ' ');
119
117
  }
@@ -192,8 +190,8 @@ function sanitizeToolFilterField(value, clean, warnings) {
192
190
  const deduped = [...new Set(sub.filter((s) => s.length > 0))];
193
191
  if (deduped.length > 0) tf[op] = deduped;
194
192
  }
195
- // Unknown toolFilter keys are ignored; an empty (or fully-invalid)
196
- // toolFilter is simply omitted from clean (mirrors the /add write path).
193
+ // 未知 toolFilter 键忽略;空(或全非法)toolFilter 直接从 clean 省略
194
+ // (与 /add 写路径一致)。
197
195
  if (Object.keys(tf).length > 0) clean.toolFilter = tf;
198
196
  }
199
197
 
@@ -215,24 +213,22 @@ function sanitizeTokenTierField(value, clean, warnings) {
215
213
  // 在 `textFrom` 之前执行,把回灌进父上下文的体积压到阈值内。
216
214
  //
217
215
  // 阈值常量:head / tail / minKeep 是**子级**预期裁剪口径(比 agent.cordis.yml
218
- // 现行 compaction-basic 的 4096/1024 更保守)。**待实测**——宿主
216
+ // 现行 compaction-basic 的 4096/1024 更保守)。宿主
219
217
  // toolResultPruner.pruneContent(blocks) 只接收 blocks,自身读取其配置
220
218
  // (thresholdChars/headChars/tailChars),因此这三个常量当前**不会**作为实参
221
219
  // 传给宿主 pruner;它们记录本条目的子级口径,并保留给后续「信封 / 精修
222
- // 剪枝」路径使用。改动前先实测真实分布再校准。
220
+ // 剪枝」路径使用。改动前先实测真实分布再校准(当前子输出样本不足,待积累)。
223
221
  export const PRUNE_HEAD_CHARS = 2048;
224
222
  export const PRUNE_TAIL_CHARS = 1024;
225
223
  export const PRUNE_MIN_KEEP = 128;
226
224
 
227
- // Apply host pruning to one subagent result's content blocks.
228
- // - `blocks` the result.output content blocks (or undefined; not an array).
229
- // - `pruner` the `toolResultPruner` service, or undefined.
230
- // Returns the (possibly pruned) blocks, or an empty array placeholder. When the
231
- // pruner is absent (headless deployment without the compaction pruner) OR the
232
- // content is not an array, it falls back to NO pruning — 剪枝是增强,绝非硬依赖。
233
- // A host pruner that throws on an unusual content shape also
234
- // falls back to the full output, because automatic pruning must NEVER swallow a
235
- // legitimate child result.
225
+ // 对单个子 Agent 结果的内容块应用宿主剪枝。
226
+ // - `blocks` — result.output 内容块(或 undefined;非数组)。
227
+ // - `pruner` — `toolResultPruner` 服务,或 undefined
228
+ // 返回(可能被剪的)块,或空数组占位。pruner 缺失(无压缩插件的无头部署)
229
+ // 或内容非数组时回退为不剪——剪枝是增强,绝非硬依赖。
230
+ // 宿主 pruner 在异常内容形状上抛错也回退为完整输出,因为自动剪枝绝不允许
231
+ // 吞掉合法的子结果。
236
232
  export function pruneBlocks(blocks, pruner) {
237
233
  if (Array.isArray(blocks) && pruner !== undefined && typeof pruner.pruneContent === 'function') {
238
234
  try {
@@ -245,12 +241,58 @@ export function pruneBlocks(blocks, pruner) {
245
241
  return Array.isArray(blocks) ? blocks : [];
246
242
  }
247
243
 
244
+ // --- 输出回灌信任标注 ----------------------------------------------------------
245
+ // trustLabel 给 completed 子结果的回收文本加结构化信任标注前缀,让父上下文
246
+ // 不把子 Agent 输出误当高信任指令(正文先见标注、再见子输出)。profile/preset
247
+ // 与 buildMeta 同源字段;preset 为 'inherit'(子继承父预设、无换用)或字段缺失
248
+ // 时省略该段。前缀只含这两个元数据标识符,绝不携带 prompt/persona 原文。
249
+ function trustFields(meta) {
250
+ const m = meta !== null && meta !== undefined && typeof meta === 'object' ? meta : {};
251
+ const fields = [];
252
+ if (typeof m.profile === 'string' && m.profile !== '') fields.push(['profile', m.profile]);
253
+ if (typeof m.preset === 'string' && m.preset !== '' && m.preset !== 'inherit') fields.push(['preset', m.preset]);
254
+ return fields;
255
+ }
256
+
257
+ // 模型可见的标注前缀:进回收 output 文本(render 行首部格式不变,前缀在 output 内部)。
258
+ export function trustLabel(meta) {
259
+ const fields = trustFields(meta);
260
+ return fields.length === 0
261
+ ? '[dispatch:trusted-output]'
262
+ : `[dispatch:trusted-output] ${fields.map(([key, value]) => `${key}=${value}`).join(' ')}`;
263
+ }
264
+
265
+ // 审计记录:与 trustLabel 同字段集的 JSON,只进 logger、不进结果字符串,无子输出
266
+ // 原文。调用方拼 `[dsh-subagent-profile] trusted-output: ` 前缀后以 info 记录。
267
+ export function trustAudit(meta) {
268
+ return JSON.stringify(Object.fromEntries(trustFields(meta)));
269
+ }
270
+
271
+ // --- 派发 label 去敏 ----------------------------------------------------------
272
+ // dispatchLabel 生成子会话标题/任务卡 label:只含结构化段(profile/preset/model)
273
+ // 加 prompt 的 sha1 前 8 位十六进制哈希,绝不落 prompt 原文前缀。哈希让排障时
274
+ // 不同任务可辨识(不同 prompt → 不同哈希),却不泄漏任何前缀内容。profile 缺失
275
+ // 时回退 '(inline)';preset 缺失或为 'inherit'(子继承父预设、无换用)时省略该
276
+ // 段;model 缺失时省略该段——与 buildMeta/trustFields 的字段口径一致。
277
+ export function dispatchLabel(args, merged) {
278
+ const a = args !== null && typeof args === 'object' ? args : {};
279
+ const m = merged !== null && typeof merged === 'object' ? merged : {};
280
+ const profile = typeof a.profile === 'string' && a.profile !== '' ? a.profile : '(inline)';
281
+ const promptText = String(a.prompt ?? '');
282
+ const hash = createHash('sha1').update(promptText, 'utf8').digest('hex').slice(0, 8);
283
+ const segments = [`dispatch:${profile}`];
284
+ if (typeof m.preset === 'string' && m.preset !== '' && m.preset !== 'inherit') segments.push(m.preset);
285
+ if (typeof m.model === 'string' && m.model !== '') segments.push(m.model);
286
+ segments.push(`#${hash}`);
287
+ return segments.join(' ');
288
+ }
289
+
248
290
  // --- continuable 可见性修复 ----------------------------------------------------
249
291
  // 共享一致性规则:三个 closed `output.schema.oneOf` 分支
250
- // (background / continuable / foreground) must carry an IDENTICAL shared meta key
251
- // set whenever a result-meta field is added — so `ignored`, `reasoningEffort`,
252
- // `profile/preset/provider/model` must appear in ALL three branches, keeping the
253
- // model-side schema from rejecting a分支 that "forgot" the field.
292
+ // (background / continuable / foreground) 在新增结果元数据字段时必须携带
293
+ // **相同**的共享元数据键集——`ignored`、`reasoningEffort`、
294
+ // `profile/preset/provider/model` 必须出现在全部三个分支,使模型侧 schema
295
+ // 不会拒绝「忘了」该字段的分支。
254
296
  //
255
297
  // 判据:元数据集合 = 每个分支 properties 的键集,**剔除**各分支自有的判别键
256
298
  // (`kind`/`jobId`/`subagentId`/`output`——background/continuable/foreground 各自
@@ -287,10 +329,9 @@ export function assertResultSchemaConsistency(schema) {
287
329
  return true;
288
330
  }
289
331
 
290
- // The full field set that may live on a profile. ANY other key including
291
- // __proto__ / constructor / prototype (which JSON.parse can produce as OWN
292
- // properties) — is dropped instead of copied, so assignment never walks into
293
- // the object prototype chain (prototype-pollution guard).
332
+ // profile 上可能存在的完整字段集。任何其它键——包括 __proto__ / constructor /
333
+ // prototypeJSON.parse 可以产出为 OWN 属性)——一律丢弃而不复制,赋值永远
334
+ // 不会走进对象原型链(原型污染防护)。
294
335
  const KNOWN_PROFILE_FIELDS = new Set([
295
336
  'id', 'name', 'description', 'persona', 'preset', 'provider', 'model',
296
337
  'reasoningEffort', 'enabled', 'maxTokens', 'maxDepth', 'toolFilter',
@@ -298,18 +339,16 @@ const KNOWN_PROFILE_FIELDS = new Set([
298
339
  ]);
299
340
 
300
341
  /**
301
- * Sanitize one profile entry. `options.strict`:
302
- * - strict=false (migration read): an over-length persona is KEPT (not
303
- * truncated) and warned; over-limit maxTokens/maxDepth and illegal
304
- * toolFilter subfields are dropped (field removed) + warned.
305
- * - strict=true (write path): an over-length persona is also REMOVED from
306
- * clean and warned, so the caller can reject the whole write with a 400.
307
- * Non-rejection normalizations (description/name flatten, toolFilter dedupe) are
308
- * silent and never produce a warning.
342
+ * 净化一条 profile。`options.strict`:
343
+ * - strict=false(迁移读取):超长 persona 保留(不截断)并告警;超限
344
+ * maxTokens/maxDepth 与非法 toolFilter 子字段丢弃(字段移除)+ 告警。
345
+ * - strict=true(写路径):超长 persona 也从 clean 移除并告警,调用方可
346
+ * 400 拒绝整次写入。
347
+ * 非拒绝型规范化(description/name 压平、toolFilter 去重)静默进行,绝不告警。
309
348
  *
310
- * Security: `clean` is `Object.create(null)` (no inherited `__proto__`
311
- * setter) and only whitelisted fields are copied, so a hostile `__proto__` /
312
- * `constructor` / `prototype` key is ignored rather than polluting the result.
349
+ * 安全:`clean` `Object.create(null)`(无继承的 `__proto__` setter),且只
350
+ * 复制白名单字段,敌意的 `__proto__` / `constructor` / `prototype` 键被忽略而
351
+ * 不会污染结果。
313
352
  */
314
353
  export function sanitizeProfile(profile, options = {}) {
315
354
  const { strict = false } = options;
@@ -325,8 +364,13 @@ export function sanitizeProfile(profile, options = {}) {
325
364
  continue;
326
365
  }
327
366
  switch (key) {
367
+ case 'id':
368
+ case 'preset':
328
369
  case 'name':
329
370
  case 'description':
371
+ // id/preset 与 name/description 同走文本采样:压平换行,阻断经 \n 注入
372
+ // orchestrator systemPrompt 的提示注入面(H1)。id/preset 此前走 default
373
+ // 分支原样拷贝,可注入换行破坏 dispatch:profiles 行。
330
374
  sanitizeTextField(key, value, clean, warnings);
331
375
  break;
332
376
  case 'persona':