dsh-agent-board 1.7.7 → 1.7.8
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 +2 -2
- package/index.mjs +1 -0
- package/lib/client/board-list.js +12 -8
- package/lib/client/dashboard.js +5 -4
- package/lib/client/kernel.js +32 -5
- package/lib/client/task-detail.js +2 -0
- package/lib/client.js +51 -17
- package/lib/core.mjs +21 -0
- package/lib/dispatch.mjs +17 -5
- package/lib/rpc.mjs +53 -9
- package/lib/store.mjs +86 -62
- package/package.json +1 -1
- package/test/core.test.mjs +142 -12
- package/test/scoreboard-trends-client.test.mjs +21 -0
- package/test/store-data-safety.test.mjs +105 -0
- package/test/verify-timing.test.mjs +26 -0
package/README.md
CHANGED
|
@@ -255,11 +255,11 @@ Team 托管档独有(调度员体验):
|
|
|
255
255
|
> 兼容:旧的 `set-board-mode` / `set-team-mode` 两个 RPC 原样保留(旧客户端与脚本不受影响),
|
|
256
256
|
> 内部仍以 `boardMode` + `teamMode` 两个字段落盘,老看板文件无损;`get-tasks` 额外返回派生字段 `workMode` 供 UI 单点读取。
|
|
257
257
|
|
|
258
|
-
##
|
|
258
|
+
## 14 个 Agent 工具
|
|
259
259
|
|
|
260
260
|
| 类别 | 工具 |
|
|
261
261
|
|---|---|
|
|
262
|
-
| 任务管理 | `task_create` / `task_list` / `task_context` / `task_update` / `task_claim` / `task_resolve` / `task_verify` / `task_archive` |
|
|
262
|
+
| 任务管理 | `task_create` / `task_list` / `task_context` / `task_update` / `task_claim` / `task_resolve` / `task_verify` / `task_archive` / `task_cancel` |
|
|
263
263
|
| 池治理 | `task_terminate` / `task_intervene` / `task_arbitrate` |
|
|
264
264
|
| 子代理上报 | `board_report` / `board_verdict` |
|
|
265
265
|
|
package/index.mjs
CHANGED
|
@@ -84,6 +84,7 @@ export function apply(ctx) {
|
|
|
84
84
|
maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone, notifyLateReject: notify.notifyLateReject,
|
|
85
85
|
pushSysNote: notify.pushSysNote, sessionCwd: session.sessionCwd, // 调研门禁③:epic 发布预检汇总投递 + 预研路径相对解析根
|
|
86
86
|
spawnOneShot: dispatch.spawnOneShot, accumulateRunUsage: dispatch.accumulateRunUsage, readContextPack: dispatch.readContextPack,
|
|
87
|
+
closeRunHistory: dispatch.closeRunHistory, // 手动终止 run 关账(doTerminate 状态无关清理,task-mv1pqy27)
|
|
87
88
|
// 上报通道结算入口(task-muwkhqf8):board_report/board_verdict 把任务推进到落定态时,
|
|
88
89
|
// 由 rpc 侧顺手对「该任务当前的 continuable Worker rec」补做收尾三件套
|
|
89
90
|
// (关 run 结局 / usage 落账 / 摘超时臂)——治「工具上报完成后 idle 事件没到 → run 永远 running」。
|
package/lib/client/board-list.js
CHANGED
|
@@ -192,18 +192,21 @@
|
|
|
192
192
|
function ArchiveView() {
|
|
193
193
|
var _R = React; var useState = _R.useState
|
|
194
194
|
var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
|
|
195
|
-
//
|
|
195
|
+
// 归档 tab 数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
|
|
196
196
|
// 不再依赖独立 state.archived/fetchArchived——旧路径异步填库却未接入 TopPanel 的 notify 更新项,
|
|
197
197
|
// 归档 tab 打开后停在「暂无归档任务」空态,而仪表盘「已归档」计数来自 state.tasks(全量),二者对不上。
|
|
198
|
-
// 全量 +
|
|
199
|
-
|
|
198
|
+
// 全量 + 按归档/取消时间倒序(archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt)+ 顶部注明条数。
|
|
199
|
+
// 含 cancelled(反馈 n-mv0zbqk... 取消通道):取消卡在待办列消失(cancelled 不在 COLUMNS)后,
|
|
200
|
+
// 归入本 tab 可见、可「恢复待办」——否则作废卡从 UI 上彻底蒸发、无法找回。
|
|
201
|
+
var archived = state.tasks.filter(function (t) { return t.status === 'archived' || t.status === 'cancelled' })
|
|
202
|
+
var cancelledCount = archived.filter(function (t) { return t.status === 'cancelled' }).length // 文案口径修正:仪表盘「已归档」只数 archived,本 tab 含 cancelled,须单列
|
|
200
203
|
var list = archived.filter(function (t) {
|
|
201
204
|
if (!q) return true
|
|
202
205
|
var qq = q.toLowerCase()
|
|
203
206
|
return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
|
|
204
207
|
}).sort(function (a, b) {
|
|
205
|
-
//
|
|
206
|
-
function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
|
|
208
|
+
// 归档/取消时间倒序:archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt(老卡可能缺 archivedAt)
|
|
209
|
+
function archTs(t) { return t.archivedAt || t.cancelledAt || t.resolvedAt || t.createdAt || '' }
|
|
207
210
|
return (archTs(b) || '').localeCompare(archTs(a) || '')
|
|
208
211
|
})
|
|
209
212
|
// 工作模式(与 fetchTasks 同口径):list=清单模式(恢复不重派)不弹;auto/team 恢复即重派 → confirm 门禁
|
|
@@ -216,8 +219,8 @@
|
|
|
216
219
|
}
|
|
217
220
|
return React.createElement('div', null,
|
|
218
221
|
React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.border2, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
|
|
219
|
-
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + '
|
|
220
|
-
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '
|
|
222
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 }, title: '归档 tab 含已归档 + 已取消两类;仪表盘「已归档」只数已归档,不数已取消' }, '共 ' + archived.length + ' 条' + (cancelledCount > 0 ? '(含已取消 ' + cancelledCount + ' 条)' : '')),
|
|
223
|
+
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档/已取消任务') :
|
|
221
224
|
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
222
225
|
list.map(function (t) {
|
|
223
226
|
// 归档行点击进详情(用户 2026-10-09 指令):复用 state.detailId 机制,与看板卡同路径;
|
|
@@ -227,7 +230,8 @@
|
|
|
227
230
|
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: t.title }, t.title),
|
|
228
231
|
reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0 } }, '👁 建议过目') : null,
|
|
229
232
|
t.verification ? React.createElement('span', { title: '验收结论: ' + (t.verification.summary || '').slice(0, 100), style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.verification.verdict === 'approved' ? C.ok : C.err, color: C_INV } }, t.verification.verdict === 'approved' ? '过' : '驳') : null,
|
|
230
|
-
React.createElement('span', { style: { fontSize: 9,
|
|
233
|
+
t.status === 'cancelled' ? React.createElement('span', { title: '已取消(可恢复待办)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 20%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0 } }, '已取消') : null,
|
|
234
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, (t.archivedAt || t.cancelledAt) ? ago(t.archivedAt || t.cancelledAt) : '')),
|
|
231
235
|
t.verification && t.verification.summary ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + t.verification.summary) : null,
|
|
232
236
|
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
|
|
233
237
|
React.createElement('button', { onClick: function (e) { e.stopPropagation(); restore(t.id) }, title: wm === 'list' ? '恢复为待办' : '恢复为待办(将按当前工作模式立即重新派发,消耗一轮 Worker+Verifier token)', style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '↩ 恢复待办'),
|
package/lib/client/dashboard.js
CHANGED
|
@@ -582,13 +582,14 @@
|
|
|
582
582
|
// 近 14 天柱形通用画法(① 通过率 / ③ 超时率共用):cells = [{ k, rate(null=当日无样本), tip }]
|
|
583
583
|
// 今日柱 brand 高亮 + 率值顶标;无样本日画 3px 灰基线(title 写明无样本,不假装 0%)。
|
|
584
584
|
function qtDayBars(cells, color, todayKey) {
|
|
585
|
-
return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46 } },
|
|
585
|
+
return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46, minWidth: 0, overflow: 'hidden' } },
|
|
586
586
|
cells.map(function (c) {
|
|
587
587
|
var isToday = c.k === todayKey
|
|
588
588
|
var h = c.rate === null ? 3 : Math.max(4, Math.round(c.rate * 34))
|
|
589
|
+
h = Math.min(22, h) // clamp 柱高:100% 柱封顶 22px——46px 绘图容器扣掉上下率值/日期标签与 gap/描边后不越出,防单日 100% 黑柱向上盖住右列
|
|
589
590
|
var kp = c.k.split('-')
|
|
590
591
|
var dLabel = Number(kp[1]) + '/' + Number(kp[2])
|
|
591
|
-
return React.createElement('div', { key: c.k, style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
|
|
592
|
+
return React.createElement('div', { key: c.k, style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
|
|
592
593
|
c.rate !== null ? React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, mpPct(c.rate)) : null,
|
|
593
594
|
React.createElement('div', { style: { width: '100%', maxWidth: 22, height: h + 'px', borderRadius: 2, background: c.rate === null ? C.nested : (isToday ? C.brand : color), border: '1px solid ' + (isToday ? C.brand : (c.rate === null ? C.border : color)) } }),
|
|
594
595
|
React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, dLabel))
|
|
@@ -633,7 +634,7 @@
|
|
|
633
634
|
})
|
|
634
635
|
var fpHas = false; fpCells.forEach(function (c) { if (c.rate !== null) fpHas = true })
|
|
635
636
|
var fpToday = fpCells[fpCells.length - 1]
|
|
636
|
-
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
637
|
+
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
|
|
637
638
|
blockHead('一次通过率(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: fpToday.rate !== null ? C.brand : C.text2, fontWeight: 600 } }, fpToday.rate !== null ? mpPct(fpToday.rate) : '—')), '一次通过率 = 当天落定「已完成」任务里零驳回(rejectCount=0)的占比;驳回 = Verifier 验收判 reject'),
|
|
638
639
|
fpHas ? qtDayBars(fpCells, C.ok, todayKey)
|
|
639
640
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 resolved 任务落定'))
|
|
@@ -661,7 +662,7 @@
|
|
|
661
662
|
})
|
|
662
663
|
var toHas = false; toCells.forEach(function (c) { if (c.rate !== null) toHas = true })
|
|
663
664
|
var toToday = toCells[toCells.length - 1]
|
|
664
|
-
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
665
|
+
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
|
|
665
666
|
blockHead('超时率走势(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: toToday.rate !== null ? C.warn : C.text2, fontWeight: 600 } }, toToday.rate !== null ? mpPct(toToday.rate) : '—')), '超时率 = 超时落定(timeout/error)的 run 占当天 run 的比例'),
|
|
666
667
|
toHas ? qtDayBars(toCells, C.warn, todayKey)
|
|
667
668
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 run 落定'))
|
package/lib/client/kernel.js
CHANGED
|
@@ -194,6 +194,11 @@
|
|
|
194
194
|
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
195
195
|
['rect', { width: 6, height: 6, x: 9, y: 9 }]
|
|
196
196
|
],
|
|
197
|
+
// 取消入口图标(详情页「取消」按钮;语义=作废不派发,与删除 trash-2 区分)
|
|
198
|
+
'ban': [
|
|
199
|
+
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
200
|
+
['path', { d: 'm4.9 4.9 14.2 14.2' }]
|
|
201
|
+
],
|
|
197
202
|
'plus': [
|
|
198
203
|
['path', { d: 'M5 12h14' }],
|
|
199
204
|
['path', { d: 'M12 5v14' }]
|
|
@@ -351,10 +356,14 @@
|
|
|
351
356
|
state.poolStatus = (d && d.poolStatus) || null
|
|
352
357
|
// token 消耗聚合(board 级,host 端现算):范围守卫——响应回来时范围若已变,保留旧值不覆盖
|
|
353
358
|
// (新范围的那次请求会带着新聚合回来;老 host 不返回该字段 → null,零渲染)
|
|
354
|
-
// ===== usageSummary
|
|
355
|
-
//
|
|
356
|
-
// →
|
|
357
|
-
//
|
|
359
|
+
// ===== usageSummary 变化检测 + React 订阅(task-muwq9u04 → task-mv10lvud 根修)=====
|
|
360
|
+
// 病根(task-mv10lvud 实测「点今天也没变化」):范围切换时 setRange 先换 state.dateRange(新对象
|
|
361
|
+
// → setDR 身份变化 → 第一帧渲染成功),随后 fetchTasks 响应把 state.usageSummary 换成新对象并 notify(),
|
|
362
|
+
// 但 TopPanel 的 update() 里所有 setX 值都 Object.is 全等(tasks 引用不变、布尔不变)→ React 18 全量
|
|
363
|
+
// bailout,零重渲染 → Token 区永远拿不到新聚合。notify ≠ React 重渲染,须有「新身份的订阅值」做载体。
|
|
364
|
+
// 修法:TopPanel 补 usageSummary 专属 React state(setUsage(state.usageSummary)),host 每次响应都是
|
|
365
|
+
// 新对象,身份变化天然触发渲染(订阅见 TopPanel 的 update())。
|
|
366
|
+
// 为什么本处用「上一次的 JSON 串」比对而不是对象引用:host 每次 get-tasks 都现算聚合、**必然返回新对象**
|
|
358
367
|
// (引用比较恒真)→ 3s 轮询每轮都 notify,tasksHash 的渲染节约当场作废。JSON 串只在这份聚合
|
|
359
368
|
// 真变了时才不等(KB 级体积、3s 一次,代价可忽略);赋值前先快照,赋值后比对。
|
|
360
369
|
var usageJsonPrev = JSON.stringify(state.usageSummary || null)
|
|
@@ -498,6 +507,23 @@
|
|
|
498
507
|
}).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
|
|
499
508
|
}
|
|
500
509
|
|
|
510
|
+
// ===== 取消通道(与 host cancel-task / task_cancel 门禁一一对应)=====
|
|
511
|
+
// 可取消状态:草稿/待办/阻塞(未进入执行)。in-progress/verifying 需先终止;resolved 引导归档。
|
|
512
|
+
// 取消=显式放弃:置 cancelled + 留档可检索、可在归档 tab「恢复待办」;与删除不同,取消不丢数据。
|
|
513
|
+
function canCancel(t) { return !!t && (t.status === 'draft' || t.status === 'pending' || t.status === 'blocked') }
|
|
514
|
+
// confirm 预警(与 note「取消后不再派发,可在归档恢复」同口径);失败时 host 门禁原因原样回显。
|
|
515
|
+
function cancelTask(id, title, onMsg) {
|
|
516
|
+
var t = getTask(id)
|
|
517
|
+
var name = title || (t && t.title) || id
|
|
518
|
+
if (!window.confirm('取消后不再派发,可在归档恢复。确认取消「' + name + '」?')) return Promise.resolve({ ok: false, cancelled: true })
|
|
519
|
+
return rpc('cancel-task', { taskId: id }).then(function (r) {
|
|
520
|
+
if (r && r.ok === false) { if (onMsg) onMsg('⚠️ ' + (r.error || '取消失败')) }
|
|
521
|
+
else if (onMsg) onMsg('🚫 已取消「' + name + '」(可在归档 tab 恢复待办)')
|
|
522
|
+
fetchTasks()
|
|
523
|
+
return r
|
|
524
|
+
}).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
|
|
525
|
+
}
|
|
526
|
+
|
|
501
527
|
function onDragStart(e, task) { e.dataTransfer.setData('text/plain', JSON.stringify({ id: task.id, status: task.status })); e.dataTransfer.effectAllowed = 'move'; state.dragTask = task.id; notify() }
|
|
502
528
|
function onDragEnd() { state.dragTask = null; state.dragOver = null; notify() }
|
|
503
529
|
function onColDragOver(e, st) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (state.dragOver !== st) { state.dragOver = st; notify() } }
|
|
@@ -576,7 +602,8 @@
|
|
|
576
602
|
var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
|
|
577
603
|
var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
|
|
578
604
|
var _vug = useState(state.verifyUserGuide), vugOn = _vug[0], setVugOn = _vug[1] // 自测指南开关勾选态(设置区「验收」)
|
|
579
|
-
|
|
605
|
+
var _usg = useState(state.usageSummary), setUsage = _usg[1] // Token 区聚合订阅(task-mv10lvud 根修):usageSummary 换新对象即触发渲染
|
|
606
|
+
useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setTeamMode(state.teamMode); setWorkModeState(state.workMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin); setFbEnabled(state.feedbackEnabled); setNdOn(state.notifyDispatch); setNnOn(state.notifyDone); setEsOn(state.epicSplit); setVugOn(state.verifyUserGuide); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash); setTasksErr(state.tasksErr); setUsage(state.usageSummary) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
580
607
|
if (!open) return null
|
|
581
608
|
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
582
609
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -461,6 +461,8 @@
|
|
|
461
461
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'approved' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('check-circle', 10), '通过') : null,
|
|
462
462
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { var r = window.prompt('驳回原因:'); doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'rejected', comment: r || '' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.err, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('x-circle', 10), '驳回') : null,
|
|
463
463
|
task.status === 'resolved' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('archive-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.text2, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('archive', 10), '归档') : null),
|
|
464
|
+
// 取消(仅草稿/待办/阻塞):confirm 预警「取消后不再派发,可在归档恢复」;与删除并列但语义=作废留档(可恢复)
|
|
465
|
+
canCancel(task) ? React.createElement('button', { onClick: function () { cancelTask(task.id, task.title, setActionMsg) }, title: '取消任务(取消后不再派发,可在归档恢复待办)', style: { fontSize: 10, padding: '3px 8px', border: '1px solid ' + C.warn, borderRadius: 3, background: 'transparent', color: C.warn, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('ban', 10), '取消') : null,
|
|
464
466
|
// 删除(仅草稿/待办/阻塞):confirm 防误删;被 host 门禁拒绝时把原因回显到详情页 actionMsg
|
|
465
467
|
// 容器 div 的收尾括号已挂在「归档」行;删除按钮作为**额外一行元素**排在它后面
|
|
466
468
|
// (多一个实参给同一个 createElement,属于合法调用),因此本行自身必须配平。
|
package/lib/client.js
CHANGED
|
@@ -207,6 +207,11 @@ function apply(ctx) {
|
|
|
207
207
|
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
208
208
|
['rect', { width: 6, height: 6, x: 9, y: 9 }]
|
|
209
209
|
],
|
|
210
|
+
// 取消入口图标(详情页「取消」按钮;语义=作废不派发,与删除 trash-2 区分)
|
|
211
|
+
'ban': [
|
|
212
|
+
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
213
|
+
['path', { d: 'm4.9 4.9 14.2 14.2' }]
|
|
214
|
+
],
|
|
210
215
|
'plus': [
|
|
211
216
|
['path', { d: 'M5 12h14' }],
|
|
212
217
|
['path', { d: 'M12 5v14' }]
|
|
@@ -364,10 +369,14 @@ function apply(ctx) {
|
|
|
364
369
|
state.poolStatus = (d && d.poolStatus) || null
|
|
365
370
|
// token 消耗聚合(board 级,host 端现算):范围守卫——响应回来时范围若已变,保留旧值不覆盖
|
|
366
371
|
// (新范围的那次请求会带着新聚合回来;老 host 不返回该字段 → null,零渲染)
|
|
367
|
-
// ===== usageSummary
|
|
368
|
-
//
|
|
369
|
-
// →
|
|
370
|
-
//
|
|
372
|
+
// ===== usageSummary 变化检测 + React 订阅(task-muwq9u04 → task-mv10lvud 根修)=====
|
|
373
|
+
// 病根(task-mv10lvud 实测「点今天也没变化」):范围切换时 setRange 先换 state.dateRange(新对象
|
|
374
|
+
// → setDR 身份变化 → 第一帧渲染成功),随后 fetchTasks 响应把 state.usageSummary 换成新对象并 notify(),
|
|
375
|
+
// 但 TopPanel 的 update() 里所有 setX 值都 Object.is 全等(tasks 引用不变、布尔不变)→ React 18 全量
|
|
376
|
+
// bailout,零重渲染 → Token 区永远拿不到新聚合。notify ≠ React 重渲染,须有「新身份的订阅值」做载体。
|
|
377
|
+
// 修法:TopPanel 补 usageSummary 专属 React state(setUsage(state.usageSummary)),host 每次响应都是
|
|
378
|
+
// 新对象,身份变化天然触发渲染(订阅见 TopPanel 的 update())。
|
|
379
|
+
// 为什么本处用「上一次的 JSON 串」比对而不是对象引用:host 每次 get-tasks 都现算聚合、**必然返回新对象**
|
|
371
380
|
// (引用比较恒真)→ 3s 轮询每轮都 notify,tasksHash 的渲染节约当场作废。JSON 串只在这份聚合
|
|
372
381
|
// 真变了时才不等(KB 级体积、3s 一次,代价可忽略);赋值前先快照,赋值后比对。
|
|
373
382
|
var usageJsonPrev = JSON.stringify(state.usageSummary || null)
|
|
@@ -511,6 +520,23 @@ function apply(ctx) {
|
|
|
511
520
|
}).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
|
|
512
521
|
}
|
|
513
522
|
|
|
523
|
+
// ===== 取消通道(与 host cancel-task / task_cancel 门禁一一对应)=====
|
|
524
|
+
// 可取消状态:草稿/待办/阻塞(未进入执行)。in-progress/verifying 需先终止;resolved 引导归档。
|
|
525
|
+
// 取消=显式放弃:置 cancelled + 留档可检索、可在归档 tab「恢复待办」;与删除不同,取消不丢数据。
|
|
526
|
+
function canCancel(t) { return !!t && (t.status === 'draft' || t.status === 'pending' || t.status === 'blocked') }
|
|
527
|
+
// confirm 预警(与 note「取消后不再派发,可在归档恢复」同口径);失败时 host 门禁原因原样回显。
|
|
528
|
+
function cancelTask(id, title, onMsg) {
|
|
529
|
+
var t = getTask(id)
|
|
530
|
+
var name = title || (t && t.title) || id
|
|
531
|
+
if (!window.confirm('取消后不再派发,可在归档恢复。确认取消「' + name + '」?')) return Promise.resolve({ ok: false, cancelled: true })
|
|
532
|
+
return rpc('cancel-task', { taskId: id }).then(function (r) {
|
|
533
|
+
if (r && r.ok === false) { if (onMsg) onMsg('⚠️ ' + (r.error || '取消失败')) }
|
|
534
|
+
else if (onMsg) onMsg('🚫 已取消「' + name + '」(可在归档 tab 恢复待办)')
|
|
535
|
+
fetchTasks()
|
|
536
|
+
return r
|
|
537
|
+
}).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
|
|
538
|
+
}
|
|
539
|
+
|
|
514
540
|
function onDragStart(e, task) { e.dataTransfer.setData('text/plain', JSON.stringify({ id: task.id, status: task.status })); e.dataTransfer.effectAllowed = 'move'; state.dragTask = task.id; notify() }
|
|
515
541
|
function onDragEnd() { state.dragTask = null; state.dragOver = null; notify() }
|
|
516
542
|
function onColDragOver(e, st) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (state.dragOver !== st) { state.dragOver = st; notify() } }
|
|
@@ -589,7 +615,8 @@ function apply(ctx) {
|
|
|
589
615
|
var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
|
|
590
616
|
var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
|
|
591
617
|
var _vug = useState(state.verifyUserGuide), vugOn = _vug[0], setVugOn = _vug[1] // 自测指南开关勾选态(设置区「验收」)
|
|
592
|
-
|
|
618
|
+
var _usg = useState(state.usageSummary), setUsage = _usg[1] // Token 区聚合订阅(task-mv10lvud 根修):usageSummary 换新对象即触发渲染
|
|
619
|
+
useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setTeamMode(state.teamMode); setWorkModeState(state.workMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin); setFbEnabled(state.feedbackEnabled); setNdOn(state.notifyDispatch); setNnOn(state.notifyDone); setEsOn(state.epicSplit); setVugOn(state.verifyUserGuide); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash); setTasksErr(state.tasksErr); setUsage(state.usageSummary) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
593
620
|
if (!open) return null
|
|
594
621
|
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
595
622
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -826,18 +853,21 @@ function apply(ctx) {
|
|
|
826
853
|
function ArchiveView() {
|
|
827
854
|
var _R = React; var useState = _R.useState
|
|
828
855
|
var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
|
|
829
|
-
//
|
|
856
|
+
// 归档 tab 数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
|
|
830
857
|
// 不再依赖独立 state.archived/fetchArchived——旧路径异步填库却未接入 TopPanel 的 notify 更新项,
|
|
831
858
|
// 归档 tab 打开后停在「暂无归档任务」空态,而仪表盘「已归档」计数来自 state.tasks(全量),二者对不上。
|
|
832
|
-
// 全量 +
|
|
833
|
-
|
|
859
|
+
// 全量 + 按归档/取消时间倒序(archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt)+ 顶部注明条数。
|
|
860
|
+
// 含 cancelled(反馈 n-mv0zbqk... 取消通道):取消卡在待办列消失(cancelled 不在 COLUMNS)后,
|
|
861
|
+
// 归入本 tab 可见、可「恢复待办」——否则作废卡从 UI 上彻底蒸发、无法找回。
|
|
862
|
+
var archived = state.tasks.filter(function (t) { return t.status === 'archived' || t.status === 'cancelled' })
|
|
863
|
+
var cancelledCount = archived.filter(function (t) { return t.status === 'cancelled' }).length // 文案口径修正:仪表盘「已归档」只数 archived,本 tab 含 cancelled,须单列
|
|
834
864
|
var list = archived.filter(function (t) {
|
|
835
865
|
if (!q) return true
|
|
836
866
|
var qq = q.toLowerCase()
|
|
837
867
|
return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
|
|
838
868
|
}).sort(function (a, b) {
|
|
839
|
-
//
|
|
840
|
-
function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
|
|
869
|
+
// 归档/取消时间倒序:archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt(老卡可能缺 archivedAt)
|
|
870
|
+
function archTs(t) { return t.archivedAt || t.cancelledAt || t.resolvedAt || t.createdAt || '' }
|
|
841
871
|
return (archTs(b) || '').localeCompare(archTs(a) || '')
|
|
842
872
|
})
|
|
843
873
|
// 工作模式(与 fetchTasks 同口径):list=清单模式(恢复不重派)不弹;auto/team 恢复即重派 → confirm 门禁
|
|
@@ -850,8 +880,8 @@ function apply(ctx) {
|
|
|
850
880
|
}
|
|
851
881
|
return React.createElement('div', null,
|
|
852
882
|
React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.border2, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
|
|
853
|
-
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + '
|
|
854
|
-
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '
|
|
883
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 }, title: '归档 tab 含已归档 + 已取消两类;仪表盘「已归档」只数已归档,不数已取消' }, '共 ' + archived.length + ' 条' + (cancelledCount > 0 ? '(含已取消 ' + cancelledCount + ' 条)' : '')),
|
|
884
|
+
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档/已取消任务') :
|
|
855
885
|
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
856
886
|
list.map(function (t) {
|
|
857
887
|
// 归档行点击进详情(用户 2026-10-09 指令):复用 state.detailId 机制,与看板卡同路径;
|
|
@@ -861,7 +891,8 @@ function apply(ctx) {
|
|
|
861
891
|
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: t.title }, t.title),
|
|
862
892
|
reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0 } }, '👁 建议过目') : null,
|
|
863
893
|
t.verification ? React.createElement('span', { title: '验收结论: ' + (t.verification.summary || '').slice(0, 100), style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.verification.verdict === 'approved' ? C.ok : C.err, color: C_INV } }, t.verification.verdict === 'approved' ? '过' : '驳') : null,
|
|
864
|
-
React.createElement('span', { style: { fontSize: 9,
|
|
894
|
+
t.status === 'cancelled' ? React.createElement('span', { title: '已取消(可恢复待办)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 20%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0 } }, '已取消') : null,
|
|
895
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, (t.archivedAt || t.cancelledAt) ? ago(t.archivedAt || t.cancelledAt) : '')),
|
|
865
896
|
t.verification && t.verification.summary ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + t.verification.summary) : null,
|
|
866
897
|
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
|
|
867
898
|
React.createElement('button', { onClick: function (e) { e.stopPropagation(); restore(t.id) }, title: wm === 'list' ? '恢复为待办' : '恢复为待办(将按当前工作模式立即重新派发,消耗一轮 Worker+Verifier token)', style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '↩ 恢复待办'),
|
|
@@ -1435,6 +1466,8 @@ function apply(ctx) {
|
|
|
1435
1466
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'approved' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('check-circle', 10), '通过') : null,
|
|
1436
1467
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { var r = window.prompt('驳回原因:'); doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'rejected', comment: r || '' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.err, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('x-circle', 10), '驳回') : null,
|
|
1437
1468
|
task.status === 'resolved' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('archive-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.text2, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('archive', 10), '归档') : null),
|
|
1469
|
+
// 取消(仅草稿/待办/阻塞):confirm 预警「取消后不再派发,可在归档恢复」;与删除并列但语义=作废留档(可恢复)
|
|
1470
|
+
canCancel(task) ? React.createElement('button', { onClick: function () { cancelTask(task.id, task.title, setActionMsg) }, title: '取消任务(取消后不再派发,可在归档恢复待办)', style: { fontSize: 10, padding: '3px 8px', border: '1px solid ' + C.warn, borderRadius: 3, background: 'transparent', color: C.warn, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('ban', 10), '取消') : null,
|
|
1438
1471
|
// 删除(仅草稿/待办/阻塞):confirm 防误删;被 host 门禁拒绝时把原因回显到详情页 actionMsg
|
|
1439
1472
|
// 容器 div 的收尾括号已挂在「归档」行;删除按钮作为**额外一行元素**排在它后面
|
|
1440
1473
|
// (多一个实参给同一个 createElement,属于合法调用),因此本行自身必须配平。
|
|
@@ -2055,13 +2088,14 @@ function apply(ctx) {
|
|
|
2055
2088
|
// 近 14 天柱形通用画法(① 通过率 / ③ 超时率共用):cells = [{ k, rate(null=当日无样本), tip }]
|
|
2056
2089
|
// 今日柱 brand 高亮 + 率值顶标;无样本日画 3px 灰基线(title 写明无样本,不假装 0%)。
|
|
2057
2090
|
function qtDayBars(cells, color, todayKey) {
|
|
2058
|
-
return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46 } },
|
|
2091
|
+
return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46, minWidth: 0, overflow: 'hidden' } },
|
|
2059
2092
|
cells.map(function (c) {
|
|
2060
2093
|
var isToday = c.k === todayKey
|
|
2061
2094
|
var h = c.rate === null ? 3 : Math.max(4, Math.round(c.rate * 34))
|
|
2095
|
+
h = Math.min(22, h) // clamp 柱高:100% 柱封顶 22px——46px 绘图容器扣掉上下率值/日期标签与 gap/描边后不越出,防单日 100% 黑柱向上盖住右列
|
|
2062
2096
|
var kp = c.k.split('-')
|
|
2063
2097
|
var dLabel = Number(kp[1]) + '/' + Number(kp[2])
|
|
2064
|
-
return React.createElement('div', { key: c.k, style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
|
|
2098
|
+
return React.createElement('div', { key: c.k, style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
|
|
2065
2099
|
c.rate !== null ? React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, mpPct(c.rate)) : null,
|
|
2066
2100
|
React.createElement('div', { style: { width: '100%', maxWidth: 22, height: h + 'px', borderRadius: 2, background: c.rate === null ? C.nested : (isToday ? C.brand : color), border: '1px solid ' + (isToday ? C.brand : (c.rate === null ? C.border : color)) } }),
|
|
2067
2101
|
React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, dLabel))
|
|
@@ -2106,7 +2140,7 @@ function apply(ctx) {
|
|
|
2106
2140
|
})
|
|
2107
2141
|
var fpHas = false; fpCells.forEach(function (c) { if (c.rate !== null) fpHas = true })
|
|
2108
2142
|
var fpToday = fpCells[fpCells.length - 1]
|
|
2109
|
-
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
2143
|
+
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
|
|
2110
2144
|
blockHead('一次通过率(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: fpToday.rate !== null ? C.brand : C.text2, fontWeight: 600 } }, fpToday.rate !== null ? mpPct(fpToday.rate) : '—')), '一次通过率 = 当天落定「已完成」任务里零驳回(rejectCount=0)的占比;驳回 = Verifier 验收判 reject'),
|
|
2111
2145
|
fpHas ? qtDayBars(fpCells, C.ok, todayKey)
|
|
2112
2146
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 resolved 任务落定'))
|
|
@@ -2134,7 +2168,7 @@ function apply(ctx) {
|
|
|
2134
2168
|
})
|
|
2135
2169
|
var toHas = false; toCells.forEach(function (c) { if (c.rate !== null) toHas = true })
|
|
2136
2170
|
var toToday = toCells[toCells.length - 1]
|
|
2137
|
-
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
2171
|
+
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
|
|
2138
2172
|
blockHead('超时率走势(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: toToday.rate !== null ? C.warn : C.text2, fontWeight: 600 } }, toToday.rate !== null ? mpPct(toToday.rate) : '—')), '超时率 = 超时落定(timeout/error)的 run 占当天 run 的比例'),
|
|
2139
2173
|
toHas ? qtDayBars(toCells, C.warn, todayKey)
|
|
2140
2174
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 run 落定'))
|
package/lib/core.mjs
CHANGED
|
@@ -260,6 +260,27 @@ export function archiveApply(d, t, actor, note) {
|
|
|
260
260
|
if (p) r.parentUpdated = true
|
|
261
261
|
return r
|
|
262
262
|
}
|
|
263
|
+
// 取消落定(task_cancel 工具与 cancel-task RPC 共享,单一行为口径;门禁 cancelGate 留在
|
|
264
|
+
// rpc.mjs——它只需判 status + claimedBy,无需 IO,与 archiveErr/deleteGate 同居「门禁在接线层、
|
|
265
|
+
// 状态流转在 core」的分工)。置 cancelled + 记历史(actor + 理由 note 可选)。
|
|
266
|
+
// spawn-pending 占位回退:claimedBy='spawn-pending' 是派发瞬时占位(无真实 run,spawn 尚在途),
|
|
267
|
+
// 取消时与 spawn 失败回退同口径清认领位(claimedBy/claimedAt → null),不留死占位。
|
|
268
|
+
// 取消不级联子卡(删除有「未归档子卡」门禁防悬空引用;取消只标记本卡,父卡不删,子卡 parentId
|
|
269
|
+
// 仍指向有效卡);但取消子卡同样触发父卡自动收口(isChildSettled 已计 cancelled)。
|
|
270
|
+
export function cancelApply(d, t, actor, note) {
|
|
271
|
+
var ps = t.status
|
|
272
|
+
if (t.claimedBy) { t.claimedBy = null; t.claimedAt = null } // spawn-pending / 残留认领位回退
|
|
273
|
+
t.status = 'cancelled'
|
|
274
|
+
t.cancelledAt = new Date().toISOString()
|
|
275
|
+
t.resolvedAt = null
|
|
276
|
+
t.resolution = null
|
|
277
|
+
delete t.stuckSince
|
|
278
|
+
ah(t, ps, 'cancelled', actor, note || 'cancelled')
|
|
279
|
+
var r = { ok: true, task: t }
|
|
280
|
+
var p = maybeAutoCloseParent(d, t) // 取消路径同样触发父卡自动收口(isChildSettled 已计 cancelled)
|
|
281
|
+
if (p) r.parentUpdated = true
|
|
282
|
+
return r
|
|
283
|
+
}
|
|
263
284
|
|
|
264
285
|
// ===== 史诗 hooks=agent run(宿主生命周期接线)=====
|
|
265
286
|
// 定位:hook 点 = 一次**真实 agent 运行**(不是声明式命令、不走 shell),挂在 epic 卡上、
|
package/lib/dispatch.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const { ah, cfg, claimApply, resolveApply, verifyApply, lateVerdictApply, parseS
|
|
|
12
12
|
export function createDispatch(ctx, state, deps) {
|
|
13
13
|
// 宿主 fs 句柄与「会话工作区解析根」随预研注入瘦身退役(task-muvjs392):派发侧不再读盘——
|
|
14
14
|
// 清单只给「路径:L行号 — 一句用途」,文件内容由 Worker 自己用 read 工具按行号范围自取。
|
|
15
|
-
var rt = deps.rt,
|
|
15
|
+
var rt = deps.rt, mutateLocked = deps.mutateLocked, kickCycle = deps.kickCycle
|
|
16
16
|
var rootForSession = deps.rootForSession, withTimeout = deps.withTimeout, runsFor = deps.runsFor, feedbackOn = deps.feedbackOn
|
|
17
17
|
// 会话日志根(可选注入):usage 增量结算(卡3)必须「真读真日志」才测得出重复计账——
|
|
18
18
|
// 生产不注入(usage.mjs 回退 os.homedir()),单测注入临时目录(否则只能开子进程改 HOME,
|
|
@@ -1114,7 +1114,7 @@ export function createDispatch(ctx, state, deps) {
|
|
|
1114
1114
|
try { await reconcileRoot(sid) } catch (e) { console.error('[task-board] reconcile 失败:', String(e)) }
|
|
1115
1115
|
// 幽灵占位回收(池冻结根修):必须排在活跃度计数与 pickDispatch 之前——残留表项正是把 capW 拉到 0、
|
|
1116
1116
|
// 并把卡挡在 Verifier 派发之外的元凶(见 reapGhostRecs 头注释)。收尾会写盘(结局/usage),
|
|
1117
|
-
// 故有回收时重读一次快照——否则下面空闲快进的
|
|
1117
|
+
// 故有回收时重读一次快照——否则下面空闲快进的 stalePool/staleInfo 判定会拿旧快照误判(写盘已归一进锁)。
|
|
1118
1118
|
var reapedN = 0
|
|
1119
1119
|
try { reapedN = await reapGhostRecs(sid, snap) } catch (e) { console.error('[task-board] 幽灵占位回收失败(本轮跳过,派发照常):', String(e)) }
|
|
1120
1120
|
if (reapedN > 0) {
|
|
@@ -1137,8 +1137,20 @@ export function createDispatch(ctx, state, deps) {
|
|
|
1137
1137
|
var stalePool = snap.poolStatus && (((snap.poolStatus.workers || []).length + (snap.poolStatus.verifiers || []).length) > 0)
|
|
1138
1138
|
// dispatchInfo 是瞬时通知(90s TTL):空闲周期也要负责过期清理,否则永久残留
|
|
1139
1139
|
var staleInfo = snap.dispatchInfo && (!snap.dispatchInfoAt || Date.now() - new Date(snap.dispatchInfoAt).getTime() > 90000)
|
|
1140
|
-
if (staleInfo) {
|
|
1141
|
-
|
|
1140
|
+
if (stalePool || staleInfo) {
|
|
1141
|
+
// 锁归一(P1 根修 task-mv1e17x9):空闲清理写盘必须走 mutateLocked,且过期判定在锁内重读重判——
|
|
1142
|
+
// 锁外读到的 stalePool/staleInfo 此刻可能已被并发写清掉,拿旧快照直写会把它覆盖回去(丢更新)。
|
|
1143
|
+
try {
|
|
1144
|
+
await mutateLocked(sid, function (d) {
|
|
1145
|
+
var sp2 = d.poolStatus && (((d.poolStatus.workers || []).length + (d.poolStatus.verifiers || []).length) > 0)
|
|
1146
|
+
var si2 = d.dispatchInfo && (!d.dispatchInfoAt || Date.now() - new Date(d.dispatchInfoAt).getTime() > 90000)
|
|
1147
|
+
if (!sp2 && !si2) return null // 锁内重判已无过期项 → 不写(避免覆盖并发写)
|
|
1148
|
+
if (sp2) d.poolStatus = emptyPool
|
|
1149
|
+
if (si2) { delete d.dispatchInfo; delete d.dispatchInfoAt }
|
|
1150
|
+
return true // 返回非空触发写盘
|
|
1151
|
+
}, true) // skipKick:本轮已是 poolCycle,无需再踢
|
|
1152
|
+
} catch (_) {}
|
|
1153
|
+
}
|
|
1142
1154
|
return snap
|
|
1143
1155
|
}
|
|
1144
1156
|
var c = cfg(snap)
|
|
@@ -1374,5 +1386,5 @@ export function createDispatch(ctx, state, deps) {
|
|
|
1374
1386
|
// 在并行 spawn 下存在认领错包的潜伏 bug,一并消灭。
|
|
1375
1387
|
}
|
|
1376
1388
|
|
|
1377
|
-
return { poolCycle: poolCycle, spawnOneShot: spawnOneShot, accumulateRunUsage: accumulateRunUsage, settleReportedRun: settleReportedRun, readContextPack: readContextPack }
|
|
1389
|
+
return { poolCycle: poolCycle, spawnOneShot: spawnOneShot, accumulateRunUsage: accumulateRunUsage, settleReportedRun: settleReportedRun, readContextPack: readContextPack, closeRunHistory: closeRunHistory }
|
|
1378
1390
|
}
|