@yangdcm/dsh-expert-team 1.3.4 → 1.3.5
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 +49 -0
- package/client.js +28 -4
- package/lib/command-parse.js +9 -2
- package/lib/command.js +83 -14
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,55 @@
|
|
|
3
3
|
本包遵循[语义化版本](https://semver.org/lang/zh-CN/)。dsh 宿主版本线的对应关系写在
|
|
4
4
|
`package.json` 的 `engines.dsh` 与 `dsh.compatibility` 里,插件市场按它判断"这个插件跟你的宿主兼不兼容"。
|
|
5
5
|
|
|
6
|
+
## 1.3.5
|
|
7
|
+
|
|
8
|
+
**性能:修掉 `/state` 的 30 秒级阻塞(它会把整个 `dsh web` 一起拖慢)**,另带两件早已排定的小事。
|
|
9
|
+
|
|
10
|
+
### 症状与根因(都是实测值,不是估算)
|
|
11
|
+
|
|
12
|
+
- `GET /plugins/dsh-expert-team/state` 实测 **热态 7.1–9.8 s、冷态 283.6 s**,而面板**绘制只要 52 ms**。
|
|
13
|
+
- **根因 D(最大头)**:端点对**每个**子会话 `await childSessionTiming`,而它对"活存储里查不到"的子代理
|
|
14
|
+
退回 `sessionQuery.readSession(id)` —— 那是**全量读该子会话日志**。本机实测:单条最大日志
|
|
15
|
+
**23.4 MiB(70,008 个 zstd frame)→ 解压 0.44 s / 45.4 MiB / 94,820 个事件**;21 条 >1MB 的会话日志
|
|
16
|
+
仅"解压 + 过管道"就要 **1.11 s**(纯磁盘下限;host 还要逐行 `JSON.parse` + `structuredClone` + replay)。
|
|
17
|
+
- **根因 B**:`WF_EVENT_TTL_MS` 原来是 **3000 ms —— 恰好等于默认轮询间隔 3000 ms** ⇒ **每个 tick 必失效**,
|
|
18
|
+
父会话日志(多 MB)被一遍遍全量重读。
|
|
19
|
+
- **根因 H3**:客户端**没有 single-flight** —— 实测 8 秒内发出 5 个 `/state`、其中 **4 个重叠**(上一个没回就发下一个)。
|
|
20
|
+
- **后果(这才最要紧)**:这些同步 CPU(≈2.3 s/请求)压在事件循环上,把**整个 web** 拖慢 ——
|
|
21
|
+
轻量端点 `/settings` 实测被拖到 **20.7 s / 42.5 s**(关掉面板后仍在排队,因为积压已经形成)。
|
|
22
|
+
|
|
23
|
+
### 修复
|
|
24
|
+
|
|
25
|
+
- **热点 D**:新增 `subHeaderIndex()` —— 子会话时间**只查表、永不读日志**。三个来源都便宜:
|
|
26
|
+
① `listSubagentStatusBySession` 的 `listSessions()` 分支本来就顺带带出了 `createdAt/parentId/depth`;
|
|
27
|
+
② 活存储 `ctx.sessions.get(id).header`;③ workflow 事件流的 `startedAt`(父会话日志已按 TTL 缓存)。
|
|
28
|
+
`/state` 的两个计时调用点全部改用它。三源都拿不到仍**如实降级** `0/''/0`(UI 显示「无时间记录」),**不臆造**。
|
|
29
|
+
- **热点 B**:`WF_EVENT_TTL_MS` 3000 → **30000**(TTL 只是兜底;代价是"run 在会话中途结束"最多晚 30 秒反映到面板)。
|
|
30
|
+
- **客户端**:`load()` 加 **single-flight**(未回绝不发下一个)+ **自适应退避**
|
|
31
|
+
`clamp(max(基础间隔, 上次耗时×2), 基础间隔, 30000)`;`display.pollMs` 仍是基础节奏(设置说了算)。
|
|
32
|
+
- **写侧守卫**:`/state` 只在**该 session 真实存在**时才 `rememberSessionRun` 落盘 —— 诊断时一个**假 id**
|
|
33
|
+
曾被写进 `$DSH_HOME/expert-team/session-runs.json`(污染用户数据)。
|
|
34
|
+
|
|
35
|
+
预计效果:热态从 7.1–9.8 s 降到 **0.15–0.4 s** 量级(以实机复测为准)。
|
|
36
|
+
|
|
37
|
+
### 另两件小事
|
|
38
|
+
|
|
39
|
+
- 设置页页头那句「标着「暂未生效」的项尚未接线…」改为**条件渲染**:`INERT_SETTINGS` 现在是空的,
|
|
40
|
+
无条件渲染会让用户去找一个不存在的标注。判据直接取 hint 里的标记(与后端**同一真源**)。
|
|
41
|
+
- 新增反向参数 **`--one-shot`** / **`--code`**:1.3.4 把设置接成"无 flag 时的默认"之后出现不对称 ——
|
|
42
|
+
设置成 `persist=true` / `artifacts-only` 后,单条命令**没法反悔**。解析改为**三态**
|
|
43
|
+
(`true` 显式要 / `false` 显式不要 / `null` 未表态听设置),优先级仍是 **flag > 设置 > 常量**。
|
|
44
|
+
(曾加过一个 `--no-persist` 别名,被本仓「实现了就必须有文档」的双向一致性断言挡下 ⇒ 去掉别名,一概念一名。)
|
|
45
|
+
|
|
46
|
+
### 护栏(进 `test:all`)
|
|
47
|
+
|
|
48
|
+
新增 `state-perf-guard.test.mjs`:热路径**零次 `readSession`**(spy 计数)、源码里不再有
|
|
49
|
+
`await childSessionTiming(ctx, s.id)`、客户端 single-flight 与退避公式、假 id 不落盘、三态反向参数与优先级。
|
|
50
|
+
|
|
51
|
+
### 1.3.6 待办(诊断列出、本版**有意不做**,避免扩大范围)
|
|
52
|
+
|
|
53
|
+
懒加载分节(`?section=people`)、`roleFromChildLog` 流式首帧、`listPersisted` 475 个文件的 header 缓存、
|
|
54
|
+
`maxSubs` / deadline 截断。
|
|
6
55
|
## 1.3.4
|
|
7
56
|
|
|
8
57
|
**把 1.3.2 留下的 7 个"有持久化、无消费者"的设置真的接上**(display 4 + roster 3),并清掉当时的假承诺。
|
package/client.js
CHANGED
|
@@ -1190,6 +1190,7 @@ window.__ModuleLoader__.load({
|
|
|
1190
1190
|
h('button', { className: 'exp-settings-retry', onClick: function () { setRetry(retry + 1) } }, esc(t('重试', 'Retry'))))
|
|
1191
1191
|
if (!data || !data.schema) return h('div', { className: 'exp-settings' }, h('div', { className: 'exp-empty' }, t('(正在读取设置…)', '(loading settings…)')))
|
|
1192
1192
|
var model = settingsFormModel(data.schema, data.settings)
|
|
1193
|
+
var hasInertMark = model.some(function (g) { return (g.rows || []).some(function (r) { return /暂未生效/.test(String(r.hint || '')) }) })
|
|
1193
1194
|
var rows = []
|
|
1194
1195
|
model.forEach(function (g) {
|
|
1195
1196
|
rows.push(h('div', { key: 'h-' + g.group, className: 'exp-settings-group' }, esc(g.label) + (g.hint ? ' · ' + esc(g.hint) : '')))
|
|
@@ -1222,7 +1223,11 @@ window.__ModuleLoader__.load({
|
|
|
1222
1223
|
// ③ **没有任何一项需要重启** ⇒ 回执只说"已保存"(`needsRestart` 恒 false)。
|
|
1223
1224
|
// ④ 标着「暂未生效」的项 = **还没接线**(`INERT_SETTINGS`,见 lib/settings.js):写在这里
|
|
1224
1225
|
// 不是承诺,而是如实告知;逐项标记由 per-item hint 携带,不在这里重复。
|
|
1225
|
-
|
|
1226
|
+
// 「暂未生效」那句**只在真有这种项时**才渲染(1.3.5):INERT_SETTINGS 现在是空的,
|
|
1227
|
+
// 无条件渲染会让用户去找一个不存在的标注。判据直接取 hint 里的标记 ⇒ 与后端单一真源一致。
|
|
1228
|
+
h('div', { className: 'exp-settings-head' }, esc(hasInertMark
|
|
1229
|
+
? t('改动即保存并即时生效(上限 / 轮次 / 档位门 / 振荡检测开关在进程内重算)。标着「暂未生效」的项尚未接线,改了不会有作用。', 'Saved on change and applied immediately (caps, rounds, the tier gate and the oscillation switch are recomputed in-process). Items marked as not yet in effect are not wired up — changing them does nothing.')
|
|
1230
|
+
: t('改动即保存并即时生效(上限 / 轮次 / 档位门 / 振荡检测开关在进程内重算)。', 'Saved on change and applied immediately (caps, rounds, the tier gate and the oscillation switch are recomputed in-process).'))),
|
|
1226
1231
|
rows,
|
|
1227
1232
|
h('div', { className: 'exp-settings-msg' + (err ? ' bad' : '') }, esc(err ? '✗ ' + err : (msg || ''))))
|
|
1228
1233
|
}
|
|
@@ -1345,7 +1350,22 @@ window.__ModuleLoader__.load({
|
|
|
1345
1350
|
if (selRunV && selRunV.workspace && selRunV.runId) { q.push('workspace=' + encodeURIComponent(selRunV.workspace)); q.push('run=' + encodeURIComponent(selRunV.runId)) }
|
|
1346
1351
|
return url + (q.length ? ('?' + q.join('&')) : '')
|
|
1347
1352
|
}
|
|
1353
|
+
// ── single-flight + 自适应退避(2026-09-15 性能修复)──────────────────────────
|
|
1354
|
+
// 实测:8 秒内曾发 5 个 /state、其中 4 个重叠,而单发可达 7–283 秒 ⇒ 队列只增不减,
|
|
1355
|
+
// 服务端被压垮(连轻量端点 /settings 都涨到 20.7 s / 42.5 s,整个 dsh web 一起卡)。
|
|
1356
|
+
// 两条纪律:① **未回绝不发下一个**(晚一点看到状态,好过把 web 拖死);
|
|
1357
|
+
// ② 间隔随上次耗时**退避**(见 reschedule),重活端点不该 1.2 秒一发。
|
|
1358
|
+
var inFlightRef = useRef(false)
|
|
1359
|
+
var lastMsRef = useRef(0)
|
|
1348
1360
|
function load() {
|
|
1361
|
+
if (inFlightRef.current) return
|
|
1362
|
+
inFlightRef.current = true
|
|
1363
|
+
var t0 = (window.performance && performance.now) ? performance.now() : Date.now()
|
|
1364
|
+
var done = function () {
|
|
1365
|
+
inFlightRef.current = false
|
|
1366
|
+
var now = (window.performance && performance.now) ? performance.now() : Date.now()
|
|
1367
|
+
lastMsRef.current = Math.max(0, now - t0)
|
|
1368
|
+
}
|
|
1349
1369
|
fetch(stateUrl()).then(function (r) { return r.ok ? r.json() : null }).then(function (d) {
|
|
1350
1370
|
if (d && d.ok) {
|
|
1351
1371
|
setData(d); setErr(''); reschedule(d)
|
|
@@ -1380,16 +1400,20 @@ window.__ModuleLoader__.load({
|
|
|
1380
1400
|
} catch (e) {}
|
|
1381
1401
|
} else if (d && !d.ok && d.runsAvailable === 0) { setData(null); if (d.runs) setRunsMeta(d.runs); setErr(t('还没有专家团 run。先运行 /team <task> 开一个。', 'No team run yet — run /team <task> first.')) }
|
|
1382
1402
|
else if (d && !d.ok) { if (d.runs) setRunsMeta(d.runs); setErr(d.error || ''); }
|
|
1383
|
-
}).catch(function () {})
|
|
1403
|
+
}).catch(function () {}).then(done, done)
|
|
1384
1404
|
}
|
|
1385
1405
|
// Adaptive refresh: while any member is running, poll fast (workers are
|
|
1386
|
-
// touching files right now); otherwise
|
|
1406
|
+
// touching files right now); otherwise the configured cadence.
|
|
1407
|
+
// 2026-09-15:再叠一层**退避** —— 间隔 = clamp(max(基础间隔, 上次耗时×2), 基础间隔, 30000)。
|
|
1408
|
+
// 单发越慢 ⇒ 下次越晚;display.pollMs 仍是基础节奏(设置里那一项依然说了算)。
|
|
1387
1409
|
function reschedule(d) {
|
|
1388
1410
|
var m = (d && d.members) || {}
|
|
1389
1411
|
var busy = Object.keys(m).some(function (k) { var v = m[k]; return v && typeof v === 'object' && (v.activity === 'running' || v.shortStatus === 'running') })
|
|
1390
1412
|
clearInterval(pollRef.current)
|
|
1391
1413
|
// 1.3.4:间隔来自设置 display.pollMs(忙碌时按同一意图加速到 40%,下限 300ms)
|
|
1392
|
-
|
|
1414
|
+
var base = busy ? Math.max(300, Math.round(dispCfg.pollMs * 0.4)) : dispCfg.pollMs
|
|
1415
|
+
var backoff = Math.max(base, Math.min(30000, Math.round((lastMsRef.current || 0) * 2)))
|
|
1416
|
+
pollRef.current = setInterval(load, backoff)
|
|
1393
1417
|
}
|
|
1394
1418
|
useEffect(function () {
|
|
1395
1419
|
if (!isOpen && !viewMode) return undefined // panel closed & not canvas → no polling
|
package/lib/command-parse.js
CHANGED
|
@@ -29,6 +29,8 @@ export const USAGE = [
|
|
|
29
29
|
' /team help 显示本用法(等价:? / -h / --help)',
|
|
30
30
|
' /team --persist <task> 启动持久化活团队(成员可反复指挥,跨会话恢复)',
|
|
31
31
|
' /team --no-code <task> 只产出计划/评审/测试工件,不改代码',
|
|
32
|
+
' /team --code <task> 显式要「代码 + 工件」(压过设置里的「交付口径 = 只出工件」)',
|
|
33
|
+
' /team --one-shot <task> 显式要「一次性组队」(压过设置里的「默认持久化活团队」)',
|
|
32
34
|
' /team --confirm <task> 大需求先确认:建 run 但**不自动派工**,面板点「执行」才开工',
|
|
33
35
|
' /team --tier <档位> <task> 指定流程档位:快速档 / 标准档 / 严格档(不写则在浮层三选一,每次都问)',
|
|
34
36
|
' /team tier <档位> [--run <run>] 运行中**升档**(快速档→标准档→严格档;不允许降档)',
|
|
@@ -145,8 +147,11 @@ export function parseTeamCommand(rawInput) {
|
|
|
145
147
|
// Consume leading flags; everything after the last flag is the task text.
|
|
146
148
|
const tokens = input.split(/\s+/);
|
|
147
149
|
let i = 0;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
// 三态(1.3.5):null = 未表态(听设置);true = 显式要;false = **显式不要**(--one-shot / --code)。
|
|
151
|
+
// 为什么必须三态:1.3.4 把设置接进"无 flag 时的默认"后就出现不对称 —— 设置成 persist=true 之后,
|
|
152
|
+
// 单条命令没有任何办法要一次性运行(旧代码的布尔默认把"没给 flag"和"显式否"混成同一个 false)。
|
|
153
|
+
let persist = null;
|
|
154
|
+
let noCode = null;
|
|
150
155
|
let roles = null;
|
|
151
156
|
let runName = null;
|
|
152
157
|
let profile = null;
|
|
@@ -156,7 +161,9 @@ export function parseTeamCommand(rawInput) {
|
|
|
156
161
|
while (i < tokens.length && tokens[i].startsWith('--')) {
|
|
157
162
|
const flag = tokens[i];
|
|
158
163
|
if (flag === '--persist') persist = true;
|
|
164
|
+
else if (flag === '--one-shot') persist = false;
|
|
159
165
|
else if (flag === '--no-code') noCode = true;
|
|
166
|
+
else if (flag === '--code') noCode = false;
|
|
160
167
|
else if (flag === '--allow-rebuild') allowRebuild = true;
|
|
161
168
|
else if (flag.startsWith('--roles')) {
|
|
162
169
|
const eq = flag.indexOf('=');
|
package/lib/command.js
CHANGED
|
@@ -1414,6 +1414,21 @@ function sessionRunsPath() { return join(dshHome(), 'expert-team', 'session-runs
|
|
|
1414
1414
|
* 旧数据没有 `via`;`runOwnerSession()` 会把"最早那条"当作归属兜底,并对
|
|
1415
1415
|
* 是否真的可信如实回报(见 `ownerResolved`)。
|
|
1416
1416
|
*/
|
|
1417
|
+
/**
|
|
1418
|
+
* 该 session id 是否**真实存在**(写侧守卫,2026-09-15)。
|
|
1419
|
+
*
|
|
1420
|
+
* 为什么需要:/state 允许带任意 `sessionId` 查询,而旧实现在"显式指定 run+workspace"时
|
|
1421
|
+
* **无条件** rememberSessionRun(sid, …) 落盘 —— 性能诊断时用一个**假 id** 就被写进了
|
|
1422
|
+
* `$DSH_HOME/expert-team/session-runs.json`(污染用户数据)。
|
|
1423
|
+
* 只查活存储(便宜、不读日志);查不到 ⇒ 当只读、**不写盘**。
|
|
1424
|
+
* 代价:已结束的会话"查看某 run"不再被记住(面板退回"按当前工作区自动选 run")——
|
|
1425
|
+
* 这是**有意**的取舍:宁可不记,也不写假 id。
|
|
1426
|
+
*/
|
|
1427
|
+
function sessionExists(ctx, sid) {
|
|
1428
|
+
const id = String(sid || '');
|
|
1429
|
+
if (!id) return false;
|
|
1430
|
+
try { return Boolean(ctx?.get?.('sessions')?.get?.(id)); } catch { return false; }
|
|
1431
|
+
}
|
|
1417
1432
|
function rememberSessionRun(sid, ws, runId, via) {
|
|
1418
1433
|
if (!sid || !ws || !runId) return;
|
|
1419
1434
|
const prev = SESSION_RUNS.get(sid);
|
|
@@ -1741,8 +1756,10 @@ export function resolveRosterDefaults(mode, rosterSet, fallbackRoles) {
|
|
|
1741
1756
|
? rosterSet['roster.defaultRoles'].slice() : null;
|
|
1742
1757
|
const explicitRoles = Array.isArray(m.roles) && m.roles.length ? m.roles.slice() : null;
|
|
1743
1758
|
return {
|
|
1744
|
-
|
|
1745
|
-
|
|
1759
|
+
// 三态(1.3.5):true = 显式要;false = **显式不要**(--one-shot / --code 压过设置);
|
|
1760
|
+
// null/undefined = 未表态 ⇒ 听设置(见 command-parse.js 的三态注释)。
|
|
1761
|
+
persist: m.persist === true ? true : (m.persist === false ? false : Boolean(rosterSet && rosterSet['roster.persist'] === true)),
|
|
1762
|
+
noCode: m.noCode === true ? true : (m.noCode === false ? false : Boolean(rosterSet && rosterSet['roster.deliverable'] === 'artifacts-only')),
|
|
1746
1763
|
roles: explicitRoles || settingsRoles,
|
|
1747
1764
|
rolesBase: settingsRoles || base,
|
|
1748
1765
|
rolesFromSettings: !explicitRoles && Boolean(settingsRoles),
|
|
@@ -2723,6 +2740,53 @@ async function childSessionTiming(ctx, id) {
|
|
|
2723
2740
|
return out;
|
|
2724
2741
|
}
|
|
2725
2742
|
|
|
2743
|
+
/**
|
|
2744
|
+
* 子会话 header **索引**:只查表,**永不读日志**。
|
|
2745
|
+
*
|
|
2746
|
+
* 为什么必须有它(2026-09-15 性能诊断):/state 曾对**每个**子会话调 childSessionTiming,
|
|
2747
|
+
* 而它对"活存储里已查不到"的子代理会退回 sessionQuery.readSession(id) —— 那是**全量读该子会话日志**
|
|
2748
|
+
* (本机实测 21 条 = 23.6 MiB zstd / ~31,000 事件;别的 run 测到 75/92 条)。结果:端点热态 7.1–9.8 s、
|
|
2749
|
+
* 冷态 **283.6 s**,而面板绘制只要 52 ms;请求还压在事件循环上,把整个 dsh web 一起拖慢
|
|
2750
|
+
* (轻量端点 /settings 因此涨到 20.7 s / 42.5 s)。
|
|
2751
|
+
*
|
|
2752
|
+
* 三个来源都**便宜**,按"准且省"排序:
|
|
2753
|
+
* ① listSubagentStatusBySession 已经带出的 durable header 字段(createdAt/parentId/depth)
|
|
2754
|
+
* —— 那一支本来就要 listSessions(),字段是**顺带**拿到的,不额外读任何日志;
|
|
2755
|
+
* ② 活存储 ctx.sessions.get(id).header(子代理还在跑时最省);
|
|
2756
|
+
* ③ workflow 事件流的 startedAt(父会话日志已按 WF_EVENT_TTL_MS 缓存,见 workflowEventIndex)。
|
|
2757
|
+
* 三源都拿不到 ⇒ 保持 0/''/0,UI 按「无时间记录」**如实降级**(hasTimestamp:false)——
|
|
2758
|
+
* **不臆造**,也**绝不**为了"看起来有数据"退回全量读日志。
|
|
2759
|
+
*
|
|
2760
|
+
* @returns {Map<string, {createdAt:number, parentId:string, depth:number}>}
|
|
2761
|
+
*/
|
|
2762
|
+
function subHeaderIndex(ctx, subs, wfLabels) {
|
|
2763
|
+
const out = new Map();
|
|
2764
|
+
for (const s of Array.isArray(subs) ? subs : []) {
|
|
2765
|
+
const id = String((s && s.id) || '');
|
|
2766
|
+
if (!id) continue;
|
|
2767
|
+
let createdAt = Number(s.createdAt) || 0;
|
|
2768
|
+
let parentId = String(s.parentId || '');
|
|
2769
|
+
let depth = Number(s.depth) || 0;
|
|
2770
|
+
if (!createdAt) {
|
|
2771
|
+
try {
|
|
2772
|
+
const hdr = ctx?.get?.('sessions')?.get?.(id)?.header;
|
|
2773
|
+
if (hdr) {
|
|
2774
|
+
createdAt = Number(hdr.createdAt) || 0;
|
|
2775
|
+
if (!parentId) parentId = String(hdr.parentSession || '');
|
|
2776
|
+
if (!depth) depth = Number(hdr.delegationDepth) || 0;
|
|
2777
|
+
}
|
|
2778
|
+
} catch { /* best-effort */ }
|
|
2779
|
+
}
|
|
2780
|
+
if (!createdAt && wfLabels) {
|
|
2781
|
+
const w = typeof wfLabels.get === 'function' ? wfLabels.get(id) : wfLabels[id];
|
|
2782
|
+
const at = w && Number(w.startedAt);
|
|
2783
|
+
if (at) createdAt = at;
|
|
2784
|
+
}
|
|
2785
|
+
out.set(id, { createdAt, parentId, depth });
|
|
2786
|
+
}
|
|
2787
|
+
return out;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2726
2790
|
async function listSubagentStatusBySession(ctx, sid, knownIds) {
|
|
2727
2791
|
const root = sid ? await rootSessionId(ctx, sid) : '';
|
|
2728
2792
|
let rows = [];
|
|
@@ -2900,7 +2964,11 @@ async function roleFromChildLog(ctx, id) {
|
|
|
2900
2964
|
* tick, while a run that finishes mid-session still converges within a tick.
|
|
2901
2965
|
* Best-effort: any read failure yields empty results (never throws).
|
|
2902
2966
|
*/
|
|
2903
|
-
|
|
2967
|
+
// ⚠️ 2026-09-15 性能诊断:TTL 原为 **3000ms,恰好等于默认轮询间隔 3000ms** ⇒ **每个 tick 必失效**,
|
|
2968
|
+
// 父会话日志(多 MB)被一遍遍全量重读,这是热态 7.1–9.8 s 的主要来源之一。现改为 30000ms:
|
|
2969
|
+
// TTL 只是**兜底**(理想是按日志 mtime/size 失效,留待后续),代价是"run 在会话中途结束"最多晚
|
|
2970
|
+
// 30 秒反映到面板 —— 与"每 3 秒重读整个日志"相比,这个交换值得。
|
|
2971
|
+
const WF_EVENT_TTL_MS = 30000;
|
|
2904
2972
|
const WF_EVENT_CACHE = new Map(); // sessionId -> { at, labels: Map, runs: [] }
|
|
2905
2973
|
async function workflowEventIndex(ctx, sid) {
|
|
2906
2974
|
const key = String(sid || '');
|
|
@@ -4581,7 +4649,7 @@ function warnLeadToolFaceOnce(status, detail) {
|
|
|
4581
4649
|
LEAD_TOOLFACE_WARNED.add(status);
|
|
4582
4650
|
console.warn(`[expert-team] lead 工具面**未**收窄(${status}):${detail}`);
|
|
4583
4651
|
}
|
|
4584
|
-
export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, buildRoleSubMap, resolveSubRoles, childSessionTiming, SUB_HEADER_CACHE, workflowEventIndex, workflowChildLabels, workflowChildMeta, workflowRuns, WF_EVENT_CACHE, rememberSessionRun, sessionRunFor, runOwnerSession, SESSION_RUNS, parseTeamCommand, deriveMemberEntries, schemaViolations, runHealth, RUN_STALL_MS, scaffoldFingerprint, SCAFFOLD_REQUIRED, strandedTasks, settleStranded, IN_FLIGHT_STATUSES, normalizeCoverage, SCHEMA_WARN_SEEN, pushActivityEvent, DEFAULT_LIMITS, LIMITS, resolveLimits, capacityViolations, DEFAULT_ROUND_LIMITS, ROUND_LIMITS, ROUND_LIMIT_ENV, resolveRoundLimits, ROUND_LIMIT_OF_KIND, roundOf, isQualityTask, normTitle, roundLimitViolations, reworkLoopWriteGuard, mutateTasks, readStandingRules, appendStandingRule, rulesRun, scopeOverlapWarnings, applyTaskStatus, waitRun, eventFamily, verdictFromToken, normalizeRoleName, truncateCodepoints, filterRunScopedSubs, runCreatedAtMs, runLogTail, liveFiles, LIVE_FILES_CACHE, DEFAULT_ROLES, resolveTierGate, TIER_GATE_ENV, snapshotRun, settingsPath, loadSettingsSync, currentSettings, limitsBaseFromSettings, roundLimitsBaseFromSettings, effectiveTierGate: () => TIER_GATE, ensureSkillInstalled, ensurePresetInstalled, uninstallInstalled, buildSkillRegistration, parseSkillMarkdown, PLUGIN_VERSION, INSTALL_STAMP, runtimeSkillRegistered: () => RUNTIME_SKILL_REGISTERED, agentScopedToolNames, warnLeadToolFaceOnce, resolveLeadToolFace, LEAD_TOOLFACE_ENV, effectiveLeadToolFace: () => LEAD_TOOLFACE, resolveLoopGuard, LOOP_GUARD_ENV, effectiveLoopGuard: () => LOOP_GUARD_ENABLED, resolveRosterDefaults, rosterSettings, createRun, watchScript, canvasPollMs, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings };
|
|
4652
|
+
export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, buildRoleSubMap, resolveSubRoles, childSessionTiming, subHeaderIndex, sessionExists, SUB_HEADER_CACHE, workflowEventIndex, workflowChildLabels, workflowChildMeta, workflowRuns, WF_EVENT_CACHE, rememberSessionRun, sessionRunFor, runOwnerSession, SESSION_RUNS, parseTeamCommand, deriveMemberEntries, schemaViolations, runHealth, RUN_STALL_MS, scaffoldFingerprint, SCAFFOLD_REQUIRED, strandedTasks, settleStranded, IN_FLIGHT_STATUSES, normalizeCoverage, SCHEMA_WARN_SEEN, pushActivityEvent, DEFAULT_LIMITS, LIMITS, resolveLimits, capacityViolations, DEFAULT_ROUND_LIMITS, ROUND_LIMITS, ROUND_LIMIT_ENV, resolveRoundLimits, ROUND_LIMIT_OF_KIND, roundOf, isQualityTask, normTitle, roundLimitViolations, reworkLoopWriteGuard, mutateTasks, readStandingRules, appendStandingRule, rulesRun, scopeOverlapWarnings, applyTaskStatus, waitRun, eventFamily, verdictFromToken, normalizeRoleName, truncateCodepoints, filterRunScopedSubs, runCreatedAtMs, runLogTail, liveFiles, LIVE_FILES_CACHE, DEFAULT_ROLES, resolveTierGate, TIER_GATE_ENV, snapshotRun, settingsPath, loadSettingsSync, currentSettings, limitsBaseFromSettings, roundLimitsBaseFromSettings, effectiveTierGate: () => TIER_GATE, ensureSkillInstalled, ensurePresetInstalled, uninstallInstalled, buildSkillRegistration, parseSkillMarkdown, PLUGIN_VERSION, INSTALL_STAMP, runtimeSkillRegistered: () => RUNTIME_SKILL_REGISTERED, agentScopedToolNames, warnLeadToolFaceOnce, resolveLeadToolFace, LEAD_TOOLFACE_ENV, effectiveLeadToolFace: () => LEAD_TOOLFACE, resolveLoopGuard, LOOP_GUARD_ENV, effectiveLoopGuard: () => LOOP_GUARD_ENABLED, resolveRosterDefaults, rosterSettings, createRun, watchScript, canvasPollMs, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings };
|
|
4585
4653
|
|
|
4586
4654
|
export function apply(ctx, config) {
|
|
4587
4655
|
// 留一份 config:设置在运行时改变(官方面板 / 浮层)时要**用同一份 config** 重算上限与档位门,
|
|
@@ -4870,7 +4938,8 @@ export function apply(ctx, config) {
|
|
|
4870
4938
|
let sel = null;
|
|
4871
4939
|
if (runParam && wsParam) {
|
|
4872
4940
|
sel = await snapshotRun(wsParam, runParam);
|
|
4873
|
-
|
|
4941
|
+
// 只在**该会话真实存在**时才记忆(假 id 不得落盘,见 sessionExists)
|
|
4942
|
+
if (sel && sessionExists(ctx, sid)) rememberSessionRun(sid, wsParam, runParam, 'view'); // user deliberately picked a run
|
|
4874
4943
|
} else if (runParam && workspaces.length === 1) {
|
|
4875
4944
|
sel = await snapshotRun(workspaces[0], runParam);
|
|
4876
4945
|
} else {
|
|
@@ -4947,10 +5016,8 @@ export function apply(ctx, config) {
|
|
|
4947
5016
|
if (membersFromState(sel.stateMembers).byRole.size) {
|
|
4948
5017
|
subById = buildRoleSubMap(subs, sel.stateMembers, wfLabels); // 已登记:不过滤、不丢
|
|
4949
5018
|
} else {
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
try { timingOfSubs.set(String(s.id || ''), await childSessionTiming(ctx, s.id)); } catch { /* best-effort */ }
|
|
4953
|
-
}
|
|
5019
|
+
// 只查表、不读日志(热点 D 修复):见 subHeaderIndex 的注释。
|
|
5020
|
+
const timingOfSubs = subHeaderIndex(ctx, subs, wfLabels);
|
|
4954
5021
|
const scoped = filterRunScopedSubs(subs, {
|
|
4955
5022
|
wfLabels,
|
|
4956
5023
|
runId: sel.runId,
|
|
@@ -5010,12 +5077,14 @@ export function apply(ctx, config) {
|
|
|
5010
5077
|
}
|
|
5011
5078
|
// 时间维度(任何"分批/波次"视图都需要):createdAt 取自子会话 header,
|
|
5012
5079
|
// lastEventAt 复用下面已收集的 per-agent feed 最后一条事件时间(零额外开销)。
|
|
5013
|
-
//
|
|
5014
|
-
//
|
|
5015
|
-
//
|
|
5016
|
-
//
|
|
5080
|
+
// 走 subHeaderIndex(**只查表**):已结束的子代理会被从活存储里清掉,但它们的
|
|
5081
|
+
// durable header 字段由 listSubagentStatusBySession 的 listSessions() 分支
|
|
5082
|
+
// **顺带**带出来了(createdAt/parentId/depth),所以不需要再读一次日志。
|
|
5083
|
+
// 2026-09-15:此前这里对每个子会话 await childSessionTiming(会全量 readSession),
|
|
5084
|
+
// 是 /state 冷态 283.6 s 的根因。不臆造:三源都拿不到就保持 0,UI 如实降级。
|
|
5085
|
+
const timingIdx = subHeaderIndex(ctx, subs, wfLabels);
|
|
5017
5086
|
const timingOf = {};
|
|
5018
|
-
for (const s of subs) timingOf[String(s.id || '')] =
|
|
5087
|
+
for (const s of subs) timingOf[String(s.id || '')] = timingIdx.get(String(s.id || '')) || { createdAt: 0, parentId: '', depth: 0 };
|
|
5019
5088
|
sel.agents = subs.map((s) => {
|
|
5020
5089
|
const sid2 = String(s.id || '');
|
|
5021
5090
|
const t = timingOf[sid2] || { createdAt: 0, parentId: '', depth: 0 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yangdcm/dsh-expert-team",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "dsh「专家团」bundle:一句自然语言自动组建/持久化一支 12 角色多智能体团队,共享工作区协议 + 阶段门控编排 + 结构化交接 + 质量门禁/自动调度,实现者直接改代码并产出持久工件;带 live 团队浮层(质量门禁/覆盖率/工件预览)。 · Role-based multi-agent expert team for DeepSeek Harness: one sentence in, a staged and gated team delivery out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"check:name": "node scripts/rename-package.mjs --check",
|
|
85
85
|
"test:regression": "node regression.test.mjs",
|
|
86
86
|
"test:e2e": "node e2e.test.mjs",
|
|
87
|
-
"test:all": "node bootstrap.test.mjs && node host-settings.test.mjs && node smoke.test.mjs && node regression.test.mjs && node e2e.test.mjs && node flow.test.mjs && node plan-decide.test.mjs && node plan-discard.test.mjs && node models-honesty.test.mjs && node evidence-gate.test.mjs && node sync-gate.test.mjs && node artifact-writer.test.mjs && node dag.test.mjs && node metrics.test.mjs && node broken-chain.test.mjs && node schema-warn.test.mjs && node template-copy.test.mjs && node task-binding.test.mjs && node role-merge.test.mjs && node wf-recovery.test.mjs && node panorama.test.mjs && node run-ownership.test.mjs && node role-identity.test.mjs && node command-parse.test.mjs && node member-registry.test.mjs && node schema-check.test.mjs && node run-health.test.mjs && node cordis-fs-port.test.mjs && node scaffold-fingerprint.test.mjs && node preset-lint.test.mjs && node stranded-tasks.test.mjs && node agent-scope-guard.test.mjs && node schema-warn-noise.test.mjs && node tool-card-status.test.mjs && node dag-status.test.mjs && node capacity-limits.test.mjs && node task-cas.test.mjs && node write-lock.test.mjs && node card-stall.test.mjs && node multi-session-and-confirm.test.mjs && node standing-rules.test.mjs && node scope-overlap.test.mjs && node task-cancel-protect.test.mjs && node wait-run.test.mjs && node rework-loop.test.mjs && node client-css-integrity.test.mjs && node interception.test.mjs && node mutation-catalog.test.mjs && node write-bypass-ratchet.test.mjs && node asi-hazards.test.mjs && node dispatch-contract.test.mjs && node closure-ledger.test.mjs && node rework-nature.test.mjs && node loop-guard.test.mjs && node vocab-consistency.test.mjs && node state-no-write.test.mjs && node routes-shared.test.mjs && node metrics-render.test.mjs && node metrics-collect.test.mjs && node first-runnable.test.mjs && node closing-budget.test.mjs && node scan-single-source.test.mjs && node tier.test.mjs && node dispatch-ledger.test.mjs && node tier-gate.test.mjs && node tier-badge.test.mjs && node validate-module.test.mjs && node settings.test.mjs && node settings-page.test.mjs && node policy.test.mjs && node phase-accounting.test.mjs && node log-parse-module.test.mjs && node command-parse-module.test.mjs && node authority.test.mjs && node write-tracer.test.mjs && node token-accounting.test.mjs && node lead-toolface.test.mjs && node settings-consumers.test.mjs && node settings-wiring.test.mjs",
|
|
87
|
+
"test:all": "node bootstrap.test.mjs && node host-settings.test.mjs && node smoke.test.mjs && node regression.test.mjs && node e2e.test.mjs && node flow.test.mjs && node plan-decide.test.mjs && node plan-discard.test.mjs && node models-honesty.test.mjs && node evidence-gate.test.mjs && node sync-gate.test.mjs && node artifact-writer.test.mjs && node dag.test.mjs && node metrics.test.mjs && node broken-chain.test.mjs && node schema-warn.test.mjs && node template-copy.test.mjs && node task-binding.test.mjs && node role-merge.test.mjs && node wf-recovery.test.mjs && node panorama.test.mjs && node run-ownership.test.mjs && node role-identity.test.mjs && node command-parse.test.mjs && node member-registry.test.mjs && node schema-check.test.mjs && node run-health.test.mjs && node cordis-fs-port.test.mjs && node scaffold-fingerprint.test.mjs && node preset-lint.test.mjs && node stranded-tasks.test.mjs && node agent-scope-guard.test.mjs && node schema-warn-noise.test.mjs && node tool-card-status.test.mjs && node dag-status.test.mjs && node capacity-limits.test.mjs && node task-cas.test.mjs && node write-lock.test.mjs && node card-stall.test.mjs && node multi-session-and-confirm.test.mjs && node standing-rules.test.mjs && node scope-overlap.test.mjs && node task-cancel-protect.test.mjs && node wait-run.test.mjs && node rework-loop.test.mjs && node client-css-integrity.test.mjs && node interception.test.mjs && node mutation-catalog.test.mjs && node write-bypass-ratchet.test.mjs && node asi-hazards.test.mjs && node dispatch-contract.test.mjs && node closure-ledger.test.mjs && node rework-nature.test.mjs && node loop-guard.test.mjs && node vocab-consistency.test.mjs && node state-no-write.test.mjs && node routes-shared.test.mjs && node metrics-render.test.mjs && node metrics-collect.test.mjs && node first-runnable.test.mjs && node closing-budget.test.mjs && node scan-single-source.test.mjs && node tier.test.mjs && node dispatch-ledger.test.mjs && node tier-gate.test.mjs && node tier-badge.test.mjs && node validate-module.test.mjs && node settings.test.mjs && node settings-page.test.mjs && node policy.test.mjs && node phase-accounting.test.mjs && node log-parse-module.test.mjs && node command-parse-module.test.mjs && node authority.test.mjs && node write-tracer.test.mjs && node token-accounting.test.mjs && node lead-toolface.test.mjs && node settings-consumers.test.mjs && node settings-wiring.test.mjs && node state-perf-guard.test.mjs",
|
|
88
88
|
"test:toolcard": "node tool-card-status.test.mjs",
|
|
89
89
|
"test:flow": "node flow.test.mjs",
|
|
90
90
|
"test:decide": "node plan-decide.test.mjs",
|