dsh-agent-board 1.7.8 → 1.7.9
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 +12 -3
- package/lib/cleanup.mjs +256 -0
- package/lib/client/dashboard.js +128 -3
- package/lib/client/kernel.js +32 -7
- package/lib/client/task-detail.js +13 -7
- package/lib/client.js +173 -17
- package/lib/core.mjs +9 -2
- package/lib/dispatch.mjs +65 -0
- package/lib/rpc.mjs +103 -18
- package/package.json +1 -1
- package/test/cleanup.test.mjs +341 -0
- package/test/conditional-get-tasks.test.mjs +96 -0
- package/test/core.test.mjs +152 -6
- package/test/helpers/mock-ctx.mjs +7 -2
- package/test/ui-zoom-client.test.mjs +76 -0
package/lib/rpc.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { TASK_SIZE_CONTRACT, withSplitHint, pushRejectLesson, pushArbitrationLes
|
|
|
13
13
|
import { makeMsg } from './notify.mjs'
|
|
14
14
|
import { computeHealthHints, computeRuntimeHealthHints } from './health.mjs'
|
|
15
15
|
import { isFullSessionId } from './session.mjs' // 幻影板防线口径(纯函数,与 policy.mjs 直引同例)
|
|
16
|
+
import { previewCleanup, runCleanup, SKIP_REASON_TEXT } from './cleanup.mjs' // 已结算会话清理(预览+执行)
|
|
16
17
|
const { ah, isb, gsb, gpt, vt, validateDeps, classifyPipeline, cfg, claimCheck, claimApply, resolveApply, verifyApply, lateVerdictApply, archiveApply, cancelApply, maybeAutoCloseParent, PRIO_RANK, touchesConflict, holdsFiles, boardHome, aggregateChildStats, createTaskWarnings, epicPrecheck, epicPrecheckNote, attachContextSuggestions, REJECT_REDISPATCH_HINT, tasksHash, normalizeHooks, mergeHooks, pushRejection, normalizeUserTest, draftLint } = core
|
|
17
18
|
|
|
18
19
|
function defineTool(options) {
|
|
@@ -38,10 +39,45 @@ function defineTool(options) {
|
|
|
38
39
|
export function createRpc(ctx, state, deps) {
|
|
39
40
|
// deps:session/store/notify/dispatch 各模块公共 API(index.mjs 接线注入)
|
|
40
41
|
var getActorId = deps.getActorId, resolveRoot = deps.resolveRoot, toolSessionId = deps.toolSessionId, rpcSessionId = deps.rpcSessionId
|
|
42
|
+
// ===== GUI RPC 操作者回退(task-mv493xjs,巡检次生观察①)=====
|
|
43
|
+
// 面板 HTTP RPC 没有 agent 调用栈(currentInitiator 为空),getActorId() 落 'unknown',
|
|
44
|
+
// 流转轨迹 actor 列就显示 unknown。GUI rpc 助手(kernel.js)必带 sessionId 入参,
|
|
45
|
+
// 这里回退到 rpcSessionId(args)(显式 sessionId 优先,缺失时退化解为 actor 归一值),
|
|
46
|
+
// 历史行 actor 落真实会话身份。工具通道(task_*)有真实 initiator,不走本回退。
|
|
47
|
+
function rpcActor(args) { var a = getActorId(); if (a && a !== 'unknown') return a; return rpcSessionId(args) }
|
|
41
48
|
var rootForSession = deps.rootForSession, deriveWorkMode = deps.deriveWorkMode, runsFor = deps.runsFor
|
|
42
49
|
var rt = deps.rt, mutateLocked = deps.mutateLocked
|
|
43
50
|
// 主窗口消耗(task-muwsol23):测试可注入临时日志根;生产 undefined → findRunLog 回退 ~/.dsh/sessions
|
|
44
51
|
var sessionsRoot = (typeof deps.sessionsRoot === 'string' && deps.sessionsRoot) ? deps.sessionsRoot : undefined
|
|
52
|
+
// ===== 已结算会话清理(task-mv2131cz):路径根 + 活跃/血统保护集 =====
|
|
53
|
+
// 会话日志与 projcache 布局(与 usage.mjs findRunLog 同源):~/.dsh/sessions/<bucket>/<id>/、
|
|
54
|
+
// ~/.dsh/storages/session_projcache/sessions/<id>.json;注册表索引 ~/.dsh/storages/session_projcache.json
|
|
55
|
+
// 是宿主托管(version/seq 结构),**不清理**(残留形态见 README「存储清理」小节)。
|
|
56
|
+
var cleanupRoots = {
|
|
57
|
+
sessions: path.join(boardHome(), 'sessions'),
|
|
58
|
+
projcacheDir: path.join(boardHome(), 'storages', 'session_projcache', 'sessions'),
|
|
59
|
+
}
|
|
60
|
+
// 体积估算主源(task-mv39ag1e):官方 SessionPersistenceSnapshot.sizeBytes——stat(id) 只读元数据
|
|
61
|
+
// (不读事件日志、不取写所有权,preview 零副作用不破),替代逐会话目录递归(消全量预览 ~340ms 同步 IO)。
|
|
62
|
+
// 老宿主/测试桩无 sessionPersistence → statSession=null → cleanup 层回退目录遍历(双轨降级不炸)。
|
|
63
|
+
var cleanupSp = ctx.get && ctx.get('sessionPersistence')
|
|
64
|
+
var statSession = (cleanupSp && typeof cleanupSp.stat === 'function') ? function (id) { return cleanupSp.stat(id) } : null
|
|
65
|
+
// 不可删保护集:本板主会话自身 + 活跃树里所有会话(roots + list)——覆盖「不活跃」与「本会话血统」两闸。
|
|
66
|
+
// 「不活跃」判定维持活跃树/血统口径(task-mv39ag1e 决策留痕):上游官方活性判据是 open(id,'write')
|
|
67
|
+
// 抛 SessionAlreadyOwnedError,但 open 'write' 不抛时即**真拿到单写所有权**(且 handle close 会 drain
|
|
68
|
+
// 落盘)——探活本身有副作用,preview 零副作用红线不允许,故不升级,维持现有活跃树判定。
|
|
69
|
+
function cleanupBlockedIds(sid) {
|
|
70
|
+
var blocked = new Set()
|
|
71
|
+
blocked.add(String(sid)) // 本板主会话自身
|
|
72
|
+
var ag = ctx.agents
|
|
73
|
+
if (ag) {
|
|
74
|
+
try {
|
|
75
|
+
var roots = ag.roots(); if (Array.isArray(roots)) for (var i = 0; i < roots.length; i++) blocked.add(String(roots[i].id))
|
|
76
|
+
var all = ag.list(); if (Array.isArray(all)) for (var j = 0; j < all.length; j++) blocked.add(String(all[j].id))
|
|
77
|
+
} catch (_) {}
|
|
78
|
+
}
|
|
79
|
+
return blocked
|
|
80
|
+
}
|
|
45
81
|
var maybeNotify = deps.maybeNotify, notifyTaskDone = deps.notifyTaskDone
|
|
46
82
|
var notifyLateReject = (typeof deps.notifyLateReject === 'function') ? deps.notifyLateReject : null // 迟到驳回 err 级通知(未接线 → 静默跳过)
|
|
47
83
|
var spawnOneShot = deps.spawnOneShot, accumulateRunUsage = deps.accumulateRunUsage, readContextPack = deps.readContextPack
|
|
@@ -197,13 +233,18 @@ export function createRpc(ctx, state, deps) {
|
|
|
197
233
|
// 旧实现 roots() 只含活体会话——休眠根会话(有板有卡)恒 false,且生成开始/结束 agents 树重建有瞬态窗口
|
|
198
234
|
// 返回不含本 sid(假 false),入口分钟级消失。改读持久 parentSession(见 isRootPersistent):
|
|
199
235
|
// 主源 sessionPersistence.stat(sid)(持久表,休眠会话也读得到)→ header.parentSession 缺省 = 根;
|
|
236
|
+
// fork/seed 会话(v3→v4 迁移产物:parentSession 有值但 delegationDepth===0)同样认根
|
|
237
|
+
// (task-mv59nn9m:「本体数据平台开发」fork 会话被旧判据误判非根导致看板不可见);
|
|
200
238
|
// 老宿主/测试桩无 sessionPersistence → 回退活跃 agent 的 session.header.parentSession,再回退旧 roots() 成员判定。
|
|
201
239
|
// 防抖仍在客户端:kernel.js applyIsRoot——曾确认 true 的会话需连续 3 次 false(3s 心跳≈9s)才收抽屉;
|
|
202
240
|
// 从未 true 的子代理会话仍即时收起(反馈 n-muuerxv9ijxs / task-muupr8ld)。
|
|
203
241
|
async function isRootPersistent(sid) {
|
|
204
242
|
var sp = ctx.get && ctx.get('sessionPersistence')
|
|
205
243
|
if (sp && typeof sp.stat === 'function') {
|
|
206
|
-
|
|
244
|
+
// 判据改用 delegationDepth(task-mv59nn9m):无父 = 根;有父但 delegationDepth===0 的 fork/seed
|
|
245
|
+
// 会话同样认根;delegationDepth 缺省(v3 时代老会话无此字段)且有 parentSession 时维持非根
|
|
246
|
+
// 保守判定——兼容 v3 时代子代理不误放行。
|
|
247
|
+
try { var snap = await sp.stat(sid); if (snap && snap.header) { var hh = snap.header; return !hh.parentSession || (typeof hh.delegationDepth === 'number' && hh.delegationDepth === 0) } } catch (_) {}
|
|
207
248
|
}
|
|
208
249
|
var ag = ctx.agents
|
|
209
250
|
if (ag) {
|
|
@@ -234,6 +275,9 @@ export function createRpc(ctx, state, deps) {
|
|
|
234
275
|
d.epicSplit = cfg(d).epicSplit
|
|
235
276
|
// 自测指南开关(设置区「验收」小节)同法透出:缺字段=开,只有显式 false 才关
|
|
236
277
|
d.verifyUserGuide = cfg(d).verifyUserGuide
|
|
278
|
+
// 清理保留天数(设置区「存储清理」小节):cfg 归一(缺字段/脏值=7,钳 1-90)后透出确定数值,
|
|
279
|
+
// 客户端设置区天数输入与 StorageCleanup 文案都读这一个字段(口径单一,不各自猜缺省)。
|
|
280
|
+
d.cleanupRetentionDays = cfg(d).cleanupRetentionDays
|
|
237
281
|
// 工作模式派生字段:UI 只读这一个字段决定三档选中态(不落盘,写入仍走 boardMode/teamMode)
|
|
238
282
|
d.workMode = deriveWorkMode(d)
|
|
239
283
|
// ===== board 级 token 消耗聚合(现算,不落盘):总量 + 输入/输出/缓存读 + 按模型 + 任务 Top8 =====
|
|
@@ -290,8 +334,21 @@ export function createRpc(ctx, state, deps) {
|
|
|
290
334
|
if (__rkt && __rhints[__rkt.id]) __rkt.reviewHint = __rhints[__rkt.id]
|
|
291
335
|
}
|
|
292
336
|
// 渲染变更检测(反馈 n-mut9rzs2mkhg):tasks 关键字段的稳定 hash(纯函数,口径见 core.tasksHash),
|
|
293
|
-
// 客户端 3s 轮询 hash 相同则跳过 state.tasks 赋值 + notify
|
|
337
|
+
// 客户端 3s 轮询 hash 相同则跳过 state.tasks 赋值 + notify
|
|
294
338
|
d.tasksHash = tasksHash(d.tasks)
|
|
339
|
+
// ===== 条件请求瘦身(task-mv492for,巡检实证 n-mv2turd1o3iv):轮询载荷 1.34MB→KB 级 =====
|
|
340
|
+
// 客户端轮询带上轮 tasksHash 作 args.ifHash:一致说明任务列表实质没变——reviewHint 挂在卡上
|
|
341
|
+
// 已进 hash 序列化口径;childStats 纯由 tasks 派生(输入 status/parentId 已覆盖);usageSummary/
|
|
342
|
+
// healthHints/poolStatus/childStats/各开关是 KB 级聚合,unchanged 分支照常现算返回(上方赋值链不动)。
|
|
343
|
+
// 响应只剔 tasks 全量(description/context/runs/deliverable 全文——1.34MB 大头全在这),
|
|
344
|
+
// 客户端收 unchanged:true 保留 state.tasks 不动(见 kernel.js fetchTasks 的 unchanged 分支)。
|
|
345
|
+
// 不带 ifHash 的老客户端与 includeArchived(归档 tab 数据源 fetchArchived)一律走原有全量分支,零回归。
|
|
346
|
+
if (args && args.ifHash && args.ifHash === d.tasksHash && !args.includeArchived) {
|
|
347
|
+
var __light = Object.assign({}, d)
|
|
348
|
+
delete __light.tasks
|
|
349
|
+
__light.unchanged = true
|
|
350
|
+
return __light
|
|
351
|
+
}
|
|
295
352
|
return d })
|
|
296
353
|
// 派发前上下文预览:主 agent 用它确认"我将注入给子代理的瘦身清单"是否足够(不发任务、不落盘)
|
|
297
354
|
handle('preview-context', async function (args) {
|
|
@@ -371,10 +428,10 @@ export function createRpc(ctx, state, deps) {
|
|
|
371
428
|
out.sort(function (a, b) { return (b.lastActivity || '').localeCompare(a.lastActivity || '') })
|
|
372
429
|
return { ok: true, boards: out }
|
|
373
430
|
})
|
|
374
|
-
handle('claim-task', async function (args) { var sid = rpcSessionId(args); var actor =
|
|
375
|
-
handle('resolve-task', async function (args) { var sid = rpcSessionId(args); var actor =
|
|
376
|
-
handle('verify-task', async function (args) { var sid = rpcSessionId(args); var actor =
|
|
377
|
-
handle('archive-task', async function (args) { var sid = rpcSessionId(args); var actor =
|
|
431
|
+
handle('claim-task', async function (args) { var sid = rpcSessionId(args); var actor = rpcActor(args); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var err = claimCheck(d, t, actor); if (err) return { ok: false, error: err }; claimApply(d, t, actor, 'manual claim via board'); return { ok: true, task: t } }) })
|
|
432
|
+
handle('resolve-task', async function (args) { var sid = rpcSessionId(args); var actor = rpcActor(args); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'in-progress') return { ok: false, error: 'not in-progress' }; return resolveApply(d, t, actor, args.status, args.resolution, args.resolution || args.status) }) })
|
|
433
|
+
handle('verify-task', async function (args) { var sid = rpcSessionId(args); var actor = rpcActor(args); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'verifying') return { ok: false, error: 'not verifying' }; var __act = activeRunOf(sid, t); if (__act && __act.role === 'verifier' && args.force !== true) return { ok: false, error: 'Verifier 还在跑(run ' + __act.id + '),提前裁决将丢弃它的独立结论。确认提前裁决请加 force:true', verifierActive: __act.id }; delete t.escalation; delete t.verifyRetries; var r = verifyApply(d, t, actor, args.verdict, args.comment); if (args.verdict === 'rejected') { t.verification = { verdict: 'rejected', summary: args.comment || '', checks: '', at: new Date().toISOString(), by: actor }; pushRejection(t, args.comment, '', t.verification.at, actor); pushRejectLesson(d, t, args.comment); r.hint = REJECT_REDISPATCH_HINT } return r }) })
|
|
434
|
+
handle('archive-task', async function (args) { var sid = rpcSessionId(args); var actor = rpcActor(args); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var __ae = archiveErr(sid, t); if (__ae) return { ok: false, error: __ae }; return archiveApply(d, t, actor, 'manual archive') }) })
|
|
378
435
|
// ===== 已阅落账(异常驱动审视①③):打开详情页即幂等落 reviewedAt(唯一新落库字段)=====
|
|
379
436
|
// 幂等只写一次:已有 reviewedAt 就原样返回、绝不重写(时间戳不漂移、history 不刷屏);
|
|
380
437
|
// 后续重复打开详情页零副作用。reviewedAt 进 tasksHash 序列化口径 → 落账后下一轮 get-tasks hash 变化,
|
|
@@ -403,7 +460,7 @@ export function createRpc(ctx, state, deps) {
|
|
|
403
460
|
// 单任务删除:真删(从 d.tasks 数组移除),不写 history(记录随任务一起消失),不留档、无 undo。
|
|
404
461
|
// console.error 留一行操作日志,便于事后溯源"某个卡片什么时候被谁删了"。
|
|
405
462
|
handle('delete-task', async function (args) {
|
|
406
|
-
var sid = rpcSessionId(args); var actor =
|
|
463
|
+
var sid = rpcSessionId(args); var actor = rpcActor(args)
|
|
407
464
|
if (!args.taskId) return { ok: false, error: 'taskId required' }
|
|
408
465
|
return mutateLocked(sid, function (d) {
|
|
409
466
|
var t = d.tasks.find(function (x) { return x.id === args.taskId })
|
|
@@ -447,8 +504,8 @@ export function createRpc(ctx, state, deps) {
|
|
|
447
504
|
return cancelApply(d, t, actor, note)
|
|
448
505
|
})
|
|
449
506
|
}
|
|
450
|
-
handle('cancel-task', async function (args) { return doCancel(rpcSessionId(args),
|
|
451
|
-
handle('update-task', async function (args) { var sid = rpcSessionId(args); var actor =
|
|
507
|
+
handle('cancel-task', async function (args) { return doCancel(rpcSessionId(args), rpcActor(args), args.taskId, args.reason) })
|
|
508
|
+
handle('update-task', async function (args) { var sid = rpcSessionId(args); var actor = rpcActor(args); if (args.hooks !== undefined && hooksDenied(actor)) return { ok: false, error: HOOKS_MAIN_ONLY }; var existsFn = existsInSession(sid); var __res = await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var __pre = null; if (args.title !== undefined) t.title = args.title; if (args.description !== undefined) t.description = args.description; if (args.priority !== undefined) t.priority = args.priority; if (args.tags !== undefined) t.tags = Array.isArray(args.tags) ? args.tags.map(function (s) { return String(s).trim() }).filter(function (s) { return s.length > 0 }) : []; if (args.assignMode !== undefined) t.assignMode = args.assignMode; if (args.assignee !== undefined) t.assignee = args.assignee || null; if (args.dependsOn !== undefined) { var derr = validateDeps(d, t.id, args.dependsOn); if (derr) return { ok: false, error: derr }; t.dependsOn = args.dependsOn } if (args.pipeline !== undefined) { t.pipeline = args.pipeline; t.pipelineAuto = false } if (args.contextFiles !== undefined) { if (!t.context) t.context = { files: [], docs: [], instructions: '', notes: '', relatedTasks: [], prerequisites: '' }; t.context.files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : [] } if (args.contextNotes !== undefined) { if (!t.context) t.context = { files: [], docs: [], instructions: '', notes: '', relatedTasks: [], prerequisites: '' }; t.context.notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : '' } if (args.publish) { if (t.status !== 'draft') return { ok: false, error: 'not a draft' }; t.status = 'pending'; ah(t, 'draft', 'pending', actor, 'published'); /* 调研门禁③:发布的是 epic(有子任务)时轻量预检子任务调研注入,缺失挂返回值由锁外 pushSysNote 汇总 */ __pre = epicPrecheck(d.tasks, t.id, existsFn) } if (args.unfreeze && t.frozen) { delete t.frozen; delete t.frozenAt; delete t.frozenBy; ah(t, t.status, t.status, actor, '解除冻结,重新进入派发池') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; delete t.retryCount; delete t.stuckSince; delete t.cancelledAt; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; if (args.hooks !== undefined) { var __he = applyHooks(t, args.hooks); if (__he.error) return { ok: false, error: __he.error } }; t.lintWarnings = draftLint(t, draftLintIo(sid, d.ownerCwd)); var __out = { ok: true, task: t }; if (__pre && __pre.missing.length) __out.epicPrecheck = __pre; return __out }); afterPublishPrecheck(sid, args.taskId, __res); return __res })
|
|
452
509
|
handle('set-board-mode', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { d.boardMode = args.mode === 'manual' ? 'manual' : 'auto'; if (d.boardMode === 'manual' && d.teamMode) { d.teamMode = false; teamModeCache[sid] = false }; return { ok: true, boardMode: d.boardMode, teamMode: !!d.teamMode, workMode: deriveWorkMode(d) } }) })
|
|
453
510
|
handle('set-team-mode', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { d.teamMode = !!args.enabled; if (d.teamMode) d.boardMode = 'auto'; teamModeCache[sid] = d.teamMode; return { ok: true, teamMode: d.teamMode, boardMode: d.boardMode, workMode: deriveWorkMode(d) } }) })
|
|
454
511
|
// 工作模式三档单入口(v75 UI 收敛):一次写入 boardMode+teamMode 两个字段,
|
|
@@ -507,7 +564,7 @@ export function createRpc(ctx, state, deps) {
|
|
|
507
564
|
return { ok: true, task: t, unfrozen: true }
|
|
508
565
|
})
|
|
509
566
|
}
|
|
510
|
-
handle('unfreeze-task', async function (args) { return doUnfreezeTask(rpcSessionId(args),
|
|
567
|
+
handle('unfreeze-task', async function (args) { return doUnfreezeTask(rpcSessionId(args), rpcActor(args), args.taskId) })
|
|
511
568
|
// 高优介入(v74):有活跃 run 则直接打进其会话;无则只记录 history(下次派发随 prompt 注入)
|
|
512
569
|
// 通道选择(v1.2.4):steer 优先——下一个 step 边界即消费;followup 要等整个 turn 结束,
|
|
513
570
|
// 长 turn 下干预形同失联(实测:Worker 单 turn 跑 10+ 分钟,「口径重写」类干预到位时活已按旧口径干完)。
|
|
@@ -640,8 +697,8 @@ export function createRpc(ctx, state, deps) {
|
|
|
640
697
|
})
|
|
641
698
|
return { ok: true }
|
|
642
699
|
}
|
|
643
|
-
handle('terminate-agent', async function (args) { return doTerminate(rpcSessionId(args),
|
|
644
|
-
handle('dismiss-suspect', async function (args) { return doDismiss(rpcSessionId(args),
|
|
700
|
+
handle('terminate-agent', async function (args) { return doTerminate(rpcSessionId(args), rpcActor(args), args.taskId) })
|
|
701
|
+
handle('dismiss-suspect', async function (args) { return doDismiss(rpcSessionId(args), rpcActor(args), args.taskId) })
|
|
645
702
|
ctx.tools.register(defineTool({ name: 'task_terminate', description: 'Team 模式:终止执行某任务的池中 Agent(卡死/跑偏时)。in-progress 任务回 pending 重派,verifying 由新 verifier 接手。', parameters: { type: 'object', properties: { taskId: { type: 'string' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; return doTerminate(toolSessionId(), getActorId(), args.taskId) } }))
|
|
646
703
|
// 手动触发单任务派发(manual 模式下"派发给 Worker"按钮,或 auto 模式手动补派)
|
|
647
704
|
// touches 文件级排他:默认尊重文件锁——候选与活动任务 touches 冲突时返回
|
|
@@ -677,8 +734,8 @@ export function createRpc(ctx, state, deps) {
|
|
|
677
734
|
if (role === 'worker') { await mutateLocked(sid, function (d) { var t2 = d.tasks.find(function (x) { return x.id === args.taskId }); if (t2 && t2.status === 'in-progress' && t2.claimedBy === 'spawn-pending') { t2.status = 'pending'; t2.claimedBy = null; t2.claimedAt = null; ah(t2, 'in-progress', 'pending', 'system', 'spawn 失败') }; return t2 }, true) }
|
|
678
735
|
return { ok: false, error: 'spawn failed' }
|
|
679
736
|
})
|
|
680
|
-
handle('resolve-escalation', async function (args) { return doResolveEscalation(rpcSessionId(args),
|
|
681
|
-
handle('intervene-agent', async function (args) { return doIntervene(rpcSessionId(args),
|
|
737
|
+
handle('resolve-escalation', async function (args) { return doResolveEscalation(rpcSessionId(args), rpcActor(args), args.taskId, args.answer, args.action) })
|
|
738
|
+
handle('intervene-agent', async function (args) { return doIntervene(rpcSessionId(args), rpcActor(args), args.taskId, args.message) })
|
|
682
739
|
// 主 Agent 工具版(Team 模式下主 Agent 通过工具裁决/介入)
|
|
683
740
|
ctx.tools.register(defineTool({ name: 'task_arbitrate', description: 'Team 模式:裁决 Worker 上报的歧义(escalation)。action=resume(默认)裁决后任务回派发池立即重派;action=hold 任务挂起冻结、不参与自动派发(留给主窗口补 dependsOn/上下文,补完后用 task_update unfreeze:true 解冻)。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, answer: { type: 'string' }, action: { type: 'string', enum: ['resume', 'hold'], description: 'resume(默认)=裁决后重新派发;hold=挂起冻结不派发' } }, required: ['taskId', 'answer'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; return doResolveEscalation(toolSessionId(), getActorId(), args.taskId, args.answer, args.action) } }))
|
|
684
741
|
ctx.tools.register(defineTool({ name: 'task_intervene', description: 'Team 模式:向执行某任务的池中 Agent 发起高优先级指令(steer 通道,当前 step 结束即响应;无活跃 run 时记录随下次派发注入)。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, message: { type: 'string' } }, required: ['taskId', 'message'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; return doIntervene(toolSessionId(), getActorId(), args.taskId, args.message) } }))
|
|
@@ -813,15 +870,15 @@ export function createRpc(ctx, state, deps) {
|
|
|
813
870
|
// 史诗拆分总开关 epicSplit(UI「功能」小节):布尔原样落盘 + **当场回填 epicSplitCache**——Team 提示词
|
|
814
871
|
// 组装是同步函数只能读缓存,不回填就得等下一次 rt() 读盘才生效(关掉后仍按旧的劝拆一轮,体感是开关没生效)。
|
|
815
872
|
// 与 feedbackEnabled 同款双写;机制面(parentId 建子卡/自动收口/hooks)不看这个键。
|
|
816
|
-
handle('set-board-config', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { if (args.key === 'maxWorkers') d.maxWorkers = Math.max(1, Math.min(10, args.value || 3)); else if (args.key === 'maxVerifiers') d.maxVerifiers = Math.max(0, Math.min(5, args.value || 0)); else if (args.key === 'workerModel') d.workerModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'verifierModel') d.verifierModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'softTimeoutMin') d.softTimeoutMin = Math.max(1, Math.min(480, Number(args.value) || 30)); else if (args.key === 'hardTimeoutMin') d.hardTimeoutMin = Math.max(1, Math.min(1440, Number(args.value) || 120)); else if (args.key === 'feedbackEnabled') { d.feedbackEnabled = !!args.value; feedbackCache[sid] = d.feedbackEnabled } else if (args.key === 'notifyDispatch') d.notifyDispatch = !!args.value; else if (args.key === 'notifyDone') d.notifyDone = !!args.value; else if (args.key === 'workerContinuable') d.workerContinuable = !!args.value; else if (args.key === 'verifyUserGuide') d.verifyUserGuide = !!args.value; else if (args.key === 'epicSplit') { d.epicSplit = !!args.value; epicSplitCache[sid] = d.epicSplit } return { ok: true } }) })
|
|
817
|
-
handle('create-task', async function (args) { var sid = rpcSessionId(args); /* 短 id 防幻影板(task-muuf0o7a):截断/不完整 sessionId 建出的板永远匹配不到活 root(每 15s 刷屏元凶),显式报错优于静默建幻影板;import- 前缀板由 isFullSessionId 放行 */ if (!isFullSessionId(sid)) return { ok: false, error: 'sessionId 不完整(疑似截断短 id),拒绝创建任务: ' + sid }; var actor =
|
|
873
|
+
handle('set-board-config', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { if (args.key === 'maxWorkers') d.maxWorkers = Math.max(1, Math.min(10, args.value || 3)); else if (args.key === 'maxVerifiers') d.maxVerifiers = Math.max(0, Math.min(5, args.value || 0)); else if (args.key === 'workerModel') d.workerModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'verifierModel') d.verifierModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'softTimeoutMin') d.softTimeoutMin = Math.max(1, Math.min(480, Number(args.value) || 30)); else if (args.key === 'hardTimeoutMin') d.hardTimeoutMin = Math.max(1, Math.min(1440, Number(args.value) || 120)); else if (args.key === 'cleanupRetentionDays') d.cleanupRetentionDays = Math.max(1, Math.min(90, Number(args.value) || 7)); else if (args.key === 'feedbackEnabled') { d.feedbackEnabled = !!args.value; feedbackCache[sid] = d.feedbackEnabled } else if (args.key === 'notifyDispatch') d.notifyDispatch = !!args.value; else if (args.key === 'notifyDone') d.notifyDone = !!args.value; else if (args.key === 'workerContinuable') d.workerContinuable = !!args.value; else if (args.key === 'verifyUserGuide') d.verifyUserGuide = !!args.value; else if (args.key === 'epicSplit') { d.epicSplit = !!args.value; epicSplitCache[sid] = d.epicSplit } return { ok: true } }) })
|
|
874
|
+
handle('create-task', async function (args) { var sid = rpcSessionId(args); /* 短 id 防幻影板(task-muuf0o7a):截断/不完整 sessionId 建出的板永远匹配不到活 root(每 15s 刷屏元凶),显式报错优于静默建幻影板;import- 前缀板由 isFullSessionId 放行 */ if (!isFullSessionId(sid)) return { ok: false, error: 'sessionId 不完整(疑似截断短 id),拒绝创建任务: ' + sid }; var actor = rpcActor(args); /* hooks 只许主窗口设置(角色门禁) */ if (args.hooks !== undefined && hooksDenied(actor)) return { ok: false, error: HOOKS_MAIN_ONLY }; return mutateLocked(sid, function (d) { var nHooks = null; if (args.hooks !== undefined) { var hn = normalizeHooks(args.hooks); if (hn.error) return { ok: false, error: hn.error }; nHooks = hn.hooks } if (args.id && d.tasks.find(function (x) { return x.id === args.id })) return { ok: false, error: 'duplicate id' }; if (args.dependsOn && args.dependsOn.length) { var derr = validateDeps(d, args.id || '(pending)', args.dependsOn); if (derr) return { ok: false, error: derr } }; var now = new Date().toISOString(); /* Team 模式护栏:draft 缺省跟随 teamMode(先补齐依赖/上下文再统一 publish);显式 draft:false 保留为立即派发的逃生门 */ var asDraft = args.draft === undefined ? !!d.teamMode : !!args.draft; var t = { id: args.id || ('task-' + Date.now().toString(36)), title: args.title || 'Untitled', description: args.description || '', status: asDraft ? 'draft' : 'pending', priority: args.priority || 'medium', tags: args.tags || [], parentId: args.parentId || null, subtaskStrategy: null, assignMode: 'auto', assignee: null, context: { files: (Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []), docs: [], instructions: args.instructions || '', notes: (typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''), relatedTasks: [], prerequisites: '' }, acceptance: args.acceptance || '', dependsOn: args.dependsOn || [], touches: normTouches(args.touches), pipeline: args.pipeline || '', claimedBy: null, claimedAt: null, createdAt: now, resolvedAt: null, verifiedAt: null, verifiedBy: null, archivedAt: null, resolution: null, waitingForTouches: null, messages: [], history: [{ from: 'created', to: asDraft ? 'draft' : 'pending', timestamp: now, actor: actor, note: asDraft ? 'created as draft' : 'created' }] }; if (nHooks && Object.keys(nHooks).length) t.hooks = nHooks; if (!t.pipeline) { t.pipeline = classifyPipeline(t); t.pipelineAuto = true }; d.tasks.push(t); t.lintWarnings = draftLint(t, draftLintIo(sid, d.ownerCwd)); /* 调研门禁 warning 族(与并行 UI 卡约定字段名 warning,core 纯函数统一口径):空描述/无调研上下文/整树 glob 三类软提示合并为一条(;分隔),不拦截创建;字段可选,老 client 无感 */ var out = withSplitHint({ ok: true, task: t }, t, cfg(d).epicSplit); var warns = createTaskWarnings(t); attachContextSuggestions(out, warns, t.touches, existsInSession(sid)); if (warns.length) out.warning = warns.join(';'); return out }) })
|
|
818
875
|
handle('list-children', async function (args) { var sid = rpcSessionId(args); var subs = ctx.subagents; if (!subs) return { ok: true, children: [] }; try { var list = await subs.listChildren(sid); var children = (list || []).map(function (c) { return { id: String(c.sessionId || c.id || ''), label: String(c.label || c.title || c.mode || '') } }).filter(function (c) { return c.id.length > 0 }); return { ok: true, children: children } } catch (e) { return { ok: true, children: [], error: String(e) } } })
|
|
819
876
|
// ===== #14 批量操作:archive(仅 resolved/cancelled)/ set-priority(全部)/ delete(真删,无 undo)=====
|
|
820
877
|
// delete op 与单任务 delete-task 走同一套 deleteGate 门禁(状态 + 未归档子任务),
|
|
821
878
|
// done/skipped 语义与 archive 完全一致:门禁不过就进 skipped(附 reasons[id] 一行原因)。
|
|
822
879
|
// 注意:批量删除是**真删**(从 tasks 数组移除),不产生 undo 快照——batch-undo 对 delete 无意义。
|
|
823
880
|
handle('batch-op', async function (args) {
|
|
824
|
-
var sid = rpcSessionId(args); var actor =
|
|
881
|
+
var sid = rpcSessionId(args); var actor = rpcActor(args)
|
|
825
882
|
var ids = Array.isArray(args.ids) ? args.ids : []
|
|
826
883
|
if (ids.length === 0) return { ok: false, error: 'no ids' }
|
|
827
884
|
return mutateLocked(sid, function (d) {
|
|
@@ -872,7 +929,7 @@ export function createRpc(ctx, state, deps) {
|
|
|
872
929
|
// 客户端也不为 delete 显示「↩️ 撤销」按钮(snapshot 只在 done>0 且 op!=='delete' 时保留)。
|
|
873
930
|
// 若老客户端硬发 op='delete' 快照进来,落到 else 分支只做 priority 回填,不会凭空复活任务。
|
|
874
931
|
handle('batch-undo', async function (args) {
|
|
875
|
-
var sid = rpcSessionId(args); var actor =
|
|
932
|
+
var sid = rpcSessionId(args); var actor = rpcActor(args)
|
|
876
933
|
var snap = args && args.snapshot
|
|
877
934
|
if (!snap || !Array.isArray(snap.items) || snap.items.length === 0) return { ok: false, error: 'no snapshot' }
|
|
878
935
|
return mutateLocked(sid, function (d) {
|
|
@@ -893,6 +950,34 @@ export function createRpc(ctx, state, deps) {
|
|
|
893
950
|
})
|
|
894
951
|
})
|
|
895
952
|
|
|
953
|
+
// ===== 已结算会话清理(task-mv2131cz):cleanup-preview / cleanup-run =====
|
|
954
|
+
// preview:dry-run,返回可删清单(N 个 / 总大小 / 按卡分组 / 跳过项与理由),只读盘估体积零副作用。
|
|
955
|
+
// run:confirm 执行真删(会话目录 + projcache json),删前再验路径逃逸;操作写 board.cleanupLog 留痕。
|
|
956
|
+
handle('cleanup-preview', async function (args) {
|
|
957
|
+
var sid = rpcSessionId(args)
|
|
958
|
+
var d = await rt(sid)
|
|
959
|
+
var view = await previewCleanup(d.tasks || [], { blockedIds: cleanupBlockedIds(sid), roots: cleanupRoots, retentionDays: cfg(d).cleanupRetentionDays, statSession: statSession })
|
|
960
|
+
var skipped = (view.skipped || []).map(function (s) { return { id: s.id, taskId: s.taskId, reason: s.reason, text: s.text || SKIP_REASON_TEXT[s.reason] || s.reason } })
|
|
961
|
+
return { ok: true, total: view.total, totalBytes: view.totalBytes, items: view.items, skipped: skipped, byTask: view.byTask }
|
|
962
|
+
})
|
|
963
|
+
handle('cleanup-run', async function (args) {
|
|
964
|
+
var sid = rpcSessionId(args)
|
|
965
|
+
if (args.confirm !== true) return { ok: false, error: '删除不可恢复,需 confirm:true 确认(token 账已入卡不受影响)' }
|
|
966
|
+
var d = await rt(sid)
|
|
967
|
+
var view = await previewCleanup(d.tasks || [], { blockedIds: cleanupBlockedIds(sid), roots: cleanupRoots, retentionDays: cfg(d).cleanupRetentionDays, statSession: statSession })
|
|
968
|
+
if (!view.items || !view.items.length) return { ok: true, deleted: 0, freedBytes: 0, note: '无可删会话' }
|
|
969
|
+
var report = await runCleanup(view.items, { roots: cleanupRoots, statSession: statSession })
|
|
970
|
+
// 留痕:board.cleanupLog 追加一条(不逐卡写 history 防刷屏;见 README「存储清理」小节)
|
|
971
|
+
await mutateLocked(sid, function (dd) {
|
|
972
|
+
if (!Array.isArray(dd.cleanupLog)) dd.cleanupLog = []
|
|
973
|
+
dd.cleanupLog.push({ at: new Date().toISOString(), by: rpcActor(args), deleted: report.deleted, freedBytes: report.freedBytes, ids: report.deletedIds })
|
|
974
|
+
if (dd.cleanupLog.length > 50) dd.cleanupLog = dd.cleanupLog.slice(-50)
|
|
975
|
+
return { ok: true }
|
|
976
|
+
}, true)
|
|
977
|
+
var skippedText = (report.skipped || []).map(function (s) { return { id: s.id, reason: s.reason, text: SKIP_REASON_TEXT[s.reason] || s.reason } })
|
|
978
|
+
return { ok: true, deleted: report.deleted, freedBytes: report.freedBytes, deletedIds: report.deletedIds, skipped: skippedText }
|
|
979
|
+
})
|
|
980
|
+
|
|
896
981
|
// ===== client ↔ host RPC:POST /dsh-agent-board { method, args } → JSON =====
|
|
897
982
|
function readBody(req, limit) {
|
|
898
983
|
return new Promise(function (resolve, reject) {
|
package/package.json
CHANGED