@yangdcm/dsh-expert-team 1.3.9 → 1.3.11
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 +120 -0
- package/client.js +96 -45
- package/lib/command-parse.js +1 -0
- package/lib/command.js +374 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,126 @@
|
|
|
3
3
|
本包遵循[语义化版本](https://semver.org/lang/zh-CN/)。dsh 宿主版本线的对应关系写在
|
|
4
4
|
`package.json` 的 `engines.dsh` 与 `dsh.compatibility` 里,插件市场按它判断"这个插件跟你的宿主兼不兼容"。
|
|
5
5
|
|
|
6
|
+
## 1.3.11
|
|
7
|
+
|
|
8
|
+
**`/state` 的 `subs` / `roles` 两段收口 —— 这次不靠预测,靠 1.3.10 留下的分步计时开关实测定位。**
|
|
9
|
+
父会话在真机上带 `DSH_EXPERT_TEAM_STATE_PROFILE=1` 复测,拿到确切归属(同一台机器、同一 session、
|
|
10
|
+
连续两次请求):
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
totalMs 2222 / 2047
|
|
14
|
+
profile.runs+select 21 / 15 ms ✓ 便宜
|
|
15
|
+
profile.subs 1327 / 1388 ms ✗ 主犯
|
|
16
|
+
profile.wfLabels 0 / 0 ms ✓
|
|
17
|
+
profile.roles 869 / 638 ms ✗ 次犯
|
|
18
|
+
profile.tail 3 / 3 ms ✓
|
|
19
|
+
rolesBudget: {per:4, left:0, deferred: 16 → 40} ← deferred 在**涨**
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### ① `subs` 段:按 session id 备忘(并纠正两处"看起来像缓存、其实没用"的设置)
|
|
23
|
+
|
|
24
|
+
- **根因**:1.3.10 的"只在真缺人时才查"**没真正省下钱** —— 已结束/历史子会话本来就不在活注册表里,
|
|
25
|
+
`missingIds` 每请求都非空 ⇒ 每请求都退化成对 **475 个 artifact 逐个读 header** 的枚举;
|
|
26
|
+
而当时设的 **2 s TTL 恰好等于真实请求间隔**(轮询 + 退避),**必然过期** ⇒ 等于没有缓存。
|
|
27
|
+
- **改法**:新增 `SUB_HEADER_MEMO`(按 session id 永久记住 `createdAt/parentId/depth`)——
|
|
28
|
+
这条分支上的 id 恰恰是"活注册表里查不到"的那些,其 header 不可变,记住它们不会让用户看到旧状态;
|
|
29
|
+
枚举本身的主失效键改为 **`(sessions 根目录, mtimeMs, size)`**(`sessionsRootStamp()`,实测 0.56 ms),
|
|
30
|
+
TTL 只作兜底并放宽到 **60 s**。
|
|
31
|
+
- **口径不变**:**活代理一律实时读,绝不进任何缓存**(有断言守着:活代理不写入备忘)。
|
|
32
|
+
- **折算前后**(单位成本取自上面的真机 profile:一次枚举 ≈ 1500–2400 ms,本机取 1800 ms):
|
|
33
|
+
**旧:每请求 ≈ 1800 ms;新:第 1 次 1800 ms,第 2 次起 0.02–0.09 ms** ⇒ 热态 `subs` ≈ **0 ms**。
|
|
34
|
+
|
|
35
|
+
### ② `roles` 段:限次摊平改成**跨请求推进的队列**
|
|
36
|
+
|
|
37
|
+
- **根因**:1.3.10 每请求都把候选集**从零重算**,预算永远喂给队首同几条,而新派的子代理不断出现在队尾
|
|
38
|
+
⇒ **进度不前进、`deferred` 只增不减**(实测 16 → 40)。"限次摊平"退化成"永远摊不完"。
|
|
39
|
+
- **改法**:模块级 `ROLE_PENDING` 队列 —— 每请求按"当前 subs 里仍未缓存"的顺序**重建队列**
|
|
40
|
+
(已缓存的自动掉队、既有顺序保留、新人追加队尾),再从队首消费 ≤ 4 条;角色结果**永久缓存**
|
|
41
|
+
(对已结束会话不可变)。`resetRoleReadBudget()` 只复位**本请求额度**,**不丢队列进度**。
|
|
42
|
+
- **两种零仍然分得开**:`deferred` = **还没解析**(队列长度,单调不增);`unresolved` = **解析不出来**
|
|
43
|
+
(读过但日志里没有角色)—— 新增这个数,正是为了不让两者混成一个。
|
|
44
|
+
- **实测(21 个子会话,每次 4 条预算)**:`deferred` **17 → 13 → 9 → 5 → 1 → 0**;
|
|
45
|
+
6 个 tick 后**零次日志读**。折算:**旧 860 ms/请求且永不收敛 → 收敛后 ≈ 0 ms**。
|
|
46
|
+
|
|
47
|
+
### ③ 推荐插件自检的**假警报**(用户贴的启动日志直接坐实)
|
|
48
|
+
|
|
49
|
+
- **现象**:日志说 `@vectorize-io/hindsight-coding-agents` 与 `dsh-cost-meter`"已安装但当前不可用 ⇒
|
|
50
|
+
`dsh plugin … add`",**紧跟着 `dsh-cost-meter` 自己就加载成功了** ⇒ 判定是错的、建议是误导的。
|
|
51
|
+
- **根因(两条)**:① 我们的 `apply()` 跑在其它插件之前,那一刻 `ctx.get('costMeter')`/工具表里当然还没有它们;
|
|
52
|
+
② 对 `installed-not-ready` 也拼了安装命令,等于叫用户装一个**已经装好**的东西。
|
|
53
|
+
- **宿主没有 app 级 ready 事件**(已核源码):`dsh-app-boot` 的 `boot()` 顺序是
|
|
54
|
+
`mountRootInclude → await ctx.get('loader').await() → assertEntriesActivated → return`,
|
|
55
|
+
期间不 emit 任何"就绪"事件;而 `loader.await()` 我们**不能用** —— 那棵树包含我们自己的挂载任务,
|
|
56
|
+
在 init 里 await 它会**自等死锁**。
|
|
57
|
+
- **改法**:`scheduleOptionalPluginCheck()` —— **有界延迟重探**(250/1000/3000 ms,最后一次才下结论)
|
|
58
|
+
+ **`ctx.inject([服务])` 事件驱动**(服务一出现立刻重探;服务始终不出现则回调不触发,零副作用);
|
|
59
|
+
外加 **`/team help` 懒重探**(刚装上/刚修好配置不必为了这行提示再重启)。
|
|
60
|
+
- **文案分档**:`missing` ⇒ 给安装命令;`installed-not-ready` ⇒ 只说"已安装但当前未就绪(可能未配置或被关闭)",
|
|
61
|
+
**不再给安装命令**;`unknown` ⇒ 不出现(四态与"两种零可区分"的语义不变)。
|
|
62
|
+
|
|
63
|
+
### 验证与口径
|
|
64
|
+
|
|
65
|
+
- `npm run test:all` **EXIT=0**(**80** 个测试文件;`state-perf-guard` 扩到 37 条断言:备忘只枚举一次、
|
|
66
|
+
活代理不入备忘、目录戳失效、队列跨请求推进、deferred 单调不增、额度复位不丢进度、
|
|
67
|
+
`installed-not-ready` 不含安装命令、服务晚挂不再误报、就绪后重探与懒重探的接线)。
|
|
68
|
+
- **诚实边界**:上面"折算前后"的数字是**用父会话真机实测的单位成本 × 新代码的调用次数**折算的
|
|
69
|
+
(本机宿主仍跑 1.3.10,我无法把新代码装进运行中的进程)。**端到端 `totalMs` 请装完 1.3.11 后带
|
|
70
|
+
`DSH_EXPERT_TEAM_STATE_PROFILE=1` 复测**:预期 `subs`/`roles` 两段都接近 0,
|
|
71
|
+
`totalMs` 由 `runs+select`(~20 ms)+ `tail`(~3 ms)主导。
|
|
72
|
+
|
|
73
|
+
## 1.3.10
|
|
74
|
+
|
|
75
|
+
**四项一次收口:推荐插件自检 · 画布轮询也走 single-flight · 底盘缓存 · 角色解析限次。**
|
|
76
|
+
这一版全部由**真机实测**驱动,改动都带可复现的判据(下面每条都写明"改前 → 目标")。
|
|
77
|
+
|
|
78
|
+
### ① 启动时自检推荐插件(缺了说一句,装了不吭声)
|
|
79
|
+
|
|
80
|
+
- 事实前提(只读调查):本插件**零硬依赖** —— `package.json` 连 `dependencies` 字段都没有,
|
|
81
|
+
全库没有一条外部 import;点名到的外部插件只有 `@vectorize-io/hindsight-coding-agents`(工具名
|
|
82
|
+
`hindsight_*`)与 `dsh-cost-meter`,**缺了都不会报错**,只是少了对应体验。
|
|
83
|
+
- `detectOptionalPlugins(ctx)`(**纯读、无副作用**)三条探测路径:
|
|
84
|
+
① `ctx.get('loader').entries()` 的 `options.name` = **装没装**;
|
|
85
|
+
② `ctx.get('costMeter')`(服务名是**驼峰**)= 费用插件是否可用;
|
|
86
|
+
③ `ctx.get('tools').get('hindsight_ingest_document')` = **此刻能不能用** —— Hindsight 在
|
|
87
|
+
"装了但未配置/被 opt-out"时**根本不注册工具**,所以这一条天然把「没装」与「装了没配」分开。
|
|
88
|
+
- 状态四态:`ready` / `missing` / `installed-not-ready` / `unknown`(loader 探测不可用 + 服务也
|
|
89
|
+
拿不到时**不假装知道**,且不进提示)。
|
|
90
|
+
- 加载时**最多一行**(进程内去重),都齐则**完全不打扰**;`/team help` 里加一行说明。
|
|
91
|
+
|
|
92
|
+
### ② 客户端的**所有** /state 轮询统一走一个 hub(上一版只护住了面板)
|
|
93
|
+
|
|
94
|
+
- 实测问题:面板一套 `load()`(1.3.5 已加守卫),而 `HeaderButton` 徽章与画布另走 `liveStore` 的
|
|
95
|
+
`setInterval(liveTick, 2500)` —— **画布一开就有 2–3 个轮询并发压同一个重端点**:10 秒 **7 发**、
|
|
96
|
+
**6 次重叠**,单发被拖到 **5,891 / 8,347 ms**(并发叠加服务端约 2 s 的同步 CPU)。
|
|
97
|
+
- 现在只有 `stateHub` 一个时钟、一份在飞表:**同 URL 同刻只跑一次**(后来者复用在飞 promise)、
|
|
98
|
+
全局单一时钟、统一退避 `clamp(max(基础间隔, 最慢一次×2), 基础间隔, 30000)`,
|
|
99
|
+
团队有人跑时基础间隔按 40% 加速(面板原有意图,现在对徽章/画布同样生效)。
|
|
100
|
+
- 结构性护栏:`client.js` 里**不再存在**任何裸的 `fetch('/plugins/dsh-expert-team/state…')`
|
|
101
|
+
(唯一调用点在 `stateHubFetch` 内),也**只剩一个** `setInterval(stateHubTick, …)`。
|
|
102
|
+
|
|
103
|
+
### ③ 底盘:`listSessions` 只在真缺人时查 + 并发合并 + 短 TTL
|
|
104
|
+
|
|
105
|
+
- 实测残留:`/state` 热态仍是 **2,887 / 2,603 ms**(诊断点名的"固定底盘 1500–2400 ms")。
|
|
106
|
+
- 两处都遵循"**先证明需要,再花这笔钱**":① 旧实现只要 `knownIds` 非空就无条件
|
|
107
|
+
`listSessions()`(宿主侧 = 枚举全部 artifact、逐个读 header,本机 475 个、~1.5–2.4 s),
|
|
108
|
+
现在先算"活注册表里查不到的人",一个不缺就整段跳过;② 真的要查时**并发合并 + 2 s TTL**。
|
|
109
|
+
- **缓存纪律(写死在注释里)**:只服务"已结束/已释放子代理"这一支(其 header 不再变化);
|
|
110
|
+
**活代理一律走 listChildren/agents 实时读,绝不经过缓存**;失败不毒化缓存。
|
|
111
|
+
- 另加**分步计时**(`DSH_EXPERT_TEAM_STATE_PROFILE=1` 才收集,默认响应体不变):
|
|
112
|
+
`profile: {runs+select, subs, wfLabels, roles, tail, rolesBudget}` —— 让"哪一步还贵"下一次**可测**
|
|
113
|
+
而不是靠猜。
|
|
114
|
+
|
|
115
|
+
### ④ 角色解析:**限次摊平**(不是流式,且不假装流式)
|
|
116
|
+
|
|
117
|
+
- 诊断建议"真流式:读到首条 `user/message` 即停"。**做不到**:宿主公开 API
|
|
118
|
+
(`readSession/listEvents/filterEvents/readEvent`)**没有**投影/限量/流式参数
|
|
119
|
+
(`projectionMode` 只在内部 corpus.read),要"只读首帧"只能碰持久化层的非契约内部方法。
|
|
120
|
+
- 改为**有界**:每次 `/state` 最多读 **4** 条子会话日志(角色结果**永久缓存** ⇒ 几个 tick 内收敛),
|
|
121
|
+
其余留到下一 tick,并把"本轮被推迟"的条数作为 `rolesDeferred` **如实**交给调用方 ——
|
|
122
|
+
「还没解析」与「解析不出来」两件事**分得清**(两种零可区分),**不臆造角色**。
|
|
123
|
+
|
|
124
|
+
> 本版**未**改文档/图片(`README*.md`、`docs/images/*` 是前几版刚定稿的);未改 `files` 字段。
|
|
125
|
+
|
|
6
126
|
## 1.3.9
|
|
7
127
|
|
|
8
128
|
**新增第二张真实画布截图:`事` 视图(任务依赖图)**(纯文档/资产:`lib/`、`client.js` 零改动)。
|
package/client.js
CHANGED
|
@@ -1342,7 +1342,6 @@ window.__ModuleLoader__.load({
|
|
|
1342
1342
|
var draftMeta = useRef({ loadedAt: '', dirty: false })
|
|
1343
1343
|
var planMsgS = useState(''); var planMsg = planMsgS[0], setPlanMsg = planMsgS[1]
|
|
1344
1344
|
var err = useState(''); var errV = err[0], setErr = err[1]
|
|
1345
|
-
var pollRef = useRef(0)
|
|
1346
1345
|
|
|
1347
1346
|
function stateUrl() {
|
|
1348
1347
|
var url = '/plugins/dsh-expert-team/state', q = []
|
|
@@ -1350,23 +1349,12 @@ window.__ModuleLoader__.load({
|
|
|
1350
1349
|
if (selRunV && selRunV.workspace && selRunV.runId) { q.push('workspace=' + encodeURIComponent(selRunV.workspace)); q.push('run=' + encodeURIComponent(selRunV.runId)) }
|
|
1351
1350
|
return url + (q.length ? ('?' + q.join('&')) : '')
|
|
1352
1351
|
}
|
|
1353
|
-
// ──
|
|
1354
|
-
//
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
1357
|
-
//
|
|
1358
|
-
|
|
1359
|
-
var lastMsRef = useRef(0)
|
|
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
|
-
}
|
|
1369
|
-
fetch(stateUrl()).then(function (r) { return r.ok ? r.json() : null }).then(function (d) {
|
|
1352
|
+
// ── 数据到达后的副作用(**本组件不再自带轮询器**;由 stateHub 统一驱动)─────────
|
|
1353
|
+
// 2026-09-15 性能修复 #2:面板原先自己 hold 一个 setInterval + in-flight 守卫,而徽章与
|
|
1354
|
+
// 画布另走 liveStore 的 setInterval ⇒ 画布一开就有 2–3 个轮询并发压同一个重端点
|
|
1355
|
+
// (实测 10 秒 7 发、6 次重叠、单发被拖到 5.9/8.3 s)。现在全部经 stateHub:
|
|
1356
|
+
// 同 URL 同刻只跑一次、全局单一时钟、统一退避(见 stateHub 的注释)。
|
|
1357
|
+
function onState(d) {
|
|
1370
1358
|
if (d && d.ok) {
|
|
1371
1359
|
setData(d); setErr(''); reschedule(d)
|
|
1372
1360
|
// N2: keep the composer takeover in sync with pendingDecision
|
|
@@ -1400,30 +1388,16 @@ window.__ModuleLoader__.load({
|
|
|
1400
1388
|
} catch (e) {}
|
|
1401
1389
|
} 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.')) }
|
|
1402
1390
|
else if (d && !d.ok) { if (d.runs) setRunsMeta(d.runs); setErr(d.error || ''); }
|
|
1403
|
-
}).catch(function () {}).then(done, done)
|
|
1404
|
-
}
|
|
1405
|
-
// Adaptive refresh: while any member is running, poll fast (workers are
|
|
1406
|
-
// touching files right now); otherwise the configured cadence.
|
|
1407
|
-
// 2026-09-15:再叠一层**退避** —— 间隔 = clamp(max(基础间隔, 上次耗时×2), 基础间隔, 30000)。
|
|
1408
|
-
// 单发越慢 ⇒ 下次越晚;display.pollMs 仍是基础节奏(设置里那一项依然说了算)。
|
|
1409
|
-
function reschedule(d) {
|
|
1410
|
-
var m = (d && d.members) || {}
|
|
1411
|
-
var busy = Object.keys(m).some(function (k) { var v = m[k]; return v && typeof v === 'object' && (v.activity === 'running' || v.shortStatus === 'running') })
|
|
1412
|
-
clearInterval(pollRef.current)
|
|
1413
|
-
// 1.3.4:间隔来自设置 display.pollMs(忙碌时按同一意图加速到 40%,下限 300ms)
|
|
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)
|
|
1417
1391
|
}
|
|
1418
1392
|
useEffect(function () {
|
|
1419
1393
|
if (!isOpen && !viewMode) return undefined // panel closed & not canvas → no polling
|
|
1420
|
-
|
|
1394
|
+
var off = stateHubSubscribe(stateUrl(), dispCfg.pollMs, onState)
|
|
1421
1395
|
// catch up instantly when the user returns to the tab / window
|
|
1422
|
-
var onVis = function () { if (document.visibilityState === 'visible')
|
|
1423
|
-
var onFocus = function () {
|
|
1396
|
+
var onVis = function () { if (document.visibilityState === 'visible') stateHubFetch(stateUrl(), onState) }
|
|
1397
|
+
var onFocus = function () { stateHubFetch(stateUrl(), onState) }
|
|
1424
1398
|
document.addEventListener('visibilitychange', onVis)
|
|
1425
1399
|
window.addEventListener('focus', onFocus)
|
|
1426
|
-
return function () {
|
|
1400
|
+
return function () { off(); document.removeEventListener('visibilitychange', onVis); window.removeEventListener('focus', onFocus) }
|
|
1427
1401
|
}, [sessionId, selRunV, isOpen, viewMode, dispCfg.pollMs])
|
|
1428
1402
|
|
|
1429
1403
|
// Make the docked panel reserve space (shift the shell frame) instead of
|
|
@@ -2365,17 +2339,93 @@ window.__ModuleLoader__.load({
|
|
|
2365
2339
|
function Boundary(props) { var e = useState(null); if (e[0]) return h('div', { className: 'exp-preview' }, t('专家团面板渲染出错:', 'Team panel render error: ') + esc(String(e[0] && e[0].message || e[0]))); return h('div', {}, props.children) }
|
|
2366
2340
|
function wrap(Component) { return function (props) { return h(Boundary, null, h(Component, props || {})) } }
|
|
2367
2341
|
|
|
2342
|
+
// ── 统一的 /state 加载器(2026-09-15 性能修复 #2)────────────────────────────
|
|
2343
|
+
// 为什么必须统一:同一个重端点在客户端曾有三套轮询 —— 面板组件自己的 load()、
|
|
2344
|
+
// HeaderButton 徽章与画布共用的 liveStore.liveTick()(还有画布标签自己那一份)。
|
|
2345
|
+
// 画布一开就有 2–3 个 setInterval 并发压同一个 /state(实测 10 秒 7 发、6 次重叠,
|
|
2346
|
+
// 单发被拖到 5.9/8.3 s;服务端每请求还有 ~2 s 同步 CPU ⇒ 把整个 web 一起拖慢)。
|
|
2347
|
+
// 现在**所有** /state 拉取都经这里:
|
|
2348
|
+
// ① 同一 URL 同刻只跑一次(in-flight 合并,后来者复用在飞 promise 的结果);
|
|
2349
|
+
// ② 全局**一个**时钟(不再每个组件一个 setInterval);
|
|
2350
|
+
// ③ 间隔 = clamp(max(基础间隔, 上次耗时×2), 基础间隔, 30000);
|
|
2351
|
+
// 团队有人在跑时基础间隔按 40% 加速(面板原有意图,现在对徽章/画布同样生效)。
|
|
2352
|
+
// 纪律不变:**未回绝不发下一个**(晚一点看到状态,好过把 web 拖死)。
|
|
2353
|
+
var stateHub = { inflight: {}, lastMs: {}, subs: [], timer: 0, busy: false }
|
|
2354
|
+
function stateHubNow() { return (window.performance && performance.now) ? performance.now() : Date.now() }
|
|
2355
|
+
function stateHubUrls() { var out = []; stateHub.subs.forEach(function (x) { if (out.indexOf(x.url) < 0) out.push(x.url) }) ; return out }
|
|
2356
|
+
function stateHubBase() {
|
|
2357
|
+
var base = 0
|
|
2358
|
+
stateHub.subs.forEach(function (x) { if (!base || x.base < base) base = x.base })
|
|
2359
|
+
if (!base) base = 3000
|
|
2360
|
+
return stateHub.busy ? Math.max(300, Math.round(base * 0.4)) : base
|
|
2361
|
+
}
|
|
2362
|
+
function stateHubSchedule() {
|
|
2363
|
+
clearInterval(stateHub.timer)
|
|
2364
|
+
stateHub.timer = 0
|
|
2365
|
+
if (!stateHub.subs.length) return
|
|
2366
|
+
var slowest = 0
|
|
2367
|
+
stateHubUrls().forEach(function (u) { if ((stateHub.lastMs[u] || 0) > slowest) slowest = stateHub.lastMs[u] })
|
|
2368
|
+
var base = stateHubBase()
|
|
2369
|
+
var next = Math.max(base, Math.min(30000, Math.round(slowest * 2)))
|
|
2370
|
+
stateHub.timer = setInterval(stateHubTick, next)
|
|
2371
|
+
}
|
|
2372
|
+
function stateHubDeliver(url) {
|
|
2373
|
+
return function (d) {
|
|
2374
|
+
var list = stateHub.subs.slice()
|
|
2375
|
+
for (var i = 0; i < list.length; i++) { if (list[i].url === url) { try { list[i].onData(d) } catch (e) {} } }
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
function stateHubFetch(url, onData) {
|
|
2379
|
+
if (typeof fetch === 'undefined') return
|
|
2380
|
+
var p = stateHub.inflight[url]
|
|
2381
|
+
if (p) { p.then(function (d) { try { onData(d) } catch (e) {} }); return } // 同刻同 URL:复用在飞结果
|
|
2382
|
+
var t0 = stateHubNow()
|
|
2383
|
+
p = fetch(url).then(function (r) { return r.ok ? r.json() : null }).then(function (d) {
|
|
2384
|
+
delete stateHub.inflight[url]
|
|
2385
|
+
stateHub.lastMs[url] = Math.max(0, stateHubNow() - t0)
|
|
2386
|
+
return d
|
|
2387
|
+
}, function () { delete stateHub.inflight[url]; return null })
|
|
2388
|
+
stateHub.inflight[url] = p
|
|
2389
|
+
p.then(function (d) { try { onData(d) } catch (e) {} })
|
|
2390
|
+
}
|
|
2391
|
+
function stateHubTick() {
|
|
2392
|
+
if (!stateHub.subs.length) { clearInterval(stateHub.timer); stateHub.timer = 0; return }
|
|
2393
|
+
stateHubUrls().forEach(function (u) { stateHubFetch(u, stateHubDeliver(u)) })
|
|
2394
|
+
stateHubSchedule()
|
|
2395
|
+
}
|
|
2396
|
+
function stateHubSubscribe(url, base, onData) {
|
|
2397
|
+
var sub = { url: url, base: base || 3000, onData: onData }
|
|
2398
|
+
stateHub.subs.push(sub)
|
|
2399
|
+
stateHubFetch(url, onData) // 立即拉一次(面板/徽章打开即有数据)
|
|
2400
|
+
stateHubSchedule()
|
|
2401
|
+
return function () {
|
|
2402
|
+
var i = stateHub.subs.indexOf(sub)
|
|
2403
|
+
if (i >= 0) stateHub.subs.splice(i, 1)
|
|
2404
|
+
if (!stateHub.subs.length) { clearInterval(stateHub.timer); stateHub.timer = 0 }
|
|
2405
|
+
else stateHubSchedule()
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2368
2409
|
// ── 共享 state 轮询(HeaderButton / LiveCapsule 复用,单例;无订阅者即停)──
|
|
2369
|
-
var liveStore = { data: null, sid: '',
|
|
2410
|
+
var liveStore = { data: null, sid: '', off: null, subs: new Set() }
|
|
2411
|
+
var LIVE_BASE_MS = 2500 // 徽章/画布的基础节奏(与面板的 dispCfg.pollMs 取更小者由 hub 统一决定)
|
|
2412
|
+
function liveUrl(sid) { return '/plugins/dsh-expert-team/state?sessionId=' + encodeURIComponent(sid) }
|
|
2413
|
+
// 手动催一次(子代理刚出现时立刻刷新一次徽章,不必等下一个 tick)
|
|
2370
2414
|
function liveTick() {
|
|
2371
2415
|
var sid = liveStore.sid
|
|
2372
2416
|
if (!sid || typeof fetch === 'undefined') return
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
}
|
|
2417
|
+
stateHubFetch(liveUrl(sid), liveDeliver)
|
|
2418
|
+
}
|
|
2419
|
+
function liveDeliver(d) {
|
|
2420
|
+
if (!d) return
|
|
2421
|
+
liveStore.data = d
|
|
2422
|
+
try { harvestActivity(d) } catch (e) {}
|
|
2423
|
+
try { stateHub.busy = teamBusy(d) } catch (e) {}
|
|
2424
|
+
liveStore.subs.forEach(function (f) { try { f(d) } catch (e) {} })
|
|
2425
|
+
}
|
|
2426
|
+
function teamBusy(d) {
|
|
2427
|
+
var m = (d && d.members) || {}
|
|
2428
|
+
return Object.keys(m).some(function (k) { var v = m[k]; return v && typeof v === 'object' && (v.activity === 'running' || v.shortStatus === 'running') })
|
|
2379
2429
|
}
|
|
2380
2430
|
function useLiveState(sid) {
|
|
2381
2431
|
var st = useState(liveStore.data); var data = st[0], setData = st[1]
|
|
@@ -2384,10 +2434,11 @@ window.__ModuleLoader__.load({
|
|
|
2384
2434
|
liveStore.sid = sid
|
|
2385
2435
|
var fn = function (d) { setData(d) }
|
|
2386
2436
|
liveStore.subs.add(fn)
|
|
2387
|
-
|
|
2437
|
+
// 经 hub 订阅:与面板/画布共用同一个时钟与在飞守卫(这里**不再**自己 setInterval)
|
|
2438
|
+
var off = stateHubSubscribe(liveUrl(sid), LIVE_BASE_MS, liveDeliver)
|
|
2388
2439
|
return function () {
|
|
2389
2440
|
liveStore.subs.delete(fn)
|
|
2390
|
-
|
|
2441
|
+
off()
|
|
2391
2442
|
}
|
|
2392
2443
|
}, [sid])
|
|
2393
2444
|
return data
|
package/lib/command-parse.js
CHANGED
|
@@ -60,6 +60,7 @@ export const USAGE = [
|
|
|
60
60
|
' /team wait [<run>] 查看在飞任务进展(不阻塞;无进展超 5 分钟会提示)',
|
|
61
61
|
' /team clear [<run>] 清理一个或全部 run',
|
|
62
62
|
' /team uninstall 回收本插件铺到 $DSH_HOME 的副本(skill 现默认走运行时注册,本就不落地)',
|
|
63
|
+
' ⓘ 推荐插件(可选,缺了不报错):Hindsight = 跨项目记忆;dsh-cost-meter = 费用视图',
|
|
63
64
|
].join('\n');
|
|
64
65
|
|
|
65
66
|
// ── command parsing ────────────────────────────────────────────────────────
|
package/lib/command.js
CHANGED
|
@@ -906,6 +906,164 @@ async function uninstallInstalled() {
|
|
|
906
906
|
* 会把"响亮"变成噪声,而噪声的代价是所有告警一起被降权。
|
|
907
907
|
*/
|
|
908
908
|
const INSTALL_FAILURE_WARNED = new Set();
|
|
909
|
+
// ── 推荐插件自检(2026-09-15 · 用户要求)───────────────────────────────────────
|
|
910
|
+
// 事实(只读调查结论,别再重证):本插件**零硬依赖** —— package.json 连 `dependencies`
|
|
911
|
+
// 字段都没有,全库没有一条外部 import;下面这些插件**缺了都不会报错**,只是会让某些体验打折。
|
|
912
|
+
// 所以这里只做一件事:**装了就一声不吭,缺了说一句怎么办**(不吓人、不重复、不影响加载)。
|
|
913
|
+
//
|
|
914
|
+
// 三条探测路径(都有宿主源码依据):
|
|
915
|
+
// ① loader 条目(`ctx.get('loader').entries()` → `options.name`)= **装没装**(最通用);
|
|
916
|
+
// ② 服务探测(`ctx.get('costMeter')`)= 费用插件是否可用(服务名是**驼峰** costMeter);
|
|
917
|
+
// ③ 工具探测(`ctx.get('tools').get('hindsight_ingest_document')`)= **此刻能不能用**:
|
|
918
|
+
// Hindsight 在"装了但未配置/被 opt-out"时**根本不注册工具**(其 registerTools 开头
|
|
919
|
+
// 模板不可用即 return)⇒ 这一条天然把「没装」与「装了没配」分开 —— 两种零可区分。
|
|
920
|
+
// 全部 best-effort:任何异常都当"不知道",**绝不**因此影响插件加载。
|
|
921
|
+
const OPTIONAL_PLUGINS = [
|
|
922
|
+
{
|
|
923
|
+
id: 'hindsight',
|
|
924
|
+
pkg: '@vectorize-io/hindsight-coding-agents',
|
|
925
|
+
match: (name) => name === '@vectorize-io/hindsight-coding-agents' || name.indexOf('@vectorize-io/hindsight-coding-agents/') === 0,
|
|
926
|
+
what: '跨项目/跨会话长期记忆(clarify 前召回、deliver 时落库)',
|
|
927
|
+
install: 'dsh plugin --profile web add @vectorize-io/hindsight-coding-agents',
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
id: 'cost-meter',
|
|
931
|
+
pkg: 'dsh-cost-meter',
|
|
932
|
+
match: (name) => name === 'dsh-cost-meter' || name.indexOf('dsh-cost-meter/') === 0,
|
|
933
|
+
what: '会话费用 / token 账(浮层「在用模型」那一行指向它)',
|
|
934
|
+
install: 'dsh plugin --profile web add dsh-cost-meter',
|
|
935
|
+
},
|
|
936
|
+
];
|
|
937
|
+
/** 装了哪些插件(loader 条目名)。返回 null = 探测不可用(**不是**"没装")。 */
|
|
938
|
+
function loaderEntryNames(ctx) {
|
|
939
|
+
try {
|
|
940
|
+
const loader = ctx && typeof ctx.get === 'function' ? ctx.get('loader') : null;
|
|
941
|
+
if (!loader || typeof loader.entries !== 'function') return null;
|
|
942
|
+
const out = [];
|
|
943
|
+
for (const entry of loader.entries() || []) {
|
|
944
|
+
const n = entry && entry.options && entry.options.name;
|
|
945
|
+
if (n) out.push(String(n));
|
|
946
|
+
}
|
|
947
|
+
return out;
|
|
948
|
+
} catch { return null; }
|
|
949
|
+
}
|
|
950
|
+
/** 工具面探测:Hindsight 的工具在"未配置/被关"时压根不注册 ⇒ 天然区分没装与装了没配。 */
|
|
951
|
+
function hindsightToolReady(ctx) {
|
|
952
|
+
try {
|
|
953
|
+
const tools = ctx && typeof ctx.get === 'function' ? ctx.get('tools') : null;
|
|
954
|
+
if (!tools || typeof tools.get !== 'function') return null;
|
|
955
|
+
return !!tools.get('hindsight_ingest_document');
|
|
956
|
+
} catch { return null; }
|
|
957
|
+
}
|
|
958
|
+
function costMeterReady(ctx) {
|
|
959
|
+
try { return !!(ctx && typeof ctx.get === 'function' && ctx.get('costMeter')); } catch { return null; }
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* 自检推荐插件。**纯读、无副作用**。
|
|
963
|
+
* @returns {{items:Array, needsAttention:Array, hint:string}}
|
|
964
|
+
* status: 'ready'(装了且可用)/ 'installed-not-ready'(装了但当下不可用)/ 'missing'(没装)
|
|
965
|
+
* / 'unknown'(探测不可用 ⇒ 不下结论)
|
|
966
|
+
*/
|
|
967
|
+
function detectOptionalPlugins(ctx) {
|
|
968
|
+
const names = loaderEntryNames(ctx);
|
|
969
|
+
const items = OPTIONAL_PLUGINS.map((spec) => {
|
|
970
|
+
const installed = names ? names.some(spec.match) : null; // null = 探测不可用
|
|
971
|
+
const usable = spec.id === 'hindsight' ? hindsightToolReady(ctx) : (spec.id === 'cost-meter' ? costMeterReady(ctx) : null);
|
|
972
|
+
let status;
|
|
973
|
+
// 只有**确定**的结论才给确定的状态:
|
|
974
|
+
// · 工具/服务可用 ⇒ ready(最强的肯定证据);
|
|
975
|
+
// · loader 明确列出"没有" ⇒ missing(确定没装);
|
|
976
|
+
// · loader 列出有、但工具/服务不可用 ⇒ installed-not-ready(装了没配 —— 第二种零);
|
|
977
|
+
// · 其余(loader 探测不可用 **且** 服务也拿不到)⇒ **unknown**:这时"没装"与"装了没配"
|
|
978
|
+
// 分不开,就**不假装知道**。unknown 不进提示、不阻断任何功能。
|
|
979
|
+
if (usable === true) status = 'ready';
|
|
980
|
+
else if (installed === false) status = 'missing';
|
|
981
|
+
else if (installed === true && usable === false) status = 'installed-not-ready';
|
|
982
|
+
else status = 'unknown';
|
|
983
|
+
return { id: spec.id, pkg: spec.pkg, what: spec.what, install: spec.install, installed, usable, status };
|
|
984
|
+
});
|
|
985
|
+
const needsAttention = items.filter((i) => i.status === 'missing' || i.status === 'installed-not-ready');
|
|
986
|
+
let hint = '';
|
|
987
|
+
if (needsAttention.length) {
|
|
988
|
+
// 分档文案(2026-09-15 真实踩到):对 `installed-not-ready` **不能**再给安装命令 ——
|
|
989
|
+
// 那等于叫用户装一个已经装好的东西(用户机器上 dsh-cost-meter 明明紧接着自己加载成功了,
|
|
990
|
+
// 我们却提示他去 `dsh plugin add dsh-cost-meter` ⇒ 假警报 + 误导)。
|
|
991
|
+
// · missing(确认没装)⇒ 给安装命令;
|
|
992
|
+
// · installed-not-ready(装了但此刻拿不到)⇒ 只说事实与可能原因,**不给命令**;
|
|
993
|
+
// · unknown ⇒ 不进 needsAttention,不出现。
|
|
994
|
+
const parts = needsAttention.map((i) => {
|
|
995
|
+
const head = `${i.pkg}(${i.what})`;
|
|
996
|
+
return i.status === 'missing'
|
|
997
|
+
? `${head}:未安装 ⇒ ${i.install}`
|
|
998
|
+
: `${head}:已安装但当前未就绪(可能未配置或被关闭;若刚装上,稍后或重启后再看)`;
|
|
999
|
+
});
|
|
1000
|
+
hint = `[expert-team] 推荐插件未就绪(不影响使用,只是少了对应体验):${parts.join(';')}`;
|
|
1001
|
+
}
|
|
1002
|
+
return { items, needsAttention, hint };
|
|
1003
|
+
}
|
|
1004
|
+
let OPTIONAL_HINT_DONE = false;
|
|
1005
|
+
/**
|
|
1006
|
+
* 提示一次(**装了/都齐则完全不吭声**;进程内最多一行)。
|
|
1007
|
+
* 语义保持:只在"真的有需要关注的东西"时打印,且只打印一次。
|
|
1008
|
+
*/
|
|
1009
|
+
function hintOptionalPluginsOnce(ctx) {
|
|
1010
|
+
if (OPTIONAL_HINT_DONE) return '';
|
|
1011
|
+
try {
|
|
1012
|
+
const r = detectOptionalPlugins(ctx);
|
|
1013
|
+
if (!r.hint) return '';
|
|
1014
|
+
OPTIONAL_HINT_DONE = true;
|
|
1015
|
+
console.log(r.hint);
|
|
1016
|
+
return r.hint;
|
|
1017
|
+
} catch { return ''; }
|
|
1018
|
+
}
|
|
1019
|
+
/** 懒重探(`/team help` 等"真正用到"的时刻调用):结果更新,但同一次进程里仍最多打印一行。 */
|
|
1020
|
+
function recheckOptionalPlugins(ctx) {
|
|
1021
|
+
try { return detectOptionalPlugins(ctx); } catch { return { items: [], needsAttention: [], hint: '' }; }
|
|
1022
|
+
}
|
|
1023
|
+
// ── 探测时机(2026-09-15 第二轮修复)─────────────────────────────────────────────
|
|
1024
|
+
// **问题(用户贴的启动日志直接坐实)**:我们的 `apply()` 跑在其它插件之前,
|
|
1025
|
+
// 那一刻 `ctx.get('costMeter')` / 工具表里当然还没有它们 ⇒ 已装且可用的插件被误判成
|
|
1026
|
+
// `installed-not-ready`,日志里紧跟着 `dsh-cost-meter` 自己就加载成功了 ⇒ **假警报**。
|
|
1027
|
+
//
|
|
1028
|
+
// **宿主没有 app 级 ready 事件**(已核源码):`dsh-app-boot` 的 `boot()` 顺序是
|
|
1029
|
+
// `await mountRootInclude(...)` → `await ctx.get('loader')?.await()` → `assertEntriesActivated` → return,
|
|
1030
|
+
// 期间**不 emit** 任何"就绪"事件(只 emit `loader/config-update`)。
|
|
1031
|
+
// 而 `loader.await()` 我们也**不能**用:那棵树包含我们自己的挂载任务 ⇒ 在 init 里 await 它会自等死锁。
|
|
1032
|
+
// ⇒ 采用两条**不会有死锁**的机制:
|
|
1033
|
+
// ① 有界延迟重探(下面的 delays):每次重探都拿最新事实,直到"无待关注项"或重试用尽;
|
|
1034
|
+
// ② `ctx.inject([服务])` 事件驱动(服务/工具一旦出现立刻重探)—— 这是 cordis 的正确姿势,
|
|
1035
|
+
// 服务始终不出现时回调**只是不触发**,不会报错、也不阻塞。
|
|
1036
|
+
// 结果:**已装且可用的插件不再被误报**;刚装上的插件也不必为了这行提示再重启一次(懒重探兜底)。
|
|
1037
|
+
const OPTIONAL_PROBE_DELAYS_MS = [250, 1000, 3000];
|
|
1038
|
+
let OPTIONAL_PROBE_TIMERS = [];
|
|
1039
|
+
function scheduleOptionalPluginCheck(ctx, delays) {
|
|
1040
|
+
const plan = Array.isArray(delays) ? delays : OPTIONAL_PROBE_DELAYS_MS;
|
|
1041
|
+
const timers = [];
|
|
1042
|
+
const run = (isLast) => {
|
|
1043
|
+
if (OPTIONAL_HINT_DONE) return;
|
|
1044
|
+
let r;
|
|
1045
|
+
try { r = detectOptionalPlugins(ctx); } catch { return; }
|
|
1046
|
+
if (!r.needsAttention.length) { OPTIONAL_HINT_DONE = true; return; } // 都就绪 ⇒ 一声不吭(且不再打扰)
|
|
1047
|
+
if (isLast) { OPTIONAL_HINT_DONE = true; console.log(r.hint); }
|
|
1048
|
+
};
|
|
1049
|
+
plan.forEach((ms, i) => {
|
|
1050
|
+
const t = setTimeout(() => run(i === plan.length - 1), ms);
|
|
1051
|
+
if (t && typeof t.unref === 'function') t.unref();
|
|
1052
|
+
timers.push(t);
|
|
1053
|
+
});
|
|
1054
|
+
OPTIONAL_PROBE_TIMERS = timers;
|
|
1055
|
+
// 事件驱动兜底:服务一出现就重探(不出现则永不触发,零副作用)。
|
|
1056
|
+
for (const svc of ['costMeter', 'tools']) {
|
|
1057
|
+
try { if (ctx && typeof ctx.inject === 'function') ctx.inject([svc], () => run(true)); } catch { /* best-effort */ }
|
|
1058
|
+
}
|
|
1059
|
+
return timers;
|
|
1060
|
+
}
|
|
1061
|
+
function _resetOptionalHintOnce() {
|
|
1062
|
+
OPTIONAL_HINT_DONE = false;
|
|
1063
|
+
for (const t of OPTIONAL_PROBE_TIMERS) { try { clearTimeout(t); } catch { /* ignore */ } }
|
|
1064
|
+
OPTIONAL_PROBE_TIMERS = [];
|
|
1065
|
+
}
|
|
1066
|
+
|
|
909
1067
|
function warnInstallFailureOnce(kind) {
|
|
910
1068
|
if (INSTALL_FAILURE_WARNED.has(kind)) return;
|
|
911
1069
|
INSTALL_FAILURE_WARNED.add(kind);
|
|
@@ -2787,6 +2945,68 @@ function subHeaderIndex(ctx, subs, wfLabels) {
|
|
|
2787
2945
|
return out;
|
|
2788
2946
|
}
|
|
2789
2947
|
|
|
2948
|
+
/**
|
|
2949
|
+
* 并发合并 + **目录戳失效** + 长 TTL 的 `sessionQuery.listSessions()`。
|
|
2950
|
+
*
|
|
2951
|
+
* 为什么需要它(2026-09-15 性能诊断):宿主的 listSessions 在持久化层是
|
|
2952
|
+
* **枚举全部 artifact、逐个读 header**(本机 475 个、实测 ~1.5–2.4 s),而 /state 每 3 秒轮询。
|
|
2953
|
+
*
|
|
2954
|
+
* **2026-09-15 第二轮(profile 实测 subs 段 1.3–1.4 s 之后的纠正)**:
|
|
2955
|
+
* ① **2 s TTL 等于没有**:真实请求间隔就在 2 s 上下(轮询 + 退避),必然过期 ⇒ 每请求重算。
|
|
2956
|
+
* 现在 TTL 只作**兜底**(60 s),主失效键改成 `(sessions 根目录, mtimeMs, size)`
|
|
2957
|
+
* —— 新增/删除会话必然改动目录 mtime/size ⇒ **立刻失效**,不必靠 TTL 猜新鲜度。
|
|
2958
|
+
* ② **真正省时的是下面的 `SUB_HEADER_MEMO`**:按 session id 记住**不可变**的 header 字段,
|
|
2959
|
+
* 第二个请求起**根本不再枚举**。这才是把 subs 段打到近 0 的手段,而不是调 TTL 骗自己。
|
|
2960
|
+
*
|
|
2961
|
+
* **缓存纪律(本仓对"看到的就是真的"很敏感,所以写死在这里)**:
|
|
2962
|
+
* · 只服务"活注册表里**查不到**的子代理"这一支 ⇒ 都是**已结束/历史**会话,其 header 不再变化;
|
|
2963
|
+
* · **活代理**的状态一律走 listChildren/agents **实时读**,绝不经过这里的任何缓存;
|
|
2964
|
+
* · 目录戳取不到时退化为 60 s TTL 兜底(有界:新生效会话最多晚 60 s 可见,且只在 stat 失败时)。
|
|
2965
|
+
*/
|
|
2966
|
+
const LIST_SESSIONS_TTL_MS = 60000;
|
|
2967
|
+
let LIST_SESSIONS_CACHE = { at: 0, stamp: null, sessions: null, inflight: null };
|
|
2968
|
+
/** 会话目录的廉价失效戳(mtime + size)。取不到 ⇒ null ⇒ 只用 TTL 兜底。 */
|
|
2969
|
+
async function sessionsRootStamp() {
|
|
2970
|
+
try {
|
|
2971
|
+
const st = await stat(join(dshHome(), 'sessions'));
|
|
2972
|
+
return `${Math.round(Number(st.mtimeMs) || 0)}:${Number(st.size) || 0}`;
|
|
2973
|
+
} catch { return null; }
|
|
2974
|
+
}
|
|
2975
|
+
async function cachedListSessions(q, stamp) {
|
|
2976
|
+
const now = Date.now();
|
|
2977
|
+
const stampOk = (stamp === undefined || stamp === null) ? true : (LIST_SESSIONS_CACHE.stamp === stamp);
|
|
2978
|
+
if (LIST_SESSIONS_CACHE.sessions && (now - LIST_SESSIONS_CACHE.at) < LIST_SESSIONS_TTL_MS && stampOk) {
|
|
2979
|
+
return LIST_SESSIONS_CACHE.sessions;
|
|
2980
|
+
}
|
|
2981
|
+
if (LIST_SESSIONS_CACHE.inflight) return LIST_SESSIONS_CACHE.inflight; // 同刻只有一个枚举在跑
|
|
2982
|
+
SUB_HEADER_STATS.enumCalls += 1;
|
|
2983
|
+
const p = Promise.resolve()
|
|
2984
|
+
.then(() => q.listSessions())
|
|
2985
|
+
.then((sessions) => {
|
|
2986
|
+
LIST_SESSIONS_CACHE = { at: Date.now(), stamp: (stamp === undefined ? null : stamp), sessions: Array.isArray(sessions) ? sessions : [], inflight: null };
|
|
2987
|
+
return LIST_SESSIONS_CACHE.sessions;
|
|
2988
|
+
})
|
|
2989
|
+
.catch((e) => { LIST_SESSIONS_CACHE = { at: 0, stamp: null, sessions: null, inflight: null }; throw e; });
|
|
2990
|
+
LIST_SESSIONS_CACHE.inflight = p;
|
|
2991
|
+
return p;
|
|
2992
|
+
}
|
|
2993
|
+
/** 仅供测试:清空枚举缓存并复位(生产代码不需要调用它)。 */
|
|
2994
|
+
function _resetListSessionsCache() { LIST_SESSIONS_CACHE = { at: 0, stamp: null, sessions: null, inflight: null }; }
|
|
2995
|
+
|
|
2996
|
+
// ── 已结束子会话的 header 备忘(按 session id,**永久**)─────────────────────────
|
|
2997
|
+
// createdAt / parentId / depth 对**已结束**会话是不可变的;而用到备忘的这条分支,其 id 恰恰是
|
|
2998
|
+
// "活注册表里查不到"的那些 ⇒ 记住它们**不会**让用户看到旧状态(活代理走实时路径,永不写入这里)。
|
|
2999
|
+
// 旧实现每请求都要枚举 475 个 artifact 才能回答"这些人是何时建的、谁派工的"——
|
|
3000
|
+
// 有了备忘,第二个请求起直接查表。
|
|
3001
|
+
const SUB_HEADER_MEMO = new Map();
|
|
3002
|
+
const SUB_HEADER_STATS = { memoHits: 0, memoWrites: 0, enumCalls: 0 };
|
|
3003
|
+
function _resetSubHeaderMemo() {
|
|
3004
|
+
SUB_HEADER_MEMO.clear();
|
|
3005
|
+
SUB_HEADER_STATS.memoHits = 0;
|
|
3006
|
+
SUB_HEADER_STATS.memoWrites = 0;
|
|
3007
|
+
SUB_HEADER_STATS.enumCalls = 0;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
2790
3010
|
async function listSubagentStatusBySession(ctx, sid, knownIds) {
|
|
2791
3011
|
const root = sid ? await rootSessionId(ctx, sid) : '';
|
|
2792
3012
|
let rows = [];
|
|
@@ -2818,30 +3038,53 @@ async function listSubagentStatusBySession(ctx, sid, knownIds) {
|
|
|
2818
3038
|
}
|
|
2819
3039
|
// Fallback: completed subagents may be disposed from the live registry —
|
|
2820
3040
|
// the durable session projection (sessionQuery) still lists them.
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
3041
|
+
//
|
|
3042
|
+
// ⚠️ 2026-09-15 两轮性能修复:
|
|
3043
|
+
// 第一轮:只有**真的缺人**才花这笔钱(旧实现无条件 listSessions());
|
|
3044
|
+
// 第二轮:缺的那些**先查 `SUB_HEADER_MEMO`**(已结束会话的 header 不可变 ⇒ 永久备忘),
|
|
3045
|
+
// 只有仍然未知的才去枚举 ⇒ 第二个请求起通常**零枚举**。
|
|
3046
|
+
const missingIds = (knownIds || []).filter((id) => !rows.some((r) => r.id === id));
|
|
3047
|
+
if (missingIds.length) {
|
|
3048
|
+
const memoById = new Map(rows.map((r) => [r.id, r]));
|
|
3049
|
+
const stillUnknown = [];
|
|
3050
|
+
for (const id of missingIds) {
|
|
3051
|
+
const memo = SUB_HEADER_MEMO.get(id);
|
|
3052
|
+
if (!memo) { stillUnknown.push(id); continue; }
|
|
3053
|
+
SUB_HEADER_STATS.memoHits += 1;
|
|
3054
|
+
memoById.set(id, {
|
|
3055
|
+
id, mode: 'continuable', label: '', activity: 'inactive', model: '',
|
|
3056
|
+
createdAt: memo.createdAt, parentId: memo.parentId, depth: memo.depth,
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
rows = [...memoById.values()];
|
|
3060
|
+
if (stillUnknown.length) {
|
|
3061
|
+
try {
|
|
3062
|
+
const q = (ctx && typeof ctx.get === 'function') ? ctx.get('sessionQuery') : null;
|
|
3063
|
+
if (q && typeof q.listSessions === 'function') {
|
|
3064
|
+
const sessions = await cachedListSessions(q, await sessionsRootStamp());
|
|
3065
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
3066
|
+
for (const rec of sessions || []) {
|
|
3067
|
+
const h = rec?.header || rec;
|
|
3068
|
+
const aid = h?.id;
|
|
3069
|
+
if (!aid || byId.has(aid) || !knownIds.includes(aid)) continue;
|
|
3070
|
+
// ⚠️ header 里的 createdAt / parentSession / delegationDepth **必须带出来**。
|
|
3071
|
+
// 旧实现只取 id,把它们丢掉,导致面板上每个成员都是 createdAt=0 ⇒ 流转图
|
|
3072
|
+
// 判为「无创建时间记录,无法分批」,尽管会话日志第一行明明写着
|
|
3073
|
+
// {"type":"session","createdAt":…,"parentSession":…,"delegationDepth":1}。
|
|
3074
|
+
const entry = {
|
|
3075
|
+
createdAt: Number(h?.createdAt) || 0,
|
|
3076
|
+
parentId: String(h?.parentSession || ''),
|
|
3077
|
+
depth: Number(h?.delegationDepth) || 0,
|
|
3078
|
+
};
|
|
3079
|
+
// 只有"活注册表里查不到"的 id 会走到这里(活代理走实时路径)⇒ header 不可变 ⇒ 备忘安全。
|
|
3080
|
+
SUB_HEADER_MEMO.set(aid, entry);
|
|
3081
|
+
SUB_HEADER_STATS.memoWrites += 1;
|
|
3082
|
+
byId.set(aid, { id: aid, mode: 'continuable', label: '', activity: 'inactive', model: '', ...entry });
|
|
3083
|
+
}
|
|
3084
|
+
rows = [...byId.values()];
|
|
2841
3085
|
}
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
} catch { /* best-effort */ }
|
|
3086
|
+
} catch { /* best-effort */ }
|
|
3087
|
+
}
|
|
2845
3088
|
}
|
|
2846
3089
|
return Array.isArray(rows) ? rows : [];
|
|
2847
3090
|
}
|
|
@@ -2907,6 +3150,58 @@ function roleOfSub(sub, wfLabels) {
|
|
|
2907
3150
|
// A child's role never changes, and reading its log is the expensive path —
|
|
2908
3151
|
// cache once per child session id ('' entries are cached too, to avoid re-reads).
|
|
2909
3152
|
const SUB_ROLE_LOG_CACHE = new Map();
|
|
3153
|
+
// ── 每请求的"子会话日志读取"预算(2026-09-15)──────────────────────────────────
|
|
3154
|
+
// 诊断建议改成"真流式:读到首条 user/message 即停"。**做不到,且不假装做到**:
|
|
3155
|
+
// 宿主公开 API(`sessionQuery.readSession/listEvents/filterEvents/readEvent`)**没有**
|
|
3156
|
+
// 投影/限量/流式参数 —— `projectionMode` 只存在于内部 `corpus.read`,公开方法不暴露;
|
|
3157
|
+
// `filterEvents`/`readEvent` 也都是"先把整条日志 load 进内存再筛"。要"只读首帧"只能绕过
|
|
3158
|
+
// 公开接口去碰持久化层的内部方法(`readFirstZstdLine` 之类),那是**非契约、脆的**。
|
|
3159
|
+
// ⇒ 采用**有界**做法:每次 /state 最多读 N 条子会话日志(角色结果**永久缓存**),其余留到下一个 tick。
|
|
3160
|
+
// **这不是"优化成流式",而是"把一次 20 秒的尖峰摊平"**,并把"本轮被推迟"的条数如实交给
|
|
3161
|
+
// 调用方(rolesDeferred)⇒「还没解析」与「解析不出来」两件事分得清
|
|
3162
|
+
// (本仓纪律:两种零必须可区分),**不臆造角色**。
|
|
3163
|
+
//
|
|
3164
|
+
// ⚠️ 2026-09-15 第二轮(profile 实测 roles 段 0.6–0.9 s、且 `deferred` 从 16 涨到 40 之后):
|
|
3165
|
+
// 旧实现每请求都把候选集**从零重算**,预算永远花在队首那几条上,而新派的人不断出现在队尾
|
|
3166
|
+
// ⇒ **进度不前进、deferred 只增不减**("限次摊平"退化成"永远摊不完")。
|
|
3167
|
+
// 现在改成**跨请求保留进度**:每次按"当前 subs 里仍未缓存"的顺序**重建队列**(已缓存的自动掉队、
|
|
3168
|
+
// 既有顺序保留),再从队首消费 ≤N 条 ⇒ 进度单调推进,`deferred` 单调不增(除非真的又新派了人)。
|
|
3169
|
+
const ROLE_READ_BUDGET_PER_REQUEST = 4;
|
|
3170
|
+
let ROLE_READS_LEFT = ROLE_READ_BUDGET_PER_REQUEST;
|
|
3171
|
+
/** 待解析队列(跨请求保留进度):元素是子会话 id。 */
|
|
3172
|
+
let ROLE_PENDING = [];
|
|
3173
|
+
/** /state 每次进来复位**本请求的读取额度**(队列进度**不**复位)。 */
|
|
3174
|
+
function resetRoleReadBudget() { ROLE_READS_LEFT = ROLE_READ_BUDGET_PER_REQUEST; }
|
|
3175
|
+
/**
|
|
3176
|
+
* 按当前 subs 重建待解析队列:保留既有顺序里"仍在场且仍未缓存"的,再把新出现的未缓存 id 追加到队尾。
|
|
3177
|
+
* 幂等、无副作用;已缓存(含"解析不出来"缓存为 '')的条目自动掉队。
|
|
3178
|
+
*/
|
|
3179
|
+
function syncRolePending(subs) {
|
|
3180
|
+
const present = new Set();
|
|
3181
|
+
for (const s of subs || []) { const id = String((s && s.id) || ''); if (id) present.add(id); }
|
|
3182
|
+
const seen = new Set();
|
|
3183
|
+
const next = [];
|
|
3184
|
+
for (const id of ROLE_PENDING) {
|
|
3185
|
+
if (SUB_ROLE_LOG_CACHE.has(id) || !present.has(id) || seen.has(id)) continue;
|
|
3186
|
+
seen.add(id); next.push(id);
|
|
3187
|
+
}
|
|
3188
|
+
for (const s of subs || []) {
|
|
3189
|
+
const id = String((s && s.id) || '');
|
|
3190
|
+
if (!id || seen.has(id) || SUB_ROLE_LOG_CACHE.has(id)) continue;
|
|
3191
|
+
seen.add(id); next.push(id);
|
|
3192
|
+
}
|
|
3193
|
+
ROLE_PENDING = next;
|
|
3194
|
+
return ROLE_PENDING;
|
|
3195
|
+
}
|
|
3196
|
+
/** 仅供测试:清空待解析队列(生产代码不需要调用它)。 */
|
|
3197
|
+
function _resetRolePending() { ROLE_PENDING = []; }
|
|
3198
|
+
function roleReadBudgetSnapshot() {
|
|
3199
|
+
// deferred = **还没解析**(队列里等着读的);unresolved = **解析不出来**(读过但日志里没有角色)。
|
|
3200
|
+
// 两者必须是两个数:本仓纪律"两种零要分得清"。
|
|
3201
|
+
let unresolved = 0;
|
|
3202
|
+
for (const v of SUB_ROLE_LOG_CACHE.values()) if (!v) unresolved += 1;
|
|
3203
|
+
return { per: ROLE_READ_BUDGET_PER_REQUEST, left: ROLE_READS_LEFT, deferred: ROLE_PENDING.length, unresolved };
|
|
3204
|
+
}
|
|
2910
3205
|
/**
|
|
2911
3206
|
* Infer a child's role from its own session log. **Only the first `user/message`
|
|
2912
3207
|
* counts** — it is the delegation prompt the lead passed, e.g.
|
|
@@ -2919,6 +3214,10 @@ const SUB_ROLE_LOG_CACHE = new Map();
|
|
|
2919
3214
|
async function roleFromChildLog(ctx, id) {
|
|
2920
3215
|
if (!id) return '';
|
|
2921
3216
|
if (SUB_ROLE_LOG_CACHE.has(id)) return SUB_ROLE_LOG_CACHE.get(id);
|
|
3217
|
+
// 预算用尽 ⇒ 本轮不读,**且不写缓存**(下一个 tick 再试)。这是"本轮被推迟",
|
|
3218
|
+
// 不是"解析不出来" —— 调用方通过 rolesDeferred(队列长度)如实区分两者(两种零可区分)。
|
|
3219
|
+
if (ROLE_READS_LEFT <= 0) { return ''; }
|
|
3220
|
+
ROLE_READS_LEFT -= 1;
|
|
2922
3221
|
let role = '';
|
|
2923
3222
|
try {
|
|
2924
3223
|
const q = ctx && typeof ctx.get === 'function' ? ctx.get('sessionQuery') : null;
|
|
@@ -3069,16 +3368,33 @@ async function workflowRuns(ctx, sid, subs) {
|
|
|
3069
3368
|
* (2026-09-11 真实踩到:30 个子代理里只有 5 个有角色,正是 wfLabels 解析不出的那 5 个。)
|
|
3070
3369
|
*/
|
|
3071
3370
|
async function resolveSubRoles(ctx, subs, wfLabels) {
|
|
3371
|
+
const byId = new Map();
|
|
3372
|
+
for (const s of subs || []) { const id = String((s && s.id) || ''); if (id) byId.set(id, s); }
|
|
3373
|
+
// ① 便宜来源(自带 label / 父会话事件流 label)与已缓存结果 —— **不花预算、不读日志**
|
|
3072
3374
|
for (const s of subs || []) {
|
|
3073
3375
|
try {
|
|
3074
|
-
// ① 自带 label(显式角色/括号 id/关键词)与 ③ 父会话事件流的 workflow 派工 label
|
|
3075
3376
|
const known = roleOfSub(s, wfLabels);
|
|
3076
3377
|
if (known) { if (!s.role) s.role = known; continue; }
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
if (
|
|
3378
|
+
const id = String((s && s.id) || '');
|
|
3379
|
+
if (!id) continue;
|
|
3380
|
+
if (SUB_ROLE_LOG_CACHE.has(id)) {
|
|
3381
|
+
const cached = SUB_ROLE_LOG_CACHE.get(id);
|
|
3382
|
+
if (cached && !s.role) s.role = cached;
|
|
3383
|
+
}
|
|
3080
3384
|
} catch (e) { /* best-effort */ }
|
|
3081
3385
|
}
|
|
3386
|
+
// ② 跨请求推进:重建待解析队列(保留进度、已缓存的掉队、新人追加到队尾),再消费本请求额度。
|
|
3387
|
+
// 这样 `deferred`(队列长度)只减不增,除非真的又新派了人 —— 旧实现每请求从零重算,
|
|
3388
|
+
// 预算永远喂给队首同几条,新派的人永远轮不到(实测 deferred 16 → 40)。
|
|
3389
|
+
syncRolePending(subs);
|
|
3390
|
+
while (ROLE_READS_LEFT > 0 && ROLE_PENDING.length) {
|
|
3391
|
+
const id = ROLE_PENDING.shift();
|
|
3392
|
+
if (SUB_ROLE_LOG_CACHE.has(id)) continue;
|
|
3393
|
+
const r = await roleFromChildLog(ctx, id);
|
|
3394
|
+
if (!SUB_ROLE_LOG_CACHE.has(id)) { ROLE_PENDING.unshift(id); break; } // 预算耗尽(未写缓存)⇒ 放回队首,下一轮继续
|
|
3395
|
+
const s = byId.get(id);
|
|
3396
|
+
if (s && r && !s.role) s.role = r;
|
|
3397
|
+
}
|
|
3082
3398
|
return subs;
|
|
3083
3399
|
}
|
|
3084
3400
|
// sub rows → Map<role, sub> (first match wins; unknown labels map to '' and
|
|
@@ -4604,7 +4920,10 @@ async function executeTeamCommand(ctx, invocation) {
|
|
|
4604
4920
|
const memRun = (mem && mem.workspace === cwd) ? mem.runId : '';
|
|
4605
4921
|
const runArg = (s) => s || memRun;
|
|
4606
4922
|
switch (c.kind) {
|
|
4607
|
-
|
|
4923
|
+
// 懒重探兜底(2026-09-15):加载当刻的探测可能"太早"(那时别人的服务还没挂上)。
|
|
4924
|
+
// `/team help` 是"用户真的来看能做什么"的时刻 ⇒ 顺手重探一次并更新结论,
|
|
4925
|
+
// 这样刚装上/刚修好配置的插件**不必为了这一行提示再重启**。
|
|
4926
|
+
case 'help': recheckOptionalPlugins(ctx); return { kind: 'success', text: USAGE };
|
|
4608
4927
|
case 'status': return renderStatus(cwd);
|
|
4609
4928
|
case 'members': return renderMembers(cwd);
|
|
4610
4929
|
case 'models': return renderModels(cwd, runArg(c.run));
|
|
@@ -4649,7 +4968,7 @@ function warnLeadToolFaceOnce(status, detail) {
|
|
|
4649
4968
|
LEAD_TOOLFACE_WARNED.add(status);
|
|
4650
4969
|
console.warn(`[expert-team] lead 工具面**未**收窄(${status}):${detail}`);
|
|
4651
4970
|
}
|
|
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 };
|
|
4971
|
+
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, detectOptionalPlugins, OPTIONAL_PLUGINS, hintOptionalPluginsOnce, _resetOptionalHintOnce, scheduleOptionalPluginCheck, recheckOptionalPlugins, OPTIONAL_PROBE_DELAYS_MS, loaderEntryNames, hindsightToolReady, costMeterReady, listSubagentStatusBySession, cachedListSessions, _resetListSessionsCache, LIST_SESSIONS_TTL_MS, SUB_HEADER_MEMO, SUB_HEADER_STATS, _resetSubHeaderMemo, sessionsRootStamp, resetRoleReadBudget, roleReadBudgetSnapshot, syncRolePending, _resetRolePending, ROLE_READ_BUDGET_PER_REQUEST, ROLE_READ_LOG_CACHE: SUB_ROLE_LOG_CACHE };
|
|
4653
4972
|
|
|
4654
4973
|
export function apply(ctx, config) {
|
|
4655
4974
|
// 留一份 config:设置在运行时改变(官方面板 / 浮层)时要**用同一份 config** 重算上限与档位门,
|
|
@@ -4694,6 +5013,11 @@ export function apply(ctx, config) {
|
|
|
4694
5013
|
installInBackground('preset', () => ensurePresetInstalled());
|
|
4695
5014
|
// skill 已有运行时注册时这里立即返回(零成本);只有宿主没有 skill 注册表时才真复制。
|
|
4696
5015
|
installInBackground('skill', () => ensureSkillInstalled());
|
|
5016
|
+
// 推荐插件自检:纯读、缺了只打一行、装了完全不打扰(见 detectOptionalPlugins 的注释)。
|
|
5017
|
+
// ⚠️ **不能在加载当刻就下结论**:我们的 apply() 跑在其它插件之前,那一刻 costMeter/工具表
|
|
5018
|
+
// 里还没有它们 ⇒ 已装且可用的插件会被误判成"已安装但当前未就绪"(2026-09-15 真实假警报)。
|
|
5019
|
+
// 现在改为**就绪后重探**(有界延迟 + `ctx.inject` 事件驱动,见 scheduleOptionalPluginCheck)。
|
|
5020
|
+
scheduleOptionalPluginCheck(ctx);
|
|
4697
5021
|
void loadSessionRuns(); // session→run memory for overlay auto-select
|
|
4698
5022
|
ctx.commands.register({
|
|
4699
5023
|
name: 'team',
|
|
@@ -4916,6 +5240,13 @@ export function apply(ctx, config) {
|
|
|
4916
5240
|
const json = (code, body) => { res.writeHead(code, { 'content-type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(body)); };
|
|
4917
5241
|
try {
|
|
4918
5242
|
if (req.method !== 'GET') { res.writeHead(405); res.end(); return; }
|
|
5243
|
+
// 每请求复位"子会话日志读取"预算(见 roleFromChildLog 的注释)。
|
|
5244
|
+
resetRoleReadBudget();
|
|
5245
|
+
// 分步计时:只有显式打开才收集,平时零成本(不打印、不进响应)。
|
|
5246
|
+
// 用途:装上新版后**由人**核对"哪一步还贵",而不是靠猜——验收需要可复现数字。
|
|
5247
|
+
const STATE_PROFILE_ON = String((typeof process !== 'undefined' && process.env && process.env.DSH_EXPERT_TEAM_STATE_PROFILE) || '') === '1';
|
|
5248
|
+
const prof = STATE_PROFILE_ON ? { at: Date.now(), steps: {} } : null;
|
|
5249
|
+
const mark = (name) => { if (prof) { const n = Date.now(); prof.steps[name] = n - prof.at; prof.at = n; } };
|
|
4919
5250
|
const url = new URL(req.url ?? '/plugins/dsh-expert-team/state', 'http://127.0.0.1');
|
|
4920
5251
|
const sid = (url.searchParams.get('sessionId') ?? '').trim();
|
|
4921
5252
|
// cwd is OPTIONAL: if absent (e.g. overlay has no session id), fall
|
|
@@ -4956,6 +5287,7 @@ export function apply(ctx, config) {
|
|
|
4956
5287
|
if (nr) sel = await snapshotRun(nr.workspace, nr.runId);
|
|
4957
5288
|
}
|
|
4958
5289
|
}
|
|
5290
|
+
mark('runs+select');
|
|
4959
5291
|
if (!runs.length) { json(200, { ok: false, runs, workspaces, runsAvailable: 0, error: '还没有专家团 run。先运行 /team <task> 开一个。' }); return; }
|
|
4960
5292
|
if (!sel) {
|
|
4961
5293
|
// current workspace has no team run (but other workspaces do)
|
|
@@ -4997,8 +5329,11 @@ export function apply(ctx, config) {
|
|
|
4997
5329
|
// ① wfLabels — the workflow delegation labels in THIS session's own
|
|
4998
5330
|
// log (covers interrupted fan-outs, which have no chat card); then
|
|
4999
5331
|
// ② the child's own role prompt (roleFromChildLog).
|
|
5332
|
+
mark('subs');
|
|
5000
5333
|
const wfLabels = await workflowChildLabels(ctx, peopleSid);
|
|
5334
|
+
mark('wfLabels');
|
|
5001
5335
|
await resolveSubRoles(ctx, subs, wfLabels);
|
|
5336
|
+
mark('roles');
|
|
5002
5337
|
// R12 + R13 + R17(F-1 · P0 跨 run 成员串号):**显示与写盘都只吃过滤后的 `subById`**。
|
|
5003
5338
|
// R13 裁决:只严写盘的话,数据污染修好了但用户看到的串号现象一点没变 —— 面板照样把
|
|
5004
5339
|
// 别的 run 的 8 个活人显示成本 run 成员,而 membersUnresolved 当前 client.js 并不渲染,
|
|
@@ -5164,7 +5499,16 @@ export function apply(ctx, config) {
|
|
|
5164
5499
|
// 以及上面的「派工即登记」写本 run 的 `STATE.json`。它们同样值得搬到显式路径,
|
|
5165
5500
|
// 但那属于「第二个写者」的设计问题(见 `docs/专家团-开发计划.md` 的第 17 项)。
|
|
5166
5501
|
persistSessionRuns();
|
|
5167
|
-
|
|
5502
|
+
mark('tail');
|
|
5503
|
+
// profile 只在 DSH_EXPERT_TEAM_STATE_PROFILE=1 时出现(默认不含该字段,
|
|
5504
|
+
// 线上响应体不变);rolesDeferred = 本轮因预算被推迟的子会话日志读取条数,
|
|
5505
|
+
// 让调用方能把"还没解析"与"解析不出来"分开(两种零可区分)。
|
|
5506
|
+
const budget = roleReadBudgetSnapshot();
|
|
5507
|
+
json(200, Object.assign(
|
|
5508
|
+
{ ok: true, runs, workspaces, cwd, agents: sel.agents, rolesDeferred: budget.deferred },
|
|
5509
|
+
sel,
|
|
5510
|
+
prof ? { profile: Object.assign({}, prof.steps, { rolesBudget: budget }) } : null,
|
|
5511
|
+
));
|
|
5168
5512
|
} catch (e) {
|
|
5169
5513
|
json(500, { ok: false, error: String(e && e.message ? e.message : e) });
|
|
5170
5514
|
}
|
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.11",
|
|
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",
|