dsh-subagent-profile 0.3.3 → 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.
@@ -88,6 +88,8 @@ function buildBase({ parent, args, merged, mode, pluginVersion }) {
88
88
  // session_id 空值归一 null:它是未来聚合层惰性 join 的键,静默缺失会破坏
89
89
  // join 且难察觉,显式 null 至少保持 JSONL 行形状稳定。
90
90
  session_id: parent?.session?.header?.id ?? null,
91
+ parent_model: parent?.options?.model ?? null,
92
+ parent_provider: parent?.options?.provider ?? null,
91
93
  mode,
92
94
  origin: ORIGIN,
93
95
  requested: cfgOf(merged),
@@ -107,7 +109,7 @@ function buildBase({ parent, args, merged, mode, pluginVersion }) {
107
109
  // tool_count 为「携带 usage 的 assistant/message 事件数」(collectChildCalls 的
108
110
  // totalCalls),是工具调用次数的可测代理而非字面计数值——宿主会话面无更直接的
109
111
  // 工具调用计数字段;拿不到记 null。
110
- function foregroundOutcome({ result, elapsedMs, prunedOutputLen, toolCount }) {
112
+ function foregroundOutcome({ result, elapsedMs, prunedOutputLen, toolCount, childTotalTokens, childUsage }) {
111
113
  const stopReason = result?.stopReason ?? 'completed';
112
114
  const completed = stopReason === 'completed';
113
115
  return {
@@ -116,6 +118,8 @@ function foregroundOutcome({ result, elapsedMs, prunedOutputLen, toolCount }) {
116
118
  elapsed_ms: elapsedMs,
117
119
  output_len: prunedOutputLen,
118
120
  tool_count: completed ? toolCount : null,
121
+ ...(typeof childTotalTokens === 'number' && Number.isFinite(childTotalTokens) ? { tokens: childTotalTokens } : {}),
122
+ ...(childUsage !== null && typeof childUsage === 'object' ? { usage: childUsage } : {}),
119
123
  };
120
124
  }
121
125
 
@@ -130,12 +134,14 @@ function backgroundOutcome(settled) {
130
134
  elapsed_ms: s.elapsedMs,
131
135
  output_len: typeof s.output === 'string' ? s.output.length : 0,
132
136
  tool_count: totalCalls,
137
+ ...(typeof s.childTotalTokens === 'number' && Number.isFinite(s.childTotalTokens) ? { tokens: s.childTotalTokens } : {}),
138
+ ...(s.childUsage !== null && typeof s.childUsage === 'object' ? { usage: s.childUsage } : {}),
133
139
  };
134
140
  }
135
141
 
136
142
  // --- 三分支 finish 助手:每条派发只加一行调用(守 dispatch-tool 行门)------------
137
143
 
138
- export function finishForegroundLedger(ledger, base, childId, result, prunedBlocks, elapsedMs, childCalls) {
144
+ export function finishForegroundLedger(ledger, base, childId, result, prunedBlocks, elapsedMs, childCalls, childTotalTokens, childUsage) {
139
145
  const calls = childCalls ?? {};
140
146
  ledger.record({
141
147
  ...base,
@@ -146,6 +152,8 @@ export function finishForegroundLedger(ledger, base, childId, result, prunedBloc
146
152
  elapsedMs,
147
153
  prunedOutputLen: textFrom(prunedBlocks).length,
148
154
  toolCount: typeof calls.totalCalls === 'number' ? calls.totalCalls : null,
155
+ childTotalTokens,
156
+ childUsage,
149
157
  }),
150
158
  });
151
159
  }
@@ -161,7 +169,7 @@ export function finishContinuableLedger(ledger, base, childId) {
161
169
  // --- 审计 meta(ledger.meta.json)----------------------------------------------
162
170
 
163
171
  // meta 默认态(缺失/损坏/形状不符均回退此值 → fail-soft 从零)。last_write_ts 为
164
- // 最近一次成功落盘时间(§12.4 字段;旧文件无该字段时经合并回退 0)。
172
+ // 最近一次成功落盘时间(旧文件无该字段时经合并回退 0)。
165
173
  const META_DEFAULTS = { v: 1, lostTelemetry: 0, lostGovernance: 0, health: 'ok', last_write_ts: 0 };
166
174
 
167
175
  // 读取并校验 meta.json;缺失/损坏/形状不符回退默认并 warn(fail-soft 从零)。
@@ -215,16 +223,19 @@ function persistAuditMeta(metaState, healthValue, warn, { warnOnFailure = true }
215
223
 
216
224
  // 治理失败统一接线:计 lostGovernance + health 置 degraded,meta 尽力落盘保证
217
225
  // 重启仍 degraded;内存 health 无论落盘成败一律降级(写失败只 warn,不降回 ok)。
218
- function markGovernanceFailureMeta(meta, persistMeta) {
226
+ // ok degraded 转变时经 onAlert 通知提醒系统(全局审计异常源)。
227
+ function markGovernanceFailureMeta(meta, persistMeta, onAlert) {
228
+ const wasOk = meta.health === 'ok';
219
229
  meta.governanceLost += 1;
220
230
  meta.health = 'degraded';
221
231
  persistMeta('degraded');
232
+ if (wasOk && typeof onAlert === 'function') onAlert(meta.governanceLost);
222
233
  }
223
234
 
224
235
  // 治理审计事件(放行审计等)追加一行到 governance-audit.jsonl(append-only 不可变)。
225
236
  // 写失败绝不静默:warn + markGovernanceFailureMeta(计 lostGovernance + degraded);
226
237
  // 写成功若正处 degraded,即为一次成功文件写 → 恢复 ok(与派发台账同口径)。
227
- function appendGovernanceAudit(meta, dir, auditFile, persistMeta, warn, entry) {
238
+ function appendGovernanceAudit(meta, dir, auditFile, persistMeta, warn, onAlert, entry) {
228
239
  const line = JSON.stringify({ v: GOVERNANCE_AUDIT_VERSION, ts: Date.now(), ...entry });
229
240
  try {
230
241
  mkdirSync(dir, { recursive: true });
@@ -232,11 +243,11 @@ function appendGovernanceAudit(meta, dir, auditFile, persistMeta, warn, entry) {
232
243
  if (meta.health === 'degraded') persistMeta('ok');
233
244
  } catch (error) {
234
245
  warn(`dispatch ledger: 治理审计事件写入失败(${error instanceof Error ? error.message : String(error)})`);
235
- markGovernanceFailureMeta(meta, persistMeta);
246
+ markGovernanceFailureMeta(meta, persistMeta, onAlert);
236
247
  }
237
248
  }
238
249
 
239
- export function createEvolutionLedger({ dshHome, pluginVersion, warn = () => {} } = {}) {
250
+ export function createEvolutionLedger({ dshHome, pluginVersion, warn = () => {}, onAlert } = {}) {
240
251
  if (typeof dshHome !== 'string' || dshHome.length === 0) {
241
252
  throw new Error('dispatch ledger: 派发台账需要 dshHome 目录');
242
253
  }
@@ -273,12 +284,12 @@ export function createEvolutionLedger({ dshHome, pluginVersion, warn = () => {}
273
284
  }
274
285
  },
275
286
  recordGovernanceAudit(entry) {
276
- appendGovernanceAudit(meta, dir, auditFile, persistMeta, warn, entry);
287
+ appendGovernanceAudit(meta, dir, auditFile, persistMeta, warn, onAlert, entry);
277
288
  },
278
289
  // 治理审计失败(profile 变更写失败等):计 lostGovernance + health 置 degraded,
279
290
  // 不抛、不阻断调用方;meta 尽力落盘保证重启仍 degraded。
280
291
  markGovernanceFailure() {
281
- markGovernanceFailureMeta(meta, persistMeta);
292
+ markGovernanceFailureMeta(meta, persistMeta, onAlert);
282
293
  },
283
294
  // 进程内 health('ok' | 'degraded')。
284
295
  metaHealth: () => meta.health,
@@ -1,23 +1,11 @@
1
- // lib/core/evolution-summary.mjs — 派发台账(dispatch.jsonl)的确定性双层聚合
2
- // (summaries.json)。纯函数为主:给定输入完全决定输出,可复算、可复现。IO 仅在
3
- // writeSummaries(原子写 tmp+rename)与顶层装配层出现;computeSummaries 不读文件。
4
- //
5
- // 输入口径与派发台账一致(见 evolution-ledger.mjs):每条 record dispatch.jsonl
6
- // 一行的解析结果,含 requested/effective 双栏 cfg(preset/model/provider/
7
- // reasoningEffort/maxTokens/persona_present/toolFilter_present 等)与可选 outcome
8
- // {status|elapsed_ms|output_len|tool_count}。continuable 无结算(无 outcome 键),
9
- // 不参与 deployments_total/性能聚合,但参与轴分布。
10
- //
11
- // 不变量(可复算):同输入恒同输出,遍历序稳定;聚合只读台账、不写回;parent_adopted
12
- // 等动态质量信号不在台账内(写入当时不存在),由聚合层惰性 join(本模块只留字段
13
- // 占位与可选输入 seam),原始台账本身不注入。
14
-
15
- // 仅 writeSummaries/readSummaries/buildAdviceText 需要 IO(node 内置 fs),与台账采集
16
- // 同种授权面;纯聚合/建议函数零 IO。
17
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
1
+ // lib/core/evolution-summary.mjs — 派发台账(dispatch.jsonl)的确定性双层聚合。
2
+ // computeSummaries 纯函数不读文件;writeSummaries 才碰 IO(派发优化建议链 readSummaries /
3
+ // refreshSummaries / computeAdvice / buildAdviceText 已拆至 evolution-advice.mjs 守行门)。
4
+ // 不变量:同输入恒同输出;聚合只读台账不写回;parent_adopted 由聚合层惰性 join。
5
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
18
6
  import { dirname } from 'node:path';
7
+ import { summarizeCosts } from './cost-evidence.mjs';
19
8
 
20
- // --- 加权成功分 ---------------------------------------------------------------
21
9
  // 公式:weighted_success = (completed − 0.3·confirmedFalse) / total,下限 0,小数点 3
22
10
  // 位。每例「已确认未被采纳」的派发(parentAdoptedConfirmedFalse)从分子扣 0.3;
23
11
  // unknown(未决)计 0 不惩罚——只有确认 false 才有惩罚,避免窗口期未判决的派发过早
@@ -53,8 +41,9 @@ export function confidenceLevel(n, minN = 3, highN = 10) {
53
41
  // --- profile 身份与轴提取(确定性)-------------------------------------------
54
42
  // L1 key = 生效配置重建的 profile 身份(台账不存 profileId,故从 effective cfg 重建);
55
43
  // 无区分性配置时记 '(inline)'。能力轴(preset/provider/model/persona/toolFilter)构成
56
- // 身份;预算轴 effort 不并入(同一配置不同档位不拆组)——但「只调 effort 未调能力轴」
57
- // 的派发需独立身份才能产出预算轴升建议,此时 effort 单独成键。
44
+ // 身份;预算轴 effort/maxTokens 不并入 L1 键(聚合语义修正:同一方案改预算不得被当成
45
+ // 不同身份——恒判「降」的混合键已废弃)。纯 effort 组与无配置组都落 '(inline)',
46
+ // 升/降方向改由组内实际配置轴(summarizeGroup 的 axes 字段)判定。
58
47
  function cfgSet(cfg) {
59
48
  return cfg !== null && typeof cfg === 'object' ? cfg : {};
60
49
  }
@@ -67,12 +56,12 @@ function profileKeyOf(cfg) {
67
56
  if (c.model !== undefined && c.model !== null) parts.push(`model:${c.model}`);
68
57
  if (c.persona_present === true) parts.push('persona:1');
69
58
  if (c.toolFilter_present === true) parts.push('toolFilter:1');
70
- if (parts.length === 0 && c.reasoningEffort !== undefined && c.reasoningEffort !== null) {
71
- parts.push(`effort:${c.reasoningEffort}`);
72
- }
73
59
  return parts.length > 0 ? parts.join('|') : '(inline)';
74
60
  }
75
61
 
62
+ // 供 AdoptionTracker 等聚合 join 复用:与 computeSummaries 的 L1 身份键完全一致。
63
+ export { profileKeyOf };
64
+
76
65
  // 能力轴取值:preset/model/provider 取 effective 值(实数命中);persona 只存 present
77
66
  // 布尔(隐私不落原文);toolFilter_from 取 toolFilter_present 的来源标记(requested/
78
67
  // effective 在台账内同源,记 'requested'/'effective'/absent)。
@@ -122,6 +111,25 @@ function mean(acc) {
122
111
  return acc === null || acc.count === 0 ? null : Number((acc.sum / acc.count).toFixed(2));
123
112
  }
124
113
 
114
+ // 组内轴存在性:能力轴(preset/provider/model/persona/toolFilter)与预算轴
115
+ // (reasoningEffort/maxTokens)。L1 键去 effort 后建议的升/降方向不再只依赖键文本
116
+ // (纯 effort 组键为 '(inline)'),改为读组内实际配置轴。
117
+ function groupAxes(records) {
118
+ let capability = false;
119
+ let budget = false;
120
+ for (const r of records) {
121
+ if (r === null || typeof r !== 'object') continue;
122
+ const eff = cfgSet(r.effective);
123
+ if ((eff.preset !== undefined && eff.preset !== null && eff.preset !== 'inherit')
124
+ || (eff.provider !== undefined && eff.provider !== null)
125
+ || (eff.model !== undefined && eff.model !== null)
126
+ || eff.persona_present === true || eff.toolFilter_present === true) capability = true;
127
+ if ((eff.reasoningEffort !== undefined && eff.reasoningEffort !== null)
128
+ || (eff.maxTokens !== undefined && eff.maxTokens !== null)) budget = true;
129
+ }
130
+ return { capability, budget };
131
+ }
132
+
125
133
  // 组装一层聚合子集(L1 与 L2 复用):outcome/score/perf/confidence/cooldown/deployments_total。
126
134
  function summarizeGroup(records, opts = {}) {
127
135
  const counts = outcomeCounts(records);
@@ -141,6 +149,7 @@ function summarizeGroup(records, opts = {}) {
141
149
  // tool_count 只对 completed 结算(见台账),且无 usage 事件时空值不计。
142
150
  if (r.outcome.status === 'completed') toolAcc = avg(toolAcc, r.outcome.tool_count);
143
151
  }
152
+ const cost = summarizeCosts(records);
144
153
  return {
145
154
  deployments_total: deploymentsTotal,
146
155
  outcome: counts,
@@ -155,15 +164,28 @@ function summarizeGroup(records, opts = {}) {
155
164
  min_n_required: 3,
156
165
  level: confidenceLevel(deploymentsTotal),
157
166
  },
167
+ // 省 token 证据(38c):avg_cost 供模型侧 section 与设置页成本预览同源 join;
168
+ // 无价格/无 usage 的样本不进 priced,avg_cost 为 null 时调用方省略显示。
169
+ avg_cost: cost.priced > 0 ? Number((cost.estimated_cost / cost.priced).toFixed(4)) : null,
170
+ cost: {
171
+ estimated_cost: cost.estimated_cost,
172
+ estimated_inherit_cost: cost.estimated_inherit_cost,
173
+ estimated_saving: cost.estimated_saving,
174
+ priced: cost.priced,
175
+ inherit_count: cost.inherit_count,
176
+ inherit_ratio: cost.inherit_ratio,
177
+ },
158
178
  // 冷却期:until_ts/last_apply_ts 由应用层(建议采纳)回填;聚合层尚无来源,置 null。
159
179
  cooldown: { until_ts: null, last_apply_ts: null },
180
+ // 组内轴存在性:L1 键去 effort 后的方向判定来源(legacy 无此字段时回退键文本)。
181
+ axes: groupAxes(records),
160
182
  };
161
183
  }
162
184
 
163
185
  // --- 顶层聚合(纯函数,无 IO)---------------------------------------------------
164
186
  // records:dispatch.jsonl 逐行解析结果(调用方可先用 parseDispatchRecords 过滤损坏行)。
165
187
  // opts.parentAdoptedConfirmedFalse:可选按聚合 key 的「已确认未采纳」计数 map,供外部
166
- // 惰性 join 注入(Task 23 建议层);缺省视为 0(台账内无该信号)。返回 l1/l2 普通对象。
188
+ // 惰性 join 注入;缺省视为 0(台账内无该信号)。返回 l1/l2 普通对象。
167
189
  export function computeSummaries(records, opts = {}) {
168
190
  const l1Map = new Map();
169
191
  const l2Map = new Map();
@@ -189,91 +211,6 @@ export function computeSummaries(records, opts = {}) {
189
211
  return { l1: build(l1Map), l2: build(l2Map) };
190
212
  }
191
213
 
192
- // --- 只读建议(suggestAdvice)---------------------------------------------------
193
- // 从单条 L1 聚合(l1Entry)产出「一项确定性建议」或 null。四条件齐备才产出:
194
- // N>=minN、weighted_success 低于阈值(表现不佳才建议)、置信度达标、cooldown
195
- // 未过期。方向按升/降轴硬规则:能力轴(preset/model/provider/persona/
196
- // toolFilter)只建议降;预算轴(effort)可建议升;无法定位安全轴的键保守维持
197
- // (平)。阈值 successThreshold / confidenceMinLevel / cooldownMs 为建议初值,
198
- // 待实测后校准。
199
- const CONFIDENCE_RANK = { low: 0, medium: 1, high: 2 };
200
-
201
- function hasCapabilityAxis(profileKey) {
202
- return /(^|\|)(preset|provider|model|persona|toolFilter):/.test(String(profileKey ?? ''));
203
- }
204
-
205
- function hasBudgetAxis(profileKey) {
206
- return /(^|\|)effort:/.test(String(profileKey ?? ''));
207
- }
208
-
209
- // 从 profileKey 提取 preset 候选值(无 preset 段返回 null,如 inline / 纯预算键)。
210
- export function presetFromKey(profileKey) {
211
- if (typeof profileKey !== 'string' || profileKey === '' || profileKey === '(inline)') return null;
212
- for (const part of profileKey.split('|')) {
213
- if (part.startsWith('preset:')) return part.slice('preset:'.length);
214
- }
215
- return null;
216
- }
217
-
218
- // 建议候选校验:preset 候选非 system-trust 白名单即 fail-loud 抛错(与派发守卫
219
- // whitelist 闸一致,不静默滤掉)。whitelist 接受 Set 或数组。
220
- export function assertSystemCandidate(whitelist, preset) {
221
- if (preset === null || preset === undefined || preset === '' || preset === 'inherit') return;
222
- const inList = whitelist instanceof Set ? whitelist.has(preset) : Array.isArray(whitelist) && whitelist.includes(preset);
223
- if (!inList) {
224
- throw new Error(`evolution:advice: 候选预设 "${preset}" 不在 system-trust 白名单(拒绝非 system 候选)`);
225
- }
226
- }
227
-
228
- function performanceText(entry) {
229
- const outcome = entry?.outcome ?? {};
230
- const perf = entry?.perf ?? {};
231
- const parts = [
232
- `过去 ${entry?.deployments_total ?? 0} 次:完成 ${outcome.completed ?? 0}、失败 ${outcome.failed ?? 0}、终止 ${outcome.killed ?? 0}`,
233
- `加权成功分 ${entry?.score?.weighted_success ?? 0}`,
234
- ];
235
- if (perf.avg_elapsed_ms !== null && perf.avg_elapsed_ms !== undefined) parts.push(`平均耗时 ${perf.avg_elapsed_ms}ms`);
236
- if (perf.avg_output_len !== null && perf.avg_output_len !== undefined) parts.push(`平均输出 ${perf.avg_output_len} 字符`);
237
- return parts.join(',');
238
- }
239
-
240
- function directionFor(profileKey, capability, budget) {
241
- if (budget && !capability) return '升';
242
- if (capability) return '降';
243
- return '平';
244
- }
245
-
246
- export function suggestAdvice({
247
- l1Entry,
248
- profileKey = '',
249
- minN = 3,
250
- cooldownMs = 600000,
251
- now = Date.now(),
252
- successThreshold = 0.6,
253
- confidenceMinLevel = 'medium',
254
- } = {}) {
255
- const entry = l1Entry !== null && typeof l1Entry === 'object' ? l1Entry : {};
256
- const key = typeof profileKey === 'string' ? profileKey : (typeof entry.profileKey === 'string' ? entry.profileKey : '');
257
- const n = Number.isFinite(entry.confidence?.n) ? entry.confidence.n : 0;
258
- const level = typeof entry.confidence?.level === 'string' ? entry.confidence.level : confidenceLevel(n, minN);
259
- const score = Number.isFinite(entry.score?.weighted_success) ? entry.score.weighted_success : 0;
260
- const until = entry.cooldown?.until_ts;
261
-
262
- // 四条件齐备才产出(任一不满足返回 null)。
263
- if (n < minN) return null;
264
- if ((CONFIDENCE_RANK[level] ?? -1) < (CONFIDENCE_RANK[confidenceMinLevel] ?? 0)) return null;
265
- if (until !== null && until !== undefined && now < until) return null;
266
- if (!(score < successThreshold)) return null;
267
-
268
- return {
269
- profileKey: key,
270
- performanceText: performanceText(entry),
271
- suggestion: directionFor(key, hasCapabilityAxis(key), hasBudgetAxis(key)),
272
- confidence: level,
273
- cooldownUntil: now + cooldownMs,
274
- };
275
- }
276
-
277
214
  // --- 损坏行解析(纯函数,fail-soft 跳过 + 计数)-------------------------------
278
215
  // 逐行 JSON.parse;损坏/空行跳过并计数,不影响有效记录。
279
216
  export function parseDispatchRecords(lines) {
@@ -315,118 +252,4 @@ export function writeSummaries(file, data) {
315
252
  }
316
253
  }
317
254
 
318
- // --- 只读建议(evolution:advice 注入段,默认关)--------------------------------
319
-
320
- // 读 summaries.json(存在才读;损坏/形状不符 → null,fail-soft)。
321
- // §12.5 版本化:只接受 v:1;未知/超前版本 fail-soft 跳过(v1 无前驱,迁移表空)。
322
- export function readSummaries(file, logger) {
323
- try {
324
- if (!existsSync(file)) return null;
325
- const parsed = JSON.parse(readFileSync(file, 'utf8'));
326
- if (parsed === null || typeof parsed !== 'object' || parsed.l1 === null || typeof parsed.l1 !== 'object') return null;
327
- if (parsed.v !== 1) {
328
- if (logger !== undefined && typeof logger.warn === 'function') {
329
- logger.warn(`[dsh-subagent-profile] summaries.json 版本 ${String(parsed.v)} 未知,跳过该资产(fail-soft)`);
330
- }
331
- return null;
332
- }
333
- return parsed;
334
- } catch {
335
- return null;
336
- }
337
- }
338
-
339
- // 生产聚合触发点(T1 修复):读 dispatch.jsonl → parseDispatchRecords → computeSummaries
340
- // → writeSummaries。此前只被测试调用,summaries.json 无初始生成路径。opts 可注入
341
- // parentAdoptedConfirmedFalse join(F3 尚未接线)。缺失台账返回 skipped:'no-ledger'。
342
- export function refreshSummaries({ dispatchFile, summariesFile, logger, opts = {} } = {}) {
343
- if (!existsSync(dispatchFile)) return { persisted: false, skipped: 'no-ledger' };
344
- let lines;
345
- try {
346
- lines = readFileSync(dispatchFile, 'utf8').split('\n');
347
- } catch (error) {
348
- const message = error instanceof Error ? error.message : String(error);
349
- if (logger !== undefined && typeof logger.warn === 'function') {
350
- logger.warn(`[dsh-subagent-profile] evolution:advice 读派发台账失败:${message}`);
351
- }
352
- return { persisted: false, error: message };
353
- }
354
- const { records, skipped } = parseDispatchRecords(lines);
355
- const summaries = computeSummaries(records, opts);
356
- const res = writeSummaries(summariesFile, summaries);
357
- return { ...res, records: records.length, skippedLines: skipped };
358
- }
359
-
360
- // summaries.json 是否过期(缺失但台账存在,或台账 mtime 更新)——惰性重算判据。
361
- function summariesStale(dispatchFile, summariesFile) {
362
- if (!existsSync(summariesFile)) return existsSync(dispatchFile);
363
- try {
364
- return statSync(dispatchFile).mtimeMs > statSync(summariesFile).mtimeMs;
365
- } catch {
366
- return false;
367
- }
368
- }
369
255
 
370
- // 注入段文本:全局聚合摘要 + 各 profile 一条确定性建议(人类读、非指令)。
371
- export function renderAdviceText(advice, global) {
372
- const direction = (value) => (value === '升' ? '可上调预算' : value === '降' ? '建议降级' : '维持现状');
373
- const lines = advice.map((a) => {
374
- const untilText = Number.isFinite(a.cooldownUntil)
375
- ? `(冷却至 ${new Date(a.cooldownUntil).toLocaleTimeString('zh-CN', { hour12: false })})`
376
- : '';
377
- return `- ${a.profileKey}:${a.performanceText};${direction(a.suggestion)}(置信度 ${a.confidence})${untilText}`;
378
- });
379
- const total = global.completed + global.failed + global.killed;
380
- return `以下为过去派发的只读统计与确定性建议(仅供人类参考,不改变派发行为):\n` +
381
- `全局:累计 ${total} 次(完成 ${global.completed}、失败 ${global.failed}、终止 ${global.killed})。\n${lines.join('\n')}`;
382
- }
383
-
384
- // 建议产出后把各 profile 的新 cooldown.until_ts 写回 summaries.json(原子写、
385
- // fail-soft)。写前重读磁盘再合并:并发注入各自基于最新磁盘态写回,把互相覆盖
386
- // 窗口压到最小(残余竞态:读-写间隙,彻底解决需进程内锁,成本不值)。
387
- function writeBackCooldowns(file, data, produced, logger) {
388
- const fresh = readSummaries(file, logger);
389
- const base = fresh !== null && typeof fresh === 'object' ? fresh : data;
390
- const l1 = { ...(base.l1 ?? {}) };
391
- for (const [profileKey, until] of produced) {
392
- const group = l1[profileKey];
393
- l1[profileKey] = { ...group, cooldown: { ...(group?.cooldown ?? {}), until_ts: until } };
394
- }
395
- const res = writeSummaries(file, { l1, l2: base.l2 ?? {} });
396
- if (res.persisted !== true) {
397
- logger.warn(`[dsh-subagent-profile] evolution:advice cooldown 写回失败:${res.error ?? '未知错误'}`);
398
- }
399
- }
400
-
401
- // 只读建议段文本生成:读 summaries → 逐 profile 校验候选(非 system fail-loud)→
402
- // suggestAdvice → 写回 cooldown → 渲染。空组跳过候选校验(只对产出建议要求 trust)。
403
- export function buildAdviceText({ summariesFile, dispatchFile, whitelist, logger }) {
404
- // T1 惰性重算:summaries.json 缺失或台账已更新(mtime)时先 refreshSummaries。
405
- if (dispatchFile !== undefined && summariesStale(dispatchFile, summariesFile)) {
406
- refreshSummaries({ dispatchFile, summariesFile, logger });
407
- }
408
- const data = readSummaries(summariesFile, logger);
409
- if (data === null) return '';
410
- const l1 = data.l1 ?? {};
411
- const keys = Object.keys(l1).sort();
412
- const advice = [];
413
- const produced = [];
414
- const global = { completed: 0, failed: 0, killed: 0 };
415
- for (const profileKey of keys) {
416
- const group = l1[profileKey];
417
- if (group === null || typeof group !== 'object') continue;
418
- global.completed += group.outcome?.completed ?? 0;
419
- global.failed += group.outcome?.failed ?? 0;
420
- global.killed += group.outcome?.killed ?? 0;
421
- const suggestion = suggestAdvice({ l1Entry: group, profileKey });
422
- if (suggestion === null) continue;
423
- // F4:候选校验移到「实际产出建议」之后——N<3 组不产出建议,不应因非 system
424
- // 候选而抛(校验先于判定与模块注释「只有实际产出的建议键才要求 system-trust」矛盾)。
425
- assertSystemCandidate(whitelist, presetFromKey(profileKey));
426
- advice.push(suggestion);
427
- produced.push([profileKey, suggestion.cooldownUntil]);
428
- }
429
- if (advice.length === 0) return '';
430
- writeBackCooldowns(summariesFile, data, produced, logger);
431
- return renderAdviceText(advice, global);
432
- }