dsh-agent-board 1.7.0 → 1.7.1
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 +5 -2
- package/index.mjs +5 -0
- package/lib/client/board-list.js +11 -1
- package/lib/client/dashboard.js +47 -2
- package/lib/client/kernel.js +20 -7
- package/lib/client/task-detail.js +25 -0
- package/lib/client.js +103 -10
- package/lib/core.mjs +94 -1
- package/lib/dispatch.mjs +11 -1
- package/lib/rpc.mjs +27 -7
- package/lib/session.mjs +18 -2
- package/package.json +1 -1
- package/test/core.test.mjs +368 -5
package/lib/core.mjs
CHANGED
|
@@ -384,6 +384,9 @@ export function buildVerifierPrompt(t, pack) {
|
|
|
384
384
|
if (notes) p += '\n\n该任务的过程记录(歧义上报/主窗口裁决/驳回/干预,若有):\n' + notes + '\n注意:若过程记录显示主窗口已裁决改变任务方向,以裁决后的方向为验收标准。'
|
|
385
385
|
if (msgs) p += '\n\n该任务的详细消息(裁决答案/干预指令/歧义原文等):\n' + msgs
|
|
386
386
|
if (pack) p += '\n\n' + pack
|
|
387
|
+
// 跑偏归因条款(调研遵循·host 三件套 ②):驳回理由注明「立单缺调研」——归因计入驳回热点统计,
|
|
388
|
+
// 供主窗口分诊「立单缺料 vs Worker 执行问题」,缺料占高了就该把建卡调研门禁拧紧。
|
|
389
|
+
p += '\n驳回归因:若 Worker 的产出明显因缺少调研上下文而跑偏/绕路,驳回时请在驳回理由里注明「立单缺调研」(归因会计入驳回热点统计)。'
|
|
387
390
|
p += '\n\n结论契约(双模,工具优先):\n1. 优先调用 board_verdict 工具(taskId=' + t.id + ', verdict=approved/rejected, summary=测试概要, checks=逐条核对证据含行号)。\n2. 工具不可用则首行 APPROVED: <结论> 或 REJECTED: <结论>,然后 ## 测试概要 / ## 核对项 分段。'
|
|
388
391
|
return p
|
|
389
392
|
}
|
|
@@ -459,6 +462,13 @@ export function isTreeGlob(p) { var v = normTouch(p); return v === '**' || /\/\*
|
|
|
459
462
|
// ②③ 的触发前提:pipeline≠'direct' 且 touches 非空(direct 主窗口直接处理、无 touches 不指望调研材料)。
|
|
460
463
|
// 返回 string[];调用方自行合并为一条 warning 字段(可选字段,老调用方无感)。
|
|
461
464
|
export var EMPTY_DESC_WARNING = '任务描述为空——Worker 只能凭标题猜需求,建议补一句目标/约束'
|
|
465
|
+
// ②的文案单独成常量:rpc 双通道在「触发②且 touches 有可建议文件」时要认出这条并补尾巴
|
|
466
|
+
// (attachContextSuggestions),字面量若散在两处会漂移。
|
|
467
|
+
export var NO_RESEARCH_WARNING = '未附调研上下文(contextFiles/contextNotes)——Worker 将自行 grep 定位,建议补上预研文件路径或勾选无需调研'
|
|
468
|
+
// ②触发且 suggestContextFiles 非空时,warning 尾巴补的最省力动作指引(不含 ';',不破坏多 warning 合并分隔)
|
|
469
|
+
export var NO_RESEARCH_HINT = '(可直接 task_update contextFiles 补上:touches 指向的文件就是最相关的调研现场)'
|
|
470
|
+
// verify 驳回响应的重派提示(task_verify 工具 + verify-task RPC 的 rejected 分支挂载;字段可选,老调用方无感)
|
|
471
|
+
export var REJECT_REDISPATCH_HINT = '建议:驳回原因写进 description,并用 contextNotes 补调研结论后再重派——新 Worker 没有上一轮记忆'
|
|
462
472
|
export function createTaskWarnings(t) {
|
|
463
473
|
var out = []
|
|
464
474
|
if (!t || typeof t !== 'object') return out
|
|
@@ -468,12 +478,60 @@ export function createTaskWarnings(t) {
|
|
|
468
478
|
var cx = t.context || {}
|
|
469
479
|
var hasFiles = Array.isArray(cx.files) && cx.files.length > 0
|
|
470
480
|
var hasNotes = !!(cx.notes && String(cx.notes).trim())
|
|
471
|
-
if (!hasFiles && !hasNotes) out.push(
|
|
481
|
+
if (!hasFiles && !hasNotes) out.push(NO_RESEARCH_WARNING)
|
|
472
482
|
if (touches.some(isTreeGlob)) out.push('touches 含整树 glob 会串行化整个批次——修复类任务建议精确到文件级')
|
|
473
483
|
}
|
|
474
484
|
return out
|
|
475
485
|
}
|
|
476
486
|
|
|
487
|
+
// ===== touches → suggestedContextFiles 自动桥接(调研遵循·host 三件套 ①)=====
|
|
488
|
+
// 让遵守成为最省力路径:建卡触发「无调研上下文」warning 时,直接把 touches 里的具体文件
|
|
489
|
+
// 提炼成可一键采纳的 contextFiles 建议(响应挂 suggestedContextFiles 字段)。
|
|
490
|
+
// 只取「非 glob 的具体文件路径」:含 * ? [ ] { } 任一通配符的条目跳过(glob 指认不了单个现场);
|
|
491
|
+
// 锚点 :L 段剥掉(存在性是对文件而言的,建议也只到文件级)。exists(path) 由调用方注入
|
|
492
|
+
// (fs.existsSync + 会话工作区相对解析包装);缺省时退化为只做 glob/锚点过滤(与 epicPrecheck 同口径)。
|
|
493
|
+
// 去重按剥锚后的原串(不强行归一 './'——相对原样返回,调用方怎么写就怎么收)。
|
|
494
|
+
// 上限 20 条(与 contextFiles 上限一致);exists 抛异常视为不存在(建议错了比没有更糟,宁缺勿滥)。
|
|
495
|
+
export var SUGGEST_CONTEXT_MAX = 20
|
|
496
|
+
var GLOB_CHARS = /[*?\[\]{}]/
|
|
497
|
+
export function suggestContextFiles(touches, exists) {
|
|
498
|
+
var out = []
|
|
499
|
+
var seen = {}
|
|
500
|
+
var list = Array.isArray(touches) ? touches : []
|
|
501
|
+
for (var i = 0; i < list.length && out.length < SUGGEST_CONTEXT_MAX; i++) {
|
|
502
|
+
var raw = list[i]
|
|
503
|
+
if (typeof raw !== 'string' || !raw.trim()) continue
|
|
504
|
+
var file = parseAnchorPath(raw.trim()).file
|
|
505
|
+
if (!file || GLOB_CHARS.test(file)) continue
|
|
506
|
+
if (seen[file]) continue
|
|
507
|
+
if (typeof exists === 'function') {
|
|
508
|
+
var ok = false
|
|
509
|
+
try { ok = !!exists(file) } catch (_) { ok = false }
|
|
510
|
+
if (!ok) continue
|
|
511
|
+
}
|
|
512
|
+
seen[file] = true
|
|
513
|
+
out.push(file)
|
|
514
|
+
}
|
|
515
|
+
return out
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 建卡响应的建议桥接(task_create 工具 + create-task RPC 双通道同口径):
|
|
519
|
+
// warnings 里有「无调研上下文」警告且 touches 能提炼出建议时——
|
|
520
|
+
// ① 该条 warning 尾巴补 NO_RESEARCH_HINT(指认最省力动作);
|
|
521
|
+
// ② out 挂 suggestedContextFiles 字段(可选字段,老调用方无感)。
|
|
522
|
+
// 不触发/无建议 → 返回 null,out 与 warnings 形态完全不变(不改既有 warning 判定逻辑,只加建议通道)。
|
|
523
|
+
// 注意:必须在 warnings join(';') 之前调用,尾巴才会进最终 warning 文案。
|
|
524
|
+
export function attachContextSuggestions(out, warnings, touches, exists) {
|
|
525
|
+
if (!out || !Array.isArray(warnings)) return null
|
|
526
|
+
var idx = warnings.indexOf(NO_RESEARCH_WARNING)
|
|
527
|
+
if (idx < 0) return null
|
|
528
|
+
var suggested = suggestContextFiles(touches, exists)
|
|
529
|
+
if (!suggested.length) return null
|
|
530
|
+
warnings[idx] = NO_RESEARCH_WARNING + NO_RESEARCH_HINT
|
|
531
|
+
out.suggestedContextFiles = suggested
|
|
532
|
+
return suggested
|
|
533
|
+
}
|
|
534
|
+
|
|
477
535
|
// epic 发布预检:父卡 publish 时对其子任务做轻量调研注入预检。
|
|
478
536
|
// 口径:只看 pipeline≠direct 的非归档子任务;「无调研注入」= context.files/context.notes 皆空,
|
|
479
537
|
// 或 files 列了路径但全部不存在(exists 回调判定,锚点 :L 段先剥掉再查——存在性是对文件而言的)。
|
|
@@ -505,3 +563,38 @@ export function epicPrecheckNote(pre) {
|
|
|
505
563
|
var list = pre.missing.slice(0, 5).map(function (m) { return m.id + '「' + m.title + '」' + (m.reason ? '(' + m.reason + ')' : '') }).join('、')
|
|
506
564
|
return 'epic 发布预检:' + pre.total + ' 个子任务中 ' + pre.missing.length + ' 个无调研注入:' + list + (pre.missing.length > 5 ? ' 等' : '') + '——建议先补 contextFiles/contextNotes 再发布'
|
|
507
565
|
}
|
|
566
|
+
|
|
567
|
+
// ===== tasksHash:任务列表渲染的变更检测(轮询渲染节约,反馈 n-mut9rzs2mkhg)=====
|
|
568
|
+
// 背景:客户端 3s 固定全量轮询 get-tasks,任务没变也全量重渲染(实测单卡 12.7KB×20/min)。
|
|
569
|
+
// host 在 get-tasks 响应附 tasksHash,kernel 存 state.tasksHash,hash 相同则跳过
|
|
570
|
+
// state.tasks 赋值 + notify(传输仍全量,省的是渲染;短期方案)。
|
|
571
|
+
// 序列化口径(单测锁定):
|
|
572
|
+
// - 只挑「驱动列表/详情渲染」的字段:id/status/priority/title/description/parentId/tags/
|
|
573
|
+
// claimedBy/claimedAt/resolvedAt/verifiedAt/archivedAt/lastError/lastProgress.text/
|
|
574
|
+
// frozen/stuckSince/escalation.question。
|
|
575
|
+
// escalation 必须在内:新歧义要触发面板自动弹开,而 board_report escalate 只写
|
|
576
|
+
// escalation/messages/history,不动其他任何字段——漏掉它自动弹开就死了。
|
|
577
|
+
// childStats 由 tasks 现算,其输入(子任务 status/parentId)已随上述字段覆盖。
|
|
578
|
+
// - history/messages/usage/context 不参与:不驱动列表渲染,结算写账/进展消息若参与
|
|
579
|
+
// 会让 hash 频繁抖动,短路失效。
|
|
580
|
+
// - 顺序有关:按数组序拼接——看板渲染本就按数组序,顺序变化也该重渲染。
|
|
581
|
+
// - hash 用 djb2-xor(32 位无符号,base36 输出):实现极小零依赖。hash 只承担
|
|
582
|
+
// 「渲染短路」语义,不承担正确性——极低概率碰撞的最坏后果是少渲染一轮。
|
|
583
|
+
export function tasksHash(tasks) {
|
|
584
|
+
var list = Array.isArray(tasks) ? tasks : []
|
|
585
|
+
var parts = []
|
|
586
|
+
for (var i = 0; i < list.length; i++) {
|
|
587
|
+
var t = list[i] || {}
|
|
588
|
+
parts.push([
|
|
589
|
+
t.id, t.status, t.priority, t.title, t.description, t.parentId,
|
|
590
|
+
Array.isArray(t.tags) ? t.tags.join(',') : '',
|
|
591
|
+
t.claimedBy, t.claimedAt, t.resolvedAt, t.verifiedAt, t.archivedAt,
|
|
592
|
+
t.lastError, t.lastProgress && t.lastProgress.text,
|
|
593
|
+
t.frozen ? '1' : '', t.stuckSince, t.escalation && t.escalation.question
|
|
594
|
+
].join('\u0001'))
|
|
595
|
+
}
|
|
596
|
+
var s = parts.join('\u0002')
|
|
597
|
+
var h = 5381
|
|
598
|
+
for (var j = 0; j < s.length; j++) h = (((h << 5) + h) ^ s.charCodeAt(j)) >>> 0
|
|
599
|
+
return h.toString(36)
|
|
600
|
+
}
|
package/lib/dispatch.mjs
CHANGED
|
@@ -140,6 +140,8 @@ export function createDispatch(ctx, state, deps) {
|
|
|
140
140
|
|
|
141
141
|
async function spawnOneShot(sid, t, role) {
|
|
142
142
|
var subagents = ctx.subagents; if (!subagents) return null
|
|
143
|
+
// 兜底保留(真异常时有用):poolCycle 入口已有 root 存活早闸门,正常派发路径不会到达这里;
|
|
144
|
+
// 只有闸门外的直接调用(如 dispatch-task RPC 指定了无 root 的 sid)才会触发此报错。
|
|
143
145
|
var parent = rootForSession(sid); if (!parent) { console.error('[task-board] no root agent for session ' + sid + ', skip spawn'); return null }
|
|
144
146
|
var providerName = pickProvider(); if (!providerName) { console.error('[task-board] no subagent provider'); return null }
|
|
145
147
|
var modelOverride = ''
|
|
@@ -330,6 +332,14 @@ export function createDispatch(ctx, state, deps) {
|
|
|
330
332
|
|
|
331
333
|
// ===== 派发周期(15s 心跳 + 写入后 kickCycle 触发)=====
|
|
332
334
|
async function poolCycle(sid) {
|
|
335
|
+
// root 存活早闸门(根治 no-root 刷屏,task-muuf0o7a):无活 root 的会话板根本不进派发循环——
|
|
336
|
+
// 不读盘、不 claim、不回收、不写盘、零日志。幻影板(裸短 id 建的 tasks-<短id>.json,真实会话
|
|
337
|
+
// id 带 session- 前缀与后缀,rootForSession 永远匹配不到)或已关闭会话的残留板若进循环,
|
|
338
|
+
// 每 15s 心跳都会 pickDispatch→占位 claim→spawnOneShot 才发现无 root→console.error→占位
|
|
339
|
+
// 超时回收→下轮再来,永久 spam。死会话的 pending 卡等会话重开后自然恢复派发
|
|
340
|
+
// (这正是孤儿板接管语义),此处无需任何动作。
|
|
341
|
+
// 返回值说明:调用方(store.kickCycle / 15s 心跳)均忽略返回值,返回 undefined 签名兼容。
|
|
342
|
+
if (!rootForSession(sid)) return undefined
|
|
333
343
|
var info = []
|
|
334
344
|
var runs = runsFor(sid)
|
|
335
345
|
var snap = await rt(sid)
|
|
@@ -424,7 +434,7 @@ export function createDispatch(ctx, state, deps) {
|
|
|
424
434
|
// 不能走 resolveRoot:该函数在每个 agent 每次 prompt 组装时同步执行,
|
|
425
435
|
// resolveRoot 全表扫 agent 注册表会把宿主事件循环卡死(曾导致全局界面卡顿、用户消息延迟渲染)。
|
|
426
436
|
if (!teamModeCache[String(agent.id)]) return ''
|
|
427
|
-
return '【任务看板 Team 模式已开启】\n本会话的任务看板处于 Team 模式。请遵循以下工作方式:\n1. 涉及代码改动、文件创建、命令执行等实质性工作时,优先用 task_create 提交为看板任务(由一次性 Worker/Verifier 子代理执行与验收),不要自己直接动手实现。\n2. 你仍保有全部工具能力——调研、读代码、讨论方案、回答问题时直接进行,无需提交任务。\n3. 创建任务时,务必在 description 里写清任务目标和约束;调研结论/原始需求/思路用 contextNotes 带上,调研时读过的关键文件用 contextFiles
|
|
437
|
+
return '【任务看板 Team 模式已开启】\n本会话的任务看板处于 Team 模式。请遵循以下工作方式:\n1. 涉及代码改动、文件创建、命令执行等实质性工作时,优先用 task_create 提交为看板任务(由一次性 Worker/Verifier 子代理执行与验收),不要自己直接动手实现。\n2. 你仍保有全部工具能力——调研、读代码、讨论方案、回答问题时直接进行,无需提交任务。\n3. 创建任务时,务必在 description 里写清任务目标和约束;调研结论/原始需求/思路用 contextNotes 带上,调研时读过的关键文件用 contextFiles 把路径带上——两者都会通过「上下文注入」通道传给子代理(独立注入区块,不占对话流)。子代理是全新会话、无你的会话记忆,上下文不够它需要从零自行调研,效率大打折扣甚至跑偏方向——开发类任务(代码改动/修复/特性)务必带文件调研,实测可省 Worker 10~15 分钟自行 grep 定位;未带调研上下文的开发类任务返回会附 warning。\n4. Worker 上报歧义时会通过 task_arbitrate 等待你裁决,请及时响应。驳回重派时同样:新 Worker 没有上一轮的记忆,驳回原因会在 prompt 里,但额外上下文需你在 description 里补上。\n5. Team 模式下 task_create 默认建为草稿(草稿不会被派发领取)。把所有任务的 dependsOn 依赖关系、contextNotes/contextFiles 都补完后,再逐个 task_update publish=true 统一发布。确实需要立即派发的单个任务才显式传 draft:false。\n' + TEAM_SPLIT_RULE + (feedbackOn(String(agent.id)) ? '\n' + LESSON_RECALL_HINT + '把检索到的相关历史教训写进任务的 contextNotes,让子代理少踩重复的坑。' : '')
|
|
428
438
|
},
|
|
429
439
|
})
|
|
430
440
|
ctx.effect(function () { return disposeSection })
|
package/lib/rpc.mjs
CHANGED
|
@@ -12,7 +12,8 @@ import { findRunLog, readLogBytes, readLogFrames, aggregateUsageSummary } from '
|
|
|
12
12
|
import { TASK_SIZE_CONTRACT, withSplitHint, pushRejectLesson, pushArbitrationLesson } from './policy.mjs'
|
|
13
13
|
import { makeMsg } from './notify.mjs'
|
|
14
14
|
import { computeHealthHints } from './health.mjs'
|
|
15
|
-
|
|
15
|
+
import { isFullSessionId } from './session.mjs' // 幻影板防线口径(纯函数,与 policy.mjs 直引同例)
|
|
16
|
+
const { ah, isb, gsb, gpt, vt, validateDeps, classifyPipeline, cfg, claimCheck, claimApply, resolveApply, verifyApply, PRIO_RANK, touchesConflict, holdsFiles, boardHome, aggregateChildStats, createTaskWarnings, epicPrecheck, epicPrecheckNote, attachContextSuggestions, REJECT_REDISPATCH_HINT, tasksHash } = core
|
|
16
17
|
|
|
17
18
|
function defineTool(options) {
|
|
18
19
|
var userExecute = options.execute
|
|
@@ -42,6 +43,22 @@ export function createRpc(ctx, state, deps) {
|
|
|
42
43
|
var maybeNotify = deps.maybeNotify, notifyTaskDone = deps.notifyTaskDone
|
|
43
44
|
var spawnOneShot = deps.spawnOneShot, accumulateRunUsage = deps.accumulateRunUsage, readContextPack = deps.readContextPack
|
|
44
45
|
var pushSysNote = deps.pushSysNote, sessionCwd = deps.sessionCwd // 调研门禁③:epic 发布预检的投递通道与路径解析根
|
|
46
|
+
// 僵尸 epic 出清(反馈 n-mutma3mmwceq):archive 门禁的活性判定由 index.mjs 接线注入
|
|
47
|
+
// (runsFor(sid)[taskId] 存在且 !settled 即活跃)。未注入时按「有活跃 run」兜底——
|
|
48
|
+
// 保守保持旧门禁行为(in-progress 一律拒归档),绝不因缺依赖误放走在跑的任务。
|
|
49
|
+
var hasActiveRun = typeof deps.hasActiveRun === 'function' ? deps.hasActiveRun : function () { return true }
|
|
50
|
+
// archive 门禁(archive-task RPC 与 task_archive 工具同一口径,返回错误串或 null 放行):
|
|
51
|
+
// resolved / cancelled → 放行(既有行为)
|
|
52
|
+
// in-progress 且无活跃 run → 放行(parentKick 僵尸态出清:direct epic 被
|
|
53
|
+
// parentKickOnDispatch 推进到 in-progress 后子任务已归档、无 run、claimedBy=null,
|
|
54
|
+
// 此前 archive(非 resolved/cancelled)与 resolve(not claimed by you)双拒,
|
|
55
|
+
// 只能 resetToPending→delete 三段舞)
|
|
56
|
+
// in-progress 有活跃 run / 其余状态 → 拒绝
|
|
57
|
+
function archiveErr(sid, t) {
|
|
58
|
+
if (t.status === 'resolved' || t.status === 'cancelled') return null
|
|
59
|
+
if (t.status === 'in-progress' && !hasActiveRun(sid, t.id)) return null
|
|
60
|
+
return 'cannot archive'
|
|
61
|
+
}
|
|
45
62
|
// 共享状态别名(本体由 index.mjs apply 统一构建并逐模块注入)
|
|
46
63
|
var teamModeCache = state.teamModeCache
|
|
47
64
|
var feedbackCache = state.feedbackCache
|
|
@@ -69,10 +86,10 @@ export function createRpc(ctx, state, deps) {
|
|
|
69
86
|
ctx.tools.register(defineTool({ name: 'task_preview_context', description: '派发前上下文预览:预演 task_create 的 contextFiles/contextNotes 将注入给子代理的实际内容(读盘后的最终形态),用于确认材料是否足够。不创建任务。返回 ok=false/empty=true 说明无可注入内容。', parameters: { type: 'object', properties: { contextFiles: { type: 'array', items: { type: 'string' }, description: '预演的文件路径列表' }, contextNotes: { type: 'string', description: '预演的调研笔记' } }, required: [] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []; var notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''; if (!files.length && !notes.trim()) return { ok: false, error: 'contextFiles/contextNotes 至少提供一个' }; try { var pack = await readContextPack(sid, { context: { files: files, notes: notes } }); return { ok: true, empty: !pack, pack: pack } } catch (e) { return { ok: false, error: String(e).slice(0, 200) } } } }))
|
|
70
87
|
ctx.tools.register(defineTool({ name: 'task_claim', description: '领取待办任务→in-progress。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, reason: { type: 'string' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); 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, args.reason || 'claimed'); return { ok: true, task: t, context: { task: t, inheritedContext: t.context || {} } } }) } }))
|
|
71
88
|
ctx.tools.register(defineTool({ name: 'task_resolve', description: '提交验证(verifying)或阻塞(blocked)。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, status: { type: 'string', enum: ['verifying', 'blocked'] }, resolution: { type: 'string' } }, required: ['taskId', 'status'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); 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' }; if (t.claimedBy !== actor) return { ok: false, error: 'not claimed by you' }; if (args.status === 'verifying' && !args.resolution) return { ok: false, error: 'resolution required' }; return resolveApply(d, t, actor, args.status, args.resolution, args.resolution || args.status) }) } }))
|
|
72
|
-
ctx.tools.register(defineTool({ name: 'task_verify', description: '验收:approved→resolved,rejected→in-progress。子任务全完成父任务自动verifying。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, verdict: { type: 'string', enum: ['approved', 'rejected'] }, comment: { type: 'string' } }, required: ['taskId', 'verdict'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); 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' };
|
|
73
|
-
ctx.tools.register(defineTool({ name: 'task_archive', description: '
|
|
89
|
+
ctx.tools.register(defineTool({ name: 'task_verify', description: '验收:approved→resolved,rejected→in-progress。子任务全完成父任务自动verifying。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, verdict: { type: 'string', enum: ['approved', 'rejected'] }, comment: { type: 'string' } }, required: ['taskId', 'verdict'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); 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 __vr = verifyApply(d, t, actor, args.verdict, args.comment); if (args.verdict === 'rejected') __vr.hint = REJECT_REDISPATCH_HINT; return __vr }) } }))
|
|
90
|
+
ctx.tools.register(defineTool({ name: 'task_archive', description: '归档已解决/已取消任务(无活跃 run 的 in-progress 僵尸卡也可归档,用于 parentKick 僵尸态出清)。', 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: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var a = d.tasks; var t = a.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 }; var ps = t.status; t.status = 'archived'; t.archivedAt = new Date().toISOString(); ah(t, ps, 'archived', actor, 'archived'); var ca = 0; gsb(t.id, a).forEach(function (c) { if (c.status !== 'archived') { ah(c, c.status, 'archived', actor, 'cascade'); c.status = 'archived'; c.archivedAt = new Date().toISOString(); ca++ } }); var r = { ok: true, task: t }; if (ca) r.childrenArchived = ca; return r }) } }))
|
|
74
91
|
ctx.tools.register(defineTool({ name: 'task_update', description: '更新任务字段,可选重置为 pending。dependsOn/pipeline 也可更新(环检测会拒绝成环依赖)。contextFiles 可更新预研文件清单。unfreeze:true 解除裁决挂起冻结(frozen)并触发派发。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, title: { type: 'string' }, description: { type: 'string' }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, assignMode: { type: 'string', enum: ['auto', 'manual'] }, assignee: { type: 'string' }, dependsOn: { type: 'array', items: { type: 'string' } }, contextFiles: { type: 'array', items: { type: 'string' }, description: '预研文件路径(替换式更新),派发时经上下文注入传给子代理' }, contextNotes: { type: 'string', description: '预研笔记(替换式更新):调研结论/原始需求/思路' }, pipeline: { type: 'string', enum: ['full', 'work', 'direct'] }, resetToPending: { type: 'boolean' }, unfreeze: { type: 'boolean', description: '解除冻结(frozen)并立即触发派发,用于 hold 裁决补完上下文后重新入池' }, publish: { type: 'boolean', description: '发布草稿为 pending(仅 draft 状态有效)' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); 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.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; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; var __out = { ok: true, task: t }; if (__pre && __pre.missing.length) __out.epicPrecheck = __pre; return __out }); afterPublishPrecheck(sid, args.taskId, __res); return __res } }))
|
|
75
|
-
ctx.tools.register(defineTool({ name: 'task_create', description: '创建新任务到当前会话看板。acceptance 可选:硬性验收脚本命令(如 "node --test src/x.test.js"),Worker 必须实际运行、Verifier 必须独立复跑。dependsOn 可选:依赖任务 id 数组,依赖全部完成后才会被派发。pipeline 可选:full(默认,工作+验证)/work(只做不验)/direct(不进池,主窗口直接处理)。contextFiles 可选:你在调研中已经读过的关键文件路径数组——派发时 host 会从磁盘读取最新内容,通过「上下文注入」通道传给 Worker/Verifier(呈现为独立注入区块,不占对话流),避免子代理从零重复调研。contextNotes
|
|
92
|
+
ctx.tools.register(defineTool({ name: 'task_create', description: '创建新任务到当前会话看板。acceptance 可选:硬性验收脚本命令(如 "node --test src/x.test.js"),Worker 必须实际运行、Verifier 必须独立复跑。dependsOn 可选:依赖任务 id 数组,依赖全部完成后才会被派发。pipeline 可选:full(默认,工作+验证)/work(只做不验)/direct(不进池,主窗口直接处理)。contextFiles 可选:你在调研中已经读过的关键文件路径数组——派发时 host 会从磁盘读取最新内容,通过「上下文注入」通道传给 Worker/Verifier(呈现为独立注入区块,不占对话流),避免子代理从零重复调研。contextNotes 可选:调研结论/原始需求/思路等非文件类上下文,同样走上下文注入。【开发类任务(涉及代码改动/修复/特性)务必带文件调研】:把调研中读过的关键文件路径放进 contextFiles、结论思路放进 contextNotes——实测可让 Worker 省去 10~15 分钟从零 grep 定位的时间,且方向感天壤之别(无调研的 Worker 只能靠标题猜需求、极易跑偏)。调研门禁:pipeline=full/work 且 touches 非空而未带调研上下文时,返回将附 warning 提示。touches 可选:本任务将要改动的文件路径/glob 数组(如 "src/x.mjs"、"src/**")——派发器在同一时刻只派发 touches 不冲突的任务,避免并行 Worker 改同一批文件互踩;任务进行中持有文件锁,提交验收/完成后释放。' + TASK_SIZE_CONTRACT, parameters: { type: 'object', properties: { id: { type: 'string' }, title: { type: 'string' }, description: { type: 'string' }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, tags: { type: 'array', items: { type: 'string' } }, parentId: { type: 'string' }, instructions: { type: 'string' }, acceptance: { type: 'string' }, dependsOn: { type: 'array', items: { type: 'string' } }, contextFiles: { type: 'array', items: { type: 'string' }, description: '预研文件路径(相对 workspace 或绝对路径),内容将在派发时经上下文注入传给子代理' }, contextNotes: { type: 'string', description: '预研笔记:调研结论/原始需求/思路等(≤8000 字符),经上下文注入传给子代理' }, pipeline: { type: 'string', enum: ['full', 'work', 'direct'] }, touches: { type: 'array', items: { type: 'string' }, description: '本任务将改动的文件路径/glob(文件级排他锁):与活动任务 touches 冲突时不派发,等锁释放;支持 "src/**" 目录、"./a/b.mjs"、裸文件名等写法' }, draft: { type: 'boolean', description: '创建为草稿(不派发)。Team 模式下缺省即为 true——补全 dependsOn/上下文后用 task_update publish=true 统一发布;非 Team 模式缺省 false,显式 draft:false 可跳过草稿' } }, required: ['title'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { if (args.id && d.tasks.find(function (x) { return x.id === args.id })) return { ok: false, error: 'duplicate id: ' + args.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, 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 (!t.pipeline) t.pipeline = classifyPipeline(t); t.pipelineAuto = !args.pipeline; d.tasks.push(t); /* 调研门禁 warning 族(与 create-task RPC 同口径,core 纯函数):空描述/无调研上下文/整树 glob 三类软提示合并为一条(;分隔),不拦截创建 */ var __out = withSplitHint({ ok: true, task: t }, t); var __warns = createTaskWarnings(t); attachContextSuggestions(__out, __warns, t.touches, existsInSession(sid)); if (__warns.length) __out.warning = __warns.join(';'); return __out }) } }))
|
|
76
93
|
|
|
77
94
|
// ===== RPC =====
|
|
78
95
|
// get-tasks 是纯读路径(rt 只读文件)——poolCycle 由 15s 心跳 + 写入后 kickCycle 驱动,
|
|
@@ -92,6 +109,9 @@ export function createRpc(ctx, state, deps) {
|
|
|
92
109
|
// 史诗父卡语义层:childStats 现算(零存储)——{ <parentId>: { total, resolved, active, activeTitle } },
|
|
93
110
|
// 父卡列位置/进度展示的数据源;无非归档子任务的父卡不出键
|
|
94
111
|
d.childStats = aggregateChildStats(d.tasks)
|
|
112
|
+
// 渲染变更检测(反馈 n-mut9rzs2mkhg):tasks 关键字段的稳定 hash(纯函数,口径见 core.tasksHash),
|
|
113
|
+
// 客户端 3s 轮询 hash 相同则跳过 state.tasks 赋值 + notify——传输仍全量,省的是渲染
|
|
114
|
+
d.tasksHash = tasksHash(d.tasks)
|
|
95
115
|
return d })
|
|
96
116
|
// 派发前上下文预览:主 agent 用它确认"我将注入给子代理的材料"是否足够(不发任务、不落盘)
|
|
97
117
|
handle('preview-context', async function (args) {
|
|
@@ -170,8 +190,8 @@ export function createRpc(ctx, state, deps) {
|
|
|
170
190
|
})
|
|
171
191
|
handle('claim-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); 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 } }) })
|
|
172
192
|
handle('resolve-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); 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) }) })
|
|
173
|
-
handle('verify-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); 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' }; delete t.escalation; delete t.verifyRetries; var r = verifyApply(d, t, actor, args.verdict, args.comment); if (args.verdict === 'rejected') pushRejectLesson(d, t, args.comment); return r }) })
|
|
174
|
-
handle('archive-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { var a = d.tasks; var t = a.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' };
|
|
193
|
+
handle('verify-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); 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' }; delete t.escalation; delete t.verifyRetries; var r = verifyApply(d, t, actor, args.verdict, args.comment); if (args.verdict === 'rejected') { pushRejectLesson(d, t, args.comment); r.hint = REJECT_REDISPATCH_HINT } return r }) })
|
|
194
|
+
handle('archive-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { var a = d.tasks; var t = a.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 }; var ps = t.status; t.status = 'archived'; t.archivedAt = new Date().toISOString(); ah(t, ps, 'archived', actor, 'manual archive'); var ca = 0; gsb(t.id, a).forEach(function (c) { if (c.status !== 'archived') { ah(c, c.status, 'archived', actor, 'cascade'); c.status = 'archived'; c.archivedAt = new Date().toISOString(); ca++ } }); var r = { ok: true, task: t }; if (ca) r.childrenArchived = ca; return r }) })
|
|
175
195
|
// ===== 任务删除通道(真删,无 undo)=====
|
|
176
196
|
// 背景:archive-task 只收 resolved/cancelled,草稿/误建卡片此前没有任何下线通道(只能永远挂着)。
|
|
177
197
|
// 状态门禁(delete-task 与 batch-op delete 共用本函数,保证两条入口语义完全一致):
|
|
@@ -460,7 +480,7 @@ export function createRpc(ctx, state, deps) {
|
|
|
460
480
|
return { ok: true, delivered: delivered }
|
|
461
481
|
})
|
|
462
482
|
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 } return { ok: true } }) })
|
|
463
|
-
handle('create-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { 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 (!t.pipeline) { t.pipeline = classifyPipeline(t); t.pipelineAuto = true }; d.tasks.push(t); /* 调研门禁 warning 族(与并行 UI 卡约定字段名 warning,core 纯函数统一口径):空描述/无调研上下文/整树 glob 三类软提示合并为一条(;分隔),不拦截创建;字段可选,老 client 无感 */ var out = withSplitHint({ ok: true, task: t }, t); var warns = createTaskWarnings(t); if (warns.length) out.warning = warns.join(';'); return out }) })
|
|
483
|
+
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 = getActorId(); return mutateLocked(sid, function (d) { 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 (!t.pipeline) { t.pipeline = classifyPipeline(t); t.pipelineAuto = true }; d.tasks.push(t); /* 调研门禁 warning 族(与并行 UI 卡约定字段名 warning,core 纯函数统一口径):空描述/无调研上下文/整树 glob 三类软提示合并为一条(;分隔),不拦截创建;字段可选,老 client 无感 */ var out = withSplitHint({ ok: true, task: t }, t); var warns = createTaskWarnings(t); attachContextSuggestions(out, warns, t.touches, existsInSession(sid)); if (warns.length) out.warning = warns.join(';'); return out }) })
|
|
464
484
|
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) } } })
|
|
465
485
|
// ===== #14 批量操作:archive(仅 resolved/cancelled)/ set-priority(全部)/ delete(真删,无 undo)=====
|
|
466
486
|
// delete op 与单任务 delete-task 走同一套 deleteGate 门禁(状态 + 未归档子任务),
|
package/lib/session.mjs
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
// createSession(ctx, state):agent 归属解析(resolveRoot 10s 缓存)/ 工具与 RPC 的会话 id 归一 /
|
|
3
3
|
// 已知会话心跳集合 / 工作模式三档派生 / feedback 开关读取 / 按会话找 root agent / 会话工作区 /
|
|
4
4
|
// 通用超时包装 / 活跃 run 表与派发履历访问。共享状态全部经 state 显式注入(别名为本地 var)。
|
|
5
|
+
// 另出口纯函数 isFullSessionId(幻影板防线口径,rpc.mjs 直接 import 复用,与 policy.mjs 纯函数同例)。
|
|
6
|
+
|
|
7
|
+
// 完整会话 id 结构判定(防幻影板,task-muuf0o7a):真实 root 会话 id 形如
|
|
8
|
+
// session-cc24eb5c-702c-...(长度 ≥20 且至少两个 '-');裸短 id(如 cc24eb5c——某次调用
|
|
9
|
+
// 传了截断短 id)不具备完整结构,用它建出的 tasks-<短id>.json 是幻影板:poolCycle 对它
|
|
10
|
+
// pickDispatch 成功却永远匹配不到活 root(真实 id 带 session- 前缀与后缀),每 15s 心跳
|
|
11
|
+
// console.error 刷屏。import- 前缀板(import-sess_df0837fa-...)是合法的导入会话板,放行。
|
|
12
|
+
export function isFullSessionId(sid) {
|
|
13
|
+
if (!sid || typeof sid !== 'string') return false
|
|
14
|
+
if (sid.indexOf('import-') === 0) return true // import- 前缀板合法,结构判断放行
|
|
15
|
+
if (sid.length < 20) return false
|
|
16
|
+
var first = sid.indexOf('-')
|
|
17
|
+
return first > 0 && sid.indexOf('-', first + 1) > first
|
|
18
|
+
}
|
|
5
19
|
|
|
6
20
|
export function createSession(ctx, state) {
|
|
7
21
|
// 共享状态别名(本体由 index.mjs apply 统一构建并逐模块注入)
|
|
@@ -32,7 +46,9 @@ export function createSession(ctx, state) {
|
|
|
32
46
|
function rpcSessionId(args) { var sid = (args && typeof args.sessionId === 'string' && args.sessionId.length > 0) ? args.sessionId : resolveRoot(getActorId()); touchSession(sid); return sid }
|
|
33
47
|
// 已知会话集合:心跳驱动这些会话的 poolCycle(摆脱对客户端轮询的依赖)
|
|
34
48
|
// 带 TTL 淘汰:>30 分钟无活跃的会话从心跳中移除,避免长期运行后空转 poolCycle
|
|
35
|
-
|
|
49
|
+
// 不完整短 id 不注册(幻影板防线 task-muuf0o7a):短 id 板永远匹配不到活 root,
|
|
50
|
+
// 进心跳集合只会让 poolCycle 每 15s 空转刷屏——在注册口直接拦住。
|
|
51
|
+
function touchSession(sid) { if (sid && typeof sid === 'string' && sid !== 'unknown' && isFullSessionId(sid)) knownSessions[sid] = Date.now() }
|
|
36
52
|
// teamMode 缓存:由 rt() 同步,供 systemPrompt 动态引导段读取(v65)
|
|
37
53
|
// feedbackEnabled 缓存(学习飞轮 v1):同样由 rt() 同步——systemPrompt 的组装是同步函数,
|
|
38
54
|
// 不能在里面读文件。与 teamModeCache 同源同生命周期(引导段本来就要 teamMode 命中才渲染)。
|
|
@@ -56,5 +72,5 @@ export function createSession(ctx, state) {
|
|
|
56
72
|
function runsFor(sid) { if (!activeRuns[sid]) activeRuns[sid] = {}; return activeRuns[sid] }
|
|
57
73
|
function isDispatched(sid, id) { return !!(id && dispatchedEver[sid] && dispatchedEver[sid][id]) }
|
|
58
74
|
|
|
59
|
-
return { getActorId: getActorId, resolveRoot: resolveRoot, toolSessionId: toolSessionId, rpcSessionId: rpcSessionId, touchSession: touchSession, rootForSession: rootForSession, sessionCwd: sessionCwd, deriveWorkMode: deriveWorkMode, feedbackOn: feedbackOn, withTimeout: withTimeout, runsFor: runsFor, isDispatched: isDispatched }
|
|
75
|
+
return { getActorId: getActorId, resolveRoot: resolveRoot, toolSessionId: toolSessionId, rpcSessionId: rpcSessionId, touchSession: touchSession, rootForSession: rootForSession, sessionCwd: sessionCwd, deriveWorkMode: deriveWorkMode, feedbackOn: feedbackOn, withTimeout: withTimeout, runsFor: runsFor, isDispatched: isDispatched, isFullSessionId: isFullSessionId }
|
|
60
76
|
}
|
package/package.json
CHANGED