dsh-agent-board 1.9.0 → 1.9.2
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/README.md +13 -8
- package/index.mjs +1 -0
- package/lib/client/board-list.js +1 -1
- package/lib/client/dashboard.js +52 -7
- package/lib/client/task-detail.js +2 -2
- package/lib/client.js +55 -10
- package/lib/core.mjs +48 -12
- package/lib/dispatch.mjs +135 -1142
- package/lib/hung.mjs +202 -0
- package/lib/model-health.mjs +553 -0
- package/lib/notify.mjs +37 -4
- package/lib/policy.mjs +19 -9
- package/lib/rpc.mjs +7 -3
- package/lib/settle.mjs +954 -0
- package/package.json +1 -1
- package/test/core.test.mjs +339 -77
- package/test/decompose.test.mjs +70 -16
- package/test/fault-e2e-circuit.test.mjs +187 -0
- package/test/fault-e2e-fallback-chain.test.mjs +249 -0
- package/test/fault-e2e-hung.test.mjs +504 -0
- package/test/fault-e2e-recovery.test.mjs +255 -0
- package/test/fault-e2e-softgate.test.mjs +206 -0
- package/test/fault-e2e-stale-dir.test.mjs +226 -0
- package/test/fault-injection-docs.test.mjs +132 -0
- package/test/fault-provider.test.mjs +458 -0
- package/test/helpers/fault-provider.mjs +475 -0
- package/test/helpers/mock-ctx.mjs +9 -0
- package/test/hung-frame-signal.test.mjs +630 -0
- package/test/integration.test.mjs +41 -0
- package/test/lane-gui-bysize.test.mjs +410 -0
- package/test/lane-models.test.mjs +134 -0
- package/test/popover-clamp.test.mjs +92 -0
- package/test/runtime-health.test.mjs +2 -1
- package/test/scorecard-data.test.mjs +2 -1
- package/test/settle-stop-interrupt.test.mjs +294 -3
- package/test/ui-zoom-client.test.mjs +9 -4
package/lib/notify.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// makeMsg(纯函数,模块级 export,rpc.mjs 直接 import):插件来源消息构造。
|
|
3
3
|
// createNotify(ctx, state, deps):歧义上报 25s 去抖通知 / 回执批量聚合(45s 或满 5 条)+ 主窗口空闲门控 /
|
|
4
4
|
// 系统异常通知队列 / 投递前按看板现状过滤过期项(含 dispatched 派发回执的离场过滤)。共享状态全部经 state 显式注入。
|
|
5
|
-
//
|
|
5
|
+
// 回执三类入口共用同一聚合队列:notifyTaskDone(完成/阻塞)+ notifyDispatched(派发即回执)+ notifyDecomposed(拆解官回执)。
|
|
6
6
|
// 回执开关(板级 notifyDispatch/notifyDone,设置区「通知」小节)不在本模块判定——闸门在调用方
|
|
7
7
|
// (dispatch.mjs 派发/结算处读 cfg 快照),这样歧义裁决通知(notifyMainWindow)天然不受开关影响。
|
|
8
8
|
|
|
@@ -99,6 +99,29 @@ export function createNotify(ctx, state, deps) {
|
|
|
99
99
|
var stamp = 'dispatched|' + String(role || 'worker') + '|' + (t.claimedAt || t.verifierRunAt || '')
|
|
100
100
|
pushReceipt(sid, t.id + ':' + stamp, { kind: 'dispatched', role: String(role || 'worker'), title: t.title, id: t.id, summary: '', note: '' })
|
|
101
101
|
}
|
|
102
|
+
// 拆解官回执(task-mvrqiu6x):拆解官落定(split 物化 / keep 判定)→ 完成型回执,走同一聚合队列,
|
|
103
|
+
// flush 出「🧩 已拆解」分区。源卡按设计停在 draft/pending(提案由用户确认后逐张 publish),
|
|
104
|
+
// 回执 payload 带源卡 id 供投递前过滤——只有源卡已 archived(人已手动归档=已知悉)才丢弃,
|
|
105
|
+
// draft/pending 等其余状态一律保留(这正是原设计绕行系统通道的原因,豁免断言钉死)。
|
|
106
|
+
// 幂等键 = 源卡id|verdict|子卡id 集合连接:同一批提案重复上报(拆解官重跑/结算重放)指纹一致 → 吞掉;
|
|
107
|
+
// 换一批提案(子卡 id 变)/ verdict 变 → 指纹不同 → 正常新回执。
|
|
108
|
+
function notifyDecomposed(sid, payload) {
|
|
109
|
+
if (!payload || !payload.sourceId) return
|
|
110
|
+
var kids = Array.isArray(payload.children) ? payload.children : []
|
|
111
|
+
var kidIds = []
|
|
112
|
+
for (var i = 0; i < kids.length; i++) kidIds.push(String((kids[i] && kids[i].id) == null ? '' : kids[i].id))
|
|
113
|
+
var summary
|
|
114
|
+
if (payload.verdict === 'split') {
|
|
115
|
+
var titles = []
|
|
116
|
+
for (var j = 0; j < kids.length; j++) titles.push(String((kids[j] && kids[j].title) == null ? '' : kids[j].title).trim().slice(0, 60))
|
|
117
|
+
var list = titles.join('、')
|
|
118
|
+
if (list.length > 300) list = list.slice(0, 300) + '…'
|
|
119
|
+
summary = 'split:' + kids.length + ' 张子卡(' + list + ')'
|
|
120
|
+
} else summary = 'keep:无需拆分'
|
|
121
|
+
var note = payload.reason ? String(payload.reason).slice(0, 150) : ''
|
|
122
|
+
var stamp = String(payload.sourceId) + '|decomposed|' + String(payload.verdict || '') + '|' + kidIds.join(',')
|
|
123
|
+
pushReceipt(sid, stamp, { kind: 'decomposed', id: String(payload.sourceId), title: payload.sourceTitle || String(payload.sourceId), summary: summary, note: note })
|
|
124
|
+
}
|
|
102
125
|
// 系统级异常通知队列(易失,随回执冲刷):模型熔断/spawn 失败/孤儿回收/看门狗标记
|
|
103
126
|
// taskId 可选:告警类通知(软超时提醒等)语义只对「任务仍在执行中」成立,
|
|
104
127
|
// 带上任务 id 后 flush 投递前可重读看板校验,任务已落定的过期告警直接丢弃
|
|
@@ -118,13 +141,18 @@ export function createNotify(ctx, state, deps) {
|
|
|
118
141
|
function noteOf(status) { return status === 'verifying' ? '验证中' : '进行中' }
|
|
119
142
|
// 组装投递文本(入参已是过滤后的存活项,避免用已丢弃项的计数)
|
|
120
143
|
function composeText(items, notes) {
|
|
121
|
-
var done = [], blocked = [], dispatched = []
|
|
122
|
-
for (var i = 0; i < items.length; i++) { (items[i].kind === 'resolved' ? done : (items[i].kind === 'dispatched' ? dispatched : blocked)).push(items[i]) }
|
|
144
|
+
var done = [], blocked = [], dispatched = [], decomposed = []
|
|
145
|
+
for (var i = 0; i < items.length; i++) { (items[i].kind === 'resolved' ? done : (items[i].kind === 'decomposed' ? decomposed : (items[i].kind === 'dispatched' ? dispatched : blocked))).push(items[i]) }
|
|
123
146
|
var lines = [items.length ? '📋 [任务看板] 回执摘要(' + items.length + ' 条)' : '📋 [任务看板] 系统通知', '']
|
|
124
147
|
if (dispatched.length) {
|
|
125
148
|
lines.push('🚀 已派发 ' + dispatched.length + ' 个:')
|
|
126
149
|
for (var d = 0; d < dispatched.length && d < 8; d++) lines.push(' · ' + dispatched[d].title + ' (' + dispatched[d].id + ') — ' + (dispatched[d].role === 'verifier' ? 'Verifier 验收中' : 'Worker 执行中'))
|
|
127
150
|
}
|
|
151
|
+
if (decomposed.length) {
|
|
152
|
+
// 拆解官正式回执分区(task-mvrqiu6x):源卡标题 (源卡id) — split:M 张子卡(标题列表)/ keep:无需拆分
|
|
153
|
+
lines.push('🧩 已拆解 ' + decomposed.length + ' 个(待确认发布):')
|
|
154
|
+
for (var m = 0; m < decomposed.length && m < 8; m++) lines.push(' · ' + decomposed[m].title + ' (' + decomposed[m].id + ')' + (decomposed[m].summary ? ' — ' + decomposed[m].summary.slice(0, 120) : ''))
|
|
155
|
+
}
|
|
128
156
|
if (done.length) {
|
|
129
157
|
lines.push('✅ 完成 ' + done.length + ' 个:')
|
|
130
158
|
for (var j = 0; j < done.length && j < 8; j++) lines.push(' · ' + done[j].title + ' (' + done[j].id + ')' + (done[j].summary ? ' — ' + done[j].summary.slice(0, 120) : ''))
|
|
@@ -169,6 +197,11 @@ export function createNotify(ctx, state, deps) {
|
|
|
169
197
|
var it = items[j]
|
|
170
198
|
var tt = findTask(it.id)
|
|
171
199
|
if (tt && tt.status === 'archived') continue
|
|
200
|
+
// 拆解回执豁免(task-mvrqiu6x):kind='decomposed' 的源卡按设计停在 draft/pending(拆解提案
|
|
201
|
+
// 由用户确认后发布)——不得因「任务非 in-progress」被丢(这正是原设计绕行系统通道的原因,
|
|
202
|
+
// 此豁免由断言钉死,防后人把「非 in-progress 丢弃」改成通用规则误伤);唯一离场规则是
|
|
203
|
+
// 源卡已 archived(上面那条,人已手动归档=已知悉)。rt 读盘失败(tt 为 null)时同样保留。
|
|
204
|
+
if (it.kind === 'decomposed') { keptItems.push(it); continue }
|
|
172
205
|
if (it.kind === 'dispatched' && tt && tt.status !== 'in-progress') continue
|
|
173
206
|
keptItems.push(it)
|
|
174
207
|
}
|
|
@@ -217,5 +250,5 @@ export function createNotify(ctx, state, deps) {
|
|
|
217
250
|
} catch (e) { console.error('[task-board] late approve notify failed:', String(e)) }
|
|
218
251
|
}
|
|
219
252
|
|
|
220
|
-
return { maybeNotify: maybeNotify, notifyTaskDone: notifyTaskDone, notifyDispatched: notifyDispatched, pushSysNote: pushSysNote, notifyBoardCorrupt: notifyBoardCorrupt, notifyLateReject: notifyLateReject, notifyLateApprove: notifyLateApprove }
|
|
253
|
+
return { maybeNotify: maybeNotify, notifyTaskDone: notifyTaskDone, notifyDispatched: notifyDispatched, notifyDecomposed: notifyDecomposed, pushSysNote: pushSysNote, notifyBoardCorrupt: notifyBoardCorrupt, notifyLateReject: notifyLateReject, notifyLateApprove: notifyLateApprove }
|
|
221
254
|
}
|
package/lib/policy.mjs
CHANGED
|
@@ -102,7 +102,7 @@ export var DECOMPOSE_SCHEMA = {
|
|
|
102
102
|
modelHint: { type: 'string' },
|
|
103
103
|
touches: { type: 'array', items: { type: 'string' } },
|
|
104
104
|
acceptance: { type: 'string' },
|
|
105
|
-
|
|
105
|
+
dependsOn: { type: 'array', items: { type: 'string' } },
|
|
106
106
|
},
|
|
107
107
|
},
|
|
108
108
|
},
|
|
@@ -115,7 +115,7 @@ export var DECOMPOSE_SCHEMA = {
|
|
|
115
115
|
// ③ size 判档语义:light=单点小改/文档/配置,standard=常规模块改动(缺省),heavy=跨模块/高风险/需强推理;
|
|
116
116
|
// modelHint 仅在有特殊理由时填(如超长上下文);
|
|
117
117
|
// ④ children 上限 DECOMPOSE_MAX_CHILDREN 张;verdict=keep 表示源卡不用拆;
|
|
118
|
-
// ⑤
|
|
118
|
+
// ⑤ dependsOn 只许引用本批**前排**子卡的序号('1' 起,string)——物化侧翻译成本批真实 id,按构造天然无环;无法解析的序号丢弃并在该子卡 messages 留痕。
|
|
119
119
|
export function buildDecomposerPrompt(t, pack) {
|
|
120
120
|
var p = '你是任务看板的拆解官(一次性 run)。你的唯一交付物是**结构化拆解提案**(由 outputSchema 强箍:verdict/children/rationale),提案落定后本会话即销毁。\n\n'
|
|
121
121
|
p += '源卡:' + t.id + ' · ' + String(t.title || '') + '\n'
|
|
@@ -129,7 +129,7 @@ export function buildDecomposerPrompt(t, pack) {
|
|
|
129
129
|
p += '1. 第一步必须先用 read 工具读上面文件清单里的文件(按所给行号范围),从**实现侧**拆(按模块/文件边界切分);禁止不读代码就拍脑袋拆,禁止只做需求侧转述。\n'
|
|
130
130
|
p += '2. 每张子卡必须 10~30 分钟可独立完成并验收:touches 精确到文件(文件级排他锁,同批子卡 touches 不得互相重叠),acceptance 必须是可执行命令(如 "node --test test/xxx.test.mjs")。\n'
|
|
131
131
|
p += '3. size 判档:light=单点小改/文档/配置;standard=常规模块改动(缺省);heavy=跨模块/高风险/需强推理。modelHint 仅在有特殊理由时填(如某子卡需超长上下文模型),否则留空。\n'
|
|
132
|
-
p += '4. children 上限 ' + DECOMPOSE_MAX_CHILDREN + '
|
|
132
|
+
p += '4. children 上限 ' + DECOMPOSE_MAX_CHILDREN + ' 张;有先后顺序时用 dependsOn 引用同批序号(只许引用本批**前排**子卡,序号 1 起计:如第 2 张依赖第 1 张填 ["1"])。\n'
|
|
133
133
|
p += '5. 源卡本身不大、不必拆时:verdict=keep,并在 rationale 写清理由。\n'
|
|
134
134
|
p += '\n权限边界:你没有任何建卡工具——不要尝试创建/修改看板任务。提案由宿主读结构化结果后自动物化为 draft 子卡(源卡状态不变,由主窗口确认后逐张 publish)。\n'
|
|
135
135
|
return p
|
|
@@ -146,7 +146,7 @@ function decomposeChildId(d, parentId, n) {
|
|
|
146
146
|
// · 校验先于清理——提案畸形/run 失败时**绝不清**旧批提案(失败不毁已有资产);
|
|
147
147
|
// · 幂等:提案有效才清旧批(proposalOf===源卡 id 且仍 draft 的提案子卡整批移除;已 publish/落定的绝不碰);
|
|
148
148
|
// · 子卡落位:status=draft、parentId=源卡、size(合法才落)、model=modelHint(空不落)、touches/acceptance/
|
|
149
|
-
// description 落位、
|
|
149
|
+
// description 落位、dependsOn 序号 → 本批真实 id(只收前排序号,天然无环;坏序号丢弃+该子卡 messages 留痕)、上限 DECOMPOSE_MAX_CHILDREN 截断;
|
|
150
150
|
// · 源卡自身状态不变(仍 draft/pending,由用户处置),只追加 messages 提案摘要行 + history 审计行。
|
|
151
151
|
// 返回 { ok, verdict, children?:[{id,title}], error? }——非空返回值驱动 mutateLocked 写盘。
|
|
152
152
|
export function applyDecomposeProposal(d, t, proposal, now) {
|
|
@@ -180,17 +180,26 @@ export function applyDecomposeProposal(d, t, proposal, now) {
|
|
|
180
180
|
ah(t, t.status, t.status, 'system', '拆解官判定无需拆分(rationale 落源卡消息)')
|
|
181
181
|
return { ok: true, verdict: 'keep', children: [] }
|
|
182
182
|
}
|
|
183
|
-
// 物化:先分配整批 id(
|
|
183
|
+
// 物化:先分配整批 id(dependsOn 序号 → 真实 id 翻译要求全员 id 已知),再逐张落 draft 子卡。
|
|
184
184
|
var ids = []
|
|
185
185
|
for (var k = 0; k < kids.length; k++) ids.push(decomposeChildId(d, t.id, k + 1))
|
|
186
186
|
var created = []
|
|
187
187
|
for (var k2 = 0; k2 < kids.length; k2++) {
|
|
188
188
|
var c = kids[k2]
|
|
189
|
+
// dependsOn 序号引用 → 真实任务 id(task-mvr44rm3):序号 '1' 起计,指本批第 n 张提案;先全批建卡
|
|
190
|
+
// 拿到真实 id 再映射。只认**前排**(序号 1..k2,当前位 k2+1)——按构造天然无环。无法解析的序号
|
|
191
|
+
//(非数字/越界/自引用/后排)一律丢弃,并在该子卡 messages 留一条「依赖序号无法解析」:
|
|
192
|
+
// 发布前审阅子卡详情即可见,主窗口可 publish 后用 task_update dependsOn 手工补挂。
|
|
189
193
|
var deps = []
|
|
190
|
-
var
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
var unres = []
|
|
195
|
+
var dr = Array.isArray(c.dependsOn) ? c.dependsOn : []
|
|
196
|
+
for (var dj = 0; dj < dr.length; dj++) {
|
|
197
|
+
var ref = String(dr[dj] == null ? '' : dr[dj]).trim()
|
|
198
|
+
if (!ref) continue
|
|
199
|
+
var n = /^\d+$/.test(ref) ? parseInt(ref, 10) : NaN
|
|
200
|
+
if (!(n >= 1 && n <= k2)) { if (unres.indexOf(ref) < 0) unres.push(ref); continue }
|
|
201
|
+
var rid = ids[n - 1]
|
|
202
|
+
if (deps.indexOf(rid) < 0) deps.push(rid)
|
|
194
203
|
}
|
|
195
204
|
var child = {
|
|
196
205
|
id: ids[k2],
|
|
@@ -225,6 +234,7 @@ export function applyDecomposeProposal(d, t, proposal, now) {
|
|
|
225
234
|
if (sz === 'light' || sz === 'standard' || sz === 'heavy') child.size = sz
|
|
226
235
|
var mh = typeof c.modelHint === 'string' ? c.modelHint.trim() : ''
|
|
227
236
|
if (mh) child.model = mh
|
|
237
|
+
if (unres.length) child.messages.push({ kind: 'decompose', text: '🧩 依赖序号无法解析(' + unres.slice(0, 10).map(function (u) { return String(u).slice(0, 20) }).join('、') + ')——已丢弃;只认同批**前排**子卡序号(1 起计),如需依赖可 publish 后用 task_update dependsOn 手工补挂', at: stamp, by: 'decomposer' })
|
|
228
238
|
d.tasks.push(child)
|
|
229
239
|
created.push(child)
|
|
230
240
|
}
|
package/lib/rpc.mjs
CHANGED
|
@@ -833,8 +833,9 @@ export function createRpc(ctx, state, deps) {
|
|
|
833
833
|
// (retryCount+1 / lastError=子会话被宿主拆除 / history 落「worker 失败…重新排队」,
|
|
834
834
|
// retryCount=2 的卡直接被打成 blocked,与「hold=停派」语义正面冲突)。
|
|
835
835
|
// 终止的账由本函数自己结(accumulateRunUsage + closeRunHistory + mutateLocked),立旗后
|
|
836
|
-
// 兜底臂/超时臂/idle 事件通道对这条 rec
|
|
837
|
-
//
|
|
836
|
+
// 兜底臂/超时臂/idle 事件通道对这条 rec 的迟到落定一律空转——与落闸弃权臂
|
|
837
|
+
// (lib/dispatch.mjs 的 spawn 落闸处置)同一纪律,且补上「离开活跃表的 rec 必然已立
|
|
838
|
+
// settled 旗」不变式的最后一个缺口。
|
|
838
839
|
rec.settled = true
|
|
839
840
|
// 清理:一次性 run 走 dispose(既有语义逐字不变);continuable 的 rec 没有 run(持久子会话),
|
|
840
841
|
// 卡2 起改走 **interrupt 留存**(task-muw5h2ps):只打断当前 turn、不销毁子会话——否则「手动终止」
|
|
@@ -1019,7 +1020,10 @@ export function createRpc(ctx, state, deps) {
|
|
|
1019
1020
|
// 双查第二查「撞他卡有效锁」要用它现算他卡持锁(core.planLockConflicts)。
|
|
1020
1021
|
function planGateApply(t, planFiles, actor, pat, d) {
|
|
1021
1022
|
// 有效边界 = touches ∪ extraTouches(task-mvonytz3:仲裁扩权登记的文件即本卡合法边界,
|
|
1022
|
-
// 不再误报越界;touches
|
|
1023
|
+
// 不再误报越界;touches 审计原值不动——边界展示带「含仲裁扩权」标注以资区别)。
|
|
1024
|
+
// 含 extraTouches(task-mvtz2jjq 勘误):此处自 task-mvonytz3 起就按有效锁集比对,此前
|
|
1025
|
+
// 「闸口不认 extraTouches」的诊断系解析伪影误判——真因在 core.parsePlanFiles 把「调用点/
|
|
1026
|
+
// 接线断言仍锁」这类中文散文片与 `/if \(...` 正则残片误判成路径,残片永远不在边界内。
|
|
1023
1027
|
var boundary = core.effectiveTouches(t)
|
|
1024
1028
|
var hasExtra = Array.isArray(t.extraTouches) && t.extraTouches.length > 0
|
|
1025
1029
|
// 卡无有效边界 → 无越界可比;但计划仍可能撞他卡锁(无边界卡的静默漂移同样现形)
|