dsh-agent-board 1.3.1 → 1.5.0
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 +43 -14
- package/index.mjs +450 -84
- package/lib/client.js +261 -24
- package/lib/core.mjs +158 -11
- package/package.json +1 -1
- package/test/core.test.mjs +379 -0
package/index.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { zstdDecompressSync } from 'node:zlib'
|
|
|
10
10
|
import os from 'node:os'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import fsNode from 'node:fs'
|
|
13
|
-
const { ah, isb, gsb, gpt, vt, validateDeps, depsSatisfied, depsCancelled, classifyPipeline, seed, normalizeBoard, cfg, claimCheck, claimApply, checkParentAuto, resolveApply, verifyApply, parseSections, parseVerdict, isEscalation, outputText, histNotes, buildContextPackSection, buildWorkerPrompt, buildVerifierPrompt, pickDispatch, isOrphan, PRIO_RANK } = core
|
|
13
|
+
const { ah, isb, gsb, gpt, vt, validateDeps, depsSatisfied, depsCancelled, classifyPipeline, seed, normalizeBoard, cfg, claimCheck, claimApply, checkParentAuto, resolveApply, verifyApply, parseSections, parseVerdict, isEscalation, outputText, histNotes, buildContextPackSection, buildWorkerPrompt, buildVerifierPrompt, pickDispatch, isOrphan, PRIO_RANK, patOverlap, overlapsTouches, touchesConflict, holdsFiles, lessonText, pushLesson, LESSON_RECALL_HINT } = core
|
|
14
14
|
|
|
15
15
|
function defineTool(options) {
|
|
16
16
|
var userExecute = options.execute
|
|
@@ -27,6 +27,161 @@ function defineTool(options) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// ===== 任务粒度治理(全软方案:只引导/提示,绝不阻断创建与派发)=====
|
|
31
|
+
// 背景:agent 容易把史诗级大任务整坨塞进看板(长程 + 跑偏风险 + 返工成本高)。
|
|
32
|
+
// 落地三件套(都不带硬闸门):
|
|
33
|
+
// 1) task_create 工具描述里的粒度契约 TASK_SIZE_CONTRACT;
|
|
34
|
+
// 2) Team 模式系统提示词第 6 条 TEAM_SPLIT_RULE(epic 父卡 + 子任务 + checkParentAuto 收尾);
|
|
35
|
+
// 3) create-task RPC / task_create 工具返回体命中时附加 suggestSplit 一行建议(不落盘、不改状态)。
|
|
36
|
+
export var TASK_SIZE_CONTRACT = '建议粒度:单任务 10~30 分钟可独立完成。超出此范围的大任务请先拆分——建一张 epic 卡(parentId 体系)再挂子任务,别整坨塞进来。'
|
|
37
|
+
export var SUGGEST_SPLIT_TEXT = '任务看起来偏大(建议单任务 10~30 分钟):考虑拆分子任务(parentId)或收窄边界'
|
|
38
|
+
// Team 模式提示词第 6 条:大任务的 epic 拆分流程(父卡 pipeline=direct 不派发,子任务全 resolved 后父卡由 checkParentAuto 自动转 verifying)
|
|
39
|
+
export var TEAM_SPLIT_RULE = '6. 大任务必须拆分:预计超过 30 分钟的任务,先建一张 epic 父卡(pipeline 传 direct,不派发),再拆成若干 10~30 分钟的子任务(task_create 传 parentId=父卡 id,有先后顺序的用 dependsOn 串联)。子任务全部完成后父卡会自动标记完成(checkParentAuto)。'
|
|
40
|
+
// 史诗特征词:命中即视为"整坨塞进来"的典型信号(与 TASK_SIZE_CONTRACT 配套;无 /g,可安全复用)
|
|
41
|
+
var EPIC_WORDS = /全量|整体|系统级|全面|重构|所有模块|整个/
|
|
42
|
+
// description 长度阈值(字符):超过它说明描述密度远超"10~30 分钟单任务"应有体量
|
|
43
|
+
var SPLIT_DESC_LIMIT = 500
|
|
44
|
+
// 软闸门判定:description > 500 字符,或 title+description 命中史诗特征词 → 返回建议文案;否则返回 ''
|
|
45
|
+
// 纯函数、无 IO、不改任务字段——调用方拿到空串即视为粒度正常(falsy 判断即可)
|
|
46
|
+
export function suggestSplitOf(t) {
|
|
47
|
+
var desc = String((t && t.description) || '')
|
|
48
|
+
var text = String((t && t.title) || '') + ' ' + desc
|
|
49
|
+
if (desc.length > SPLIT_DESC_LIMIT || EPIC_WORDS.test(text)) return SUGGEST_SPLIT_TEXT
|
|
50
|
+
return ''
|
|
51
|
+
}
|
|
52
|
+
// 统一出口:只在命中时附加字段,老调用方拿到的返回体形态完全不变(多一个可选字段而已)
|
|
53
|
+
export function withSplitHint(out, t) {
|
|
54
|
+
var hint = suggestSplitOf(t)
|
|
55
|
+
if (hint && out && typeof out === 'object') out.suggestSplit = hint
|
|
56
|
+
return out
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ===== Token 消耗统计(v4 会话日志 usage 聚合)=====
|
|
60
|
+
// 数据源:Worker/Verifier 一次性子会话的 v4 追加写日志
|
|
61
|
+
// ~/.dsh/sessions/<bucket>/<childSessionId>/session.v4.jsonl.zstd
|
|
62
|
+
// 字段形状(用本机真实日志逐帧核对后确认,2026-10):
|
|
63
|
+
// type === 'assistant/message' 的事件带 data.usage =
|
|
64
|
+
// { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, totalTokens }
|
|
65
|
+
// 模型名不在 assistant/message 上,而在同会话的 request/context(data.model,
|
|
66
|
+
// 与 request/header 的 data.header.config.model 同源)——模型小计以日志记录的为准。
|
|
67
|
+
// 定位/分帧逻辑与 agent-activity RPC 共用(同一套日志名探测 + zstd 帧头切分),
|
|
68
|
+
// 区别只是 usage 要扫全部帧、activity 只看最新几帧。
|
|
69
|
+
// 约束:只做展示、不做计费断言;读不到日志/无 usage 一律返回 null(调用方降级为「暂无数据」),绝不抛错。
|
|
70
|
+
var USAGE_LOG_NAMES = ['session.v4.jsonl.zstd', 'session.v3.jsonl.zstd', 'session.v2.jsonl.zstd', 'session.jsonl.zstd']
|
|
71
|
+
var ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
72
|
+
|
|
73
|
+
// 定位某个 run(子会话 id)的日志文件:bucket × 版本名逐个探测(新版本名优先);找不到/不可读返回 null
|
|
74
|
+
export function findRunLog(runId, sessionsRoot) {
|
|
75
|
+
if (!runId) return null
|
|
76
|
+
try {
|
|
77
|
+
var root = sessionsRoot || path.join(os.homedir(), '.dsh', 'sessions')
|
|
78
|
+
var buckets = fsNode.readdirSync(root)
|
|
79
|
+
for (var bi = 0; bi < buckets.length; bi++) {
|
|
80
|
+
for (var ni = 0; ni < USAGE_LOG_NAMES.length; ni++) {
|
|
81
|
+
var cand = path.join(root, buckets[bi], String(runId), USAGE_LOG_NAMES[ni])
|
|
82
|
+
if (fsNode.existsSync(cand)) return cand
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch (_) {}
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 读日志字节:tailBytes > 0 时只读末尾这么多字节(activity 热路径用),否则读整份(usage 结算用)。
|
|
90
|
+
// 尾部截断可能切在帧中间,交给 readLogFrames 逐帧 try/catch 降级。
|
|
91
|
+
function readLogBytes(logPath, tailBytes) {
|
|
92
|
+
try {
|
|
93
|
+
var sz = fsNode.statSync(logPath).size
|
|
94
|
+
var start = (tailBytes > 0 && sz > tailBytes) ? sz - tailBytes : 0
|
|
95
|
+
var buf = Buffer.alloc(sz - start)
|
|
96
|
+
var fd = fsNode.openSync(logPath, 'r')
|
|
97
|
+
try { fsNode.readSync(fd, buf, 0, buf.length, start) } finally { fsNode.closeSync(fd) }
|
|
98
|
+
return buf
|
|
99
|
+
} catch (_) { return null }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 追加写多帧格式:按帧头 MAGIC 切段、逐帧解压、逐行 JSON.parse。
|
|
103
|
+
// 返回帧数组(从新到旧),每帧是事件数组(保持帧内原序);limit > 0 只取最新 limit 帧。
|
|
104
|
+
// 坏帧/半帧(强杀截断、MAGIC 假命中、尾部切在压缩流中间)整帧跳过,绝不抛错。
|
|
105
|
+
function readLogFrames(buf, limit) {
|
|
106
|
+
var out = []
|
|
107
|
+
if (!buf || !buf.length) return out
|
|
108
|
+
var offs = []
|
|
109
|
+
var at = buf.indexOf(ZSTD_MAGIC)
|
|
110
|
+
while (at >= 0) { offs.push(at); at = buf.indexOf(ZSTD_MAGIC, at + 1) }
|
|
111
|
+
for (var k = offs.length - 1; k >= 0; k--) {
|
|
112
|
+
if (limit > 0 && out.length >= limit) break
|
|
113
|
+
var end = k + 1 < offs.length ? offs[k + 1] : buf.length
|
|
114
|
+
var evs = []
|
|
115
|
+
try {
|
|
116
|
+
var text = zstdDecompressSync(buf.subarray(offs[k], end)).toString('utf8')
|
|
117
|
+
var lines = text.split('\n')
|
|
118
|
+
for (var i = 0; i < lines.length; i++) {
|
|
119
|
+
if (!lines[i]) continue
|
|
120
|
+
try { evs.push(JSON.parse(lines[i])) } catch (_) {}
|
|
121
|
+
}
|
|
122
|
+
} catch (_) { continue }
|
|
123
|
+
out.push(evs)
|
|
124
|
+
}
|
|
125
|
+
return out
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 聚合一次 run 的全部 token 消耗:{input, output, cacheRead, cacheWrite, total, model}。
|
|
129
|
+
// 逐帧扫 assistant/message 的 data.usage 累加(无 usage 的事件跳过、没有 usage 事件返回 null)。
|
|
130
|
+
// total 优先取日志自带的 totalTokens,缺失时才用 输入+输出+缓存读+缓存写 兜底。
|
|
131
|
+
export function readRunUsage(runId, sessionsRoot) {
|
|
132
|
+
try {
|
|
133
|
+
var log = findRunLog(runId, sessionsRoot)
|
|
134
|
+
if (!log) return null
|
|
135
|
+
// 整份读取:usage 必须全量累加(单次 run 日志量级 MB,结算时只读一次)。
|
|
136
|
+
// 极端超大日志封顶 64MB——只丢最老的历史帧,好过结算路径被一次同步 IO 拖住。
|
|
137
|
+
var buf = readLogBytes(log, 64 * 1024 * 1024)
|
|
138
|
+
if (!buf) return null
|
|
139
|
+
var frames = readLogFrames(buf, 0)
|
|
140
|
+
var out = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, model: '' }
|
|
141
|
+
var hasUsage = false
|
|
142
|
+
for (var fi = 0; fi < frames.length; fi++) {
|
|
143
|
+
var evs = frames[fi]
|
|
144
|
+
for (var i = 0; i < evs.length; i++) {
|
|
145
|
+
var e = evs[i]; var dta = (e && e.data) || {}
|
|
146
|
+
if (e && e.type === 'request/context' && dta.model && !out.model) out.model = String(dta.model)
|
|
147
|
+
if (!e || e.type !== 'assistant/message') continue
|
|
148
|
+
var u = dta.usage
|
|
149
|
+
if (!u) continue
|
|
150
|
+
var inp = numOr0(u.inputTokens), outp = numOr0(u.outputTokens)
|
|
151
|
+
var cr = numOr0(u.cacheReadTokens), cw = numOr0(u.cacheWriteTokens)
|
|
152
|
+
out.input += inp; out.output += outp; out.cacheRead += cr; out.cacheWrite += cw
|
|
153
|
+
out.total += numOr0(u.totalTokens) || (inp + outp + cr + cw)
|
|
154
|
+
hasUsage = true
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return hasUsage ? out : null
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// 日志解析失败一律降级:usage 只是展示统计,绝不影响结算与状态流转
|
|
160
|
+
console.error('[task-board] readRunUsage failed (' + runId + '):', String(e))
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function numOr0(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
|
|
165
|
+
|
|
166
|
+
// board 级聚合(get-tasks 现算,不落盘额外表):总量 + 输入/输出/缓存读拆分 + 按模型小计 + 任务 Top8。
|
|
167
|
+
// 归档任务同样计入(它们确实消耗过 token);无 usage 的任务跳过。
|
|
168
|
+
export function aggregateUsageSummary(tasks) {
|
|
169
|
+
var s = { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, byModel: {}, topTasks: [] }
|
|
170
|
+
var list = Array.isArray(tasks) ? tasks : []
|
|
171
|
+
for (var i = 0; i < list.length; i++) {
|
|
172
|
+
var t = list[i]; var u = t && t.usage
|
|
173
|
+
if (!u) continue
|
|
174
|
+
s.total += u.total || 0; s.input += u.input || 0; s.output += u.output || 0
|
|
175
|
+
s.cacheRead += u.cacheRead || 0; s.cacheWrite += u.cacheWrite || 0
|
|
176
|
+
var ms = u.models || {}
|
|
177
|
+
for (var mk in ms) { if (Object.prototype.hasOwnProperty.call(ms, mk)) s.byModel[mk] = (s.byModel[mk] || 0) + (ms[mk] || 0) }
|
|
178
|
+
if (u.total) s.topTasks.push({ id: t.id, title: t.title, total: u.total, runs: u.runs || 0 })
|
|
179
|
+
}
|
|
180
|
+
s.topTasks.sort(function (a, b) { return b.total - a.total })
|
|
181
|
+
s.topTasks = s.topTasks.slice(0, 8)
|
|
182
|
+
return s
|
|
183
|
+
}
|
|
184
|
+
|
|
30
185
|
export const name = 'dsh-agent-board'
|
|
31
186
|
export const inject = ['fs', 'timer', 'subagents', 'agents', 'tools', 'webServer']
|
|
32
187
|
|
|
@@ -62,6 +217,16 @@ export function apply(ctx) {
|
|
|
62
217
|
function touchSession(sid) { if (sid && typeof sid === 'string' && sid !== 'unknown') knownSessions[sid] = Date.now() }
|
|
63
218
|
// teamMode 缓存:由 rt() 同步,供 systemPrompt 动态引导段读取(v65)
|
|
64
219
|
var teamModeCache = {}
|
|
220
|
+
// feedbackEnabled 缓存(学习飞轮 v1):同样由 rt() 同步——systemPrompt 的组装是同步函数,
|
|
221
|
+
// 不能在里面读文件。与 teamModeCache 同源同生命周期(引导段本来就要 teamMode 命中才渲染)。
|
|
222
|
+
var feedbackCache = {}
|
|
223
|
+
function feedbackOn(sid) { return feedbackCache[sid] !== false }
|
|
224
|
+
// 工作模式三档收敛(UI 一维化):内部仍存 boardMode+teamMode 两个 flag(老数据/老 RPC 无损),
|
|
225
|
+
// workMode 是纯派生字段——由两 flag 算出,不落盘(normalizeBoard 无需改动)。
|
|
226
|
+
// 'team' → boardMode=auto + teamMode=true(主窗口当调度员:默认草稿 + 裁决歧义)
|
|
227
|
+
// 'auto' → boardMode=auto + teamMode=false(即建即派给 Worker)
|
|
228
|
+
// 'list' → boardMode=manual + teamMode=false(看板=TODO 列表,手动 claim 或逐张派发)
|
|
229
|
+
function deriveWorkMode(d) { return (d && d.teamMode) ? 'team' : ((d && d.boardMode) === 'auto' ? 'auto' : 'list') }
|
|
65
230
|
// 看板文件用「家目录绝对路径 + node:fs」直读写——不再走 fs 服务的相对路径解析
|
|
66
231
|
// (fs 服务的相对路径解析根 = 进程启动 cwd,cwd 一变所有看板静默读成空板;
|
|
67
232
|
// 曾因此导致整个看板状态异常。绝对路径对此永久免疫)
|
|
@@ -72,7 +237,7 @@ export function apply(ctx) {
|
|
|
72
237
|
try { r = await fsNode.promises.readFile(boardPath(sid), 'utf8') } catch (_) { return seed(sid) } // 不存在/不可读 → 空板
|
|
73
238
|
try {
|
|
74
239
|
var d = JSON.parse(r)
|
|
75
|
-
if (vt(d) && d.ownerSession === sid) { teamModeCache[sid] = !!d.teamMode;
|
|
240
|
+
if (vt(d) && d.ownerSession === sid) { teamModeCache[sid] = !!d.teamMode; var nd = normalizeBoard(d); feedbackCache[sid] = nd.feedbackEnabled !== false; return nd }
|
|
76
241
|
return seed(sid)
|
|
77
242
|
} catch (_) {
|
|
78
243
|
// JSON 截断/损坏(如强杀打断写盘):隔离留档再种新板——数据不丢,坏文件也不反复 poison
|
|
@@ -112,6 +277,9 @@ export function apply(ctx) {
|
|
|
112
277
|
function kickCycle(sid) { if (cyclePending[sid]) return; cyclePending[sid] = true; var tm = ctx.timer; var go = function () { cyclePending[sid] = false; poolCycle(sid).catch(function () {}) }; if (tm) tm.timeout(50).then(go); else Promise.resolve().then(go) }
|
|
113
278
|
function mutateLocked(sid, mutate, skipKick) { return withLock(sid, async function () { var d = await rt(sid); var r = await mutate(d); if (r !== null && r !== undefined) { await wt(sid, d); if (!skipKick) kickCycle(sid); return r } return r }) }
|
|
114
279
|
function jo() { return { schema: { type: 'object', additionalProperties: true }, render: function (a, v) { return [{ type: 'text', text: JSON.stringify(v, null, 2) }] } } }
|
|
280
|
+
// touches(文件级排他声明)归一化:非字符串项过滤掉,空数组 = 不声明(无锁语义)。
|
|
281
|
+
// 上限 20 条,与 contextFiles 上限一致,防止 prompt/看板文件被超长清单撑爆。
|
|
282
|
+
function normTouches(v) { return Array.isArray(v) ? v.filter(function (x) { return typeof x === 'string' && x.trim() }).slice(0, 20).map(function (x) { return x.trim() }) : [] }
|
|
115
283
|
// 按会话找 root agent(静态插件挂 host 层后多会话共存,不能"取第一个"——会把 worker 挂到别的会话上)
|
|
116
284
|
function rootForSession(sid) { var s = ctx.agents; if (!s) return undefined; var r = s.roots(); for (var i = 0; i < r.length; i++) { if (String(r[i].id) === sid) return r[i] } return undefined }
|
|
117
285
|
function makeSignal() { try { return new AbortController().signal } catch (_) { return { aborted: false, addEventListener: function () {}, removeEventListener: function () {} } } }
|
|
@@ -127,6 +295,21 @@ export function apply(ctx) {
|
|
|
127
295
|
// 超时保护:run 挂死时走失败重试路径
|
|
128
296
|
function withTimeout(promise, ms, label) { var timer = ctx.timer; if (!timer) return promise; return Promise.race([promise, timer.timeout(ms).then(function () { throw new Error(label + ' timeout ' + ms + 'ms') })]) }
|
|
129
297
|
|
|
298
|
+
// ===== 学习飞轮 v1:候选教训信号(只产信号,不做存储)=====
|
|
299
|
+
// 看板在这里只做一件事:把「Verifier 驳回 / 主窗口仲裁结论」变成一条结构化的候选教训,
|
|
300
|
+
// 落进 t.messages(kind='lesson-candidate':详情页可见 + 带「沉淀」按钮)。
|
|
301
|
+
// 零耦合红线:不调用任何笔记/记忆工具的 API、不写任何外部文件——教训最终存到哪、要不要存,
|
|
302
|
+
// 全由主窗口 agent 自己用可用工具决定。触发前一律先过 feedbackEnabled 总开关(关掉 = 不生成)。
|
|
303
|
+
function lessonSource(t) { return '任务 ' + t.id + '「' + String(t.title || '').slice(0, 60) + '」 · ' + new Date().toISOString() }
|
|
304
|
+
function pushRejectLesson(d, t, reason, at) {
|
|
305
|
+
if (!cfg(d).feedbackEnabled) return false
|
|
306
|
+
return pushLesson(t, lessonText('任务「' + String(t.title || '') + '」(' + t.id + ') 被 Verifier 驳回', [['错误做法', String(reason || '(Verifier 未给出理由)')]], lessonSource(t)), at, 'system')
|
|
307
|
+
}
|
|
308
|
+
function pushArbitrationLesson(d, t, question, answer, at) {
|
|
309
|
+
if (!cfg(d).feedbackEnabled) return false
|
|
310
|
+
return pushLesson(t, lessonText('任务「' + String(t.title || '') + '」(' + t.id + ') 的歧义裁决', [['疑问', question], ['裁决结论', answer]], lessonSource(t)), at, 'system')
|
|
311
|
+
}
|
|
312
|
+
|
|
130
313
|
|
|
131
314
|
// ===== 状态流转(已抽取到 lib/core.mjs)=====
|
|
132
315
|
// ===== 一次性派发引擎(v74 去池化重写)=====
|
|
@@ -225,7 +408,7 @@ export function apply(ctx) {
|
|
|
225
408
|
try { pack = await readContextPack(sid, t) } catch (e) { console.error('[task-board] context pack read failed:', String(e)) }
|
|
226
409
|
// user prompt 只留一行指引,内容走上下文注入区块
|
|
227
410
|
var packNote = pack ? '本任务附带主窗口预研文件,已通过「上下文注入」区提供(含文件完整内容),直接基于其内容工作,不要重复读取这些文件。' : ''
|
|
228
|
-
var req = { label: role + ':' + t.id, prompt: [{ type: 'text', text: role === 'worker' ? buildWorkerPrompt(t, packNote) : buildVerifierPrompt(t, packNote) }], parent: parent, signal: makeSignal() }
|
|
411
|
+
var req = { label: role + ':' + t.id, prompt: [{ type: 'text', text: role === 'worker' ? buildWorkerPrompt(t, packNote, cfg(dsnap).feedbackEnabled) : buildVerifierPrompt(t, packNote) }], parent: parent, signal: makeSignal() }
|
|
229
412
|
if (modelOverride) {
|
|
230
413
|
// list-models 返回的 id 是 "provider/model" 复合格式(如 "cmss/zhanlu/glm-5.2"),
|
|
231
414
|
// 但 AgentOptions 的 provider 和 model 是分开的——整串塞进 model 会报 UNKNOWN_MODEL
|
|
@@ -263,7 +446,8 @@ export function apply(ctx) {
|
|
|
263
446
|
tm.timeout(softMs).then(function () {
|
|
264
447
|
if (rec.settled) return
|
|
265
448
|
var mins = Math.round((Date.now() - startedAt) / 60000)
|
|
266
|
-
|
|
449
|
+
// 带 taskId:投递前会按任务现状复查,任务已完成/落定的过期告警直接丢弃(避免误报)
|
|
450
|
+
pushSysNote(sid, '⏱ 任务「' + t.title + '」的 ' + role + '(' + t.id + ')已运行 ' + mins + ' 分钟仍未完成——如属正常长任务可忽略;需要干预可在看板详情页「立即终止」(硬超时 ' + c.hardTimeoutMin + ' 分钟后将自动终止并重试)', t.id)
|
|
267
451
|
softArm() // 持续提醒直到结算或硬超时
|
|
268
452
|
}).catch(function () {})
|
|
269
453
|
})()
|
|
@@ -286,6 +470,37 @@ export function apply(ctx) {
|
|
|
286
470
|
} catch (e) { console.error('[task-board] settle ' + rec.role + ' failed (task ' + rec.taskId + '):', String(e)) }
|
|
287
471
|
// 历史会话留档:记录该次 run 的结局(完成/失败/硬超时),详情页可据此标注阶段状态
|
|
288
472
|
closeRunHistory(sid, rec.taskId, String(rec.run.id), failed ? (err ? 'timeout/error' : 'incomplete') : 'completed').catch(function () {})
|
|
473
|
+
// ===== token 消耗结算:读该次 run 的 v4 日志聚合 usage,累加到任务 =====
|
|
474
|
+
// 放在状态推进之后:统计是附加信息,读日志失败/无 usage 时静默跳过,绝不影响结算语义。
|
|
475
|
+
// Worker 失败重试、驳回重做都会各走一次 settleRun,因此多轮消耗天然累加(runs 计数)。
|
|
476
|
+
await accumulateRunUsage(sid, rec)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 把一次 run 的 token 消耗累加到任务(t.usage):总量/输入/输出/缓存读写 + 按模型小计 + runs 计数。
|
|
480
|
+
// 模型小计的 key:优先本次派发显式覆盖的模型(rec.model),否则用日志里记录的会话模型。
|
|
481
|
+
async function accumulateRunUsage(sid, rec) {
|
|
482
|
+
var u = null
|
|
483
|
+
try { u = readRunUsage(String(rec.run.id)) } catch (_) { u = null }
|
|
484
|
+
if (!u || !u.total) return
|
|
485
|
+
try {
|
|
486
|
+
await mutateLocked(sid, function (d) {
|
|
487
|
+
var t = d.tasks.find(function (x) { return x.id === rec.taskId })
|
|
488
|
+
if (!t) return null
|
|
489
|
+
if (!t.usage) t.usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, runs: 0, models: {} }
|
|
490
|
+
// 逐字段兜底 0:脏数据(半写/老形态)也不会把 NaN 带进看板(NaN 会让工具输出的 lossless-JSON 校验拒整条结果)
|
|
491
|
+
t.usage.input = (t.usage.input || 0) + u.input
|
|
492
|
+
t.usage.output = (t.usage.output || 0) + u.output
|
|
493
|
+
t.usage.cacheRead = (t.usage.cacheRead || 0) + u.cacheRead
|
|
494
|
+
t.usage.cacheWrite = (t.usage.cacheWrite || 0) + u.cacheWrite
|
|
495
|
+
t.usage.total = (t.usage.total || 0) + u.total
|
|
496
|
+
t.usage.runs = (t.usage.runs || 0) + 1
|
|
497
|
+
if (!t.usage.models) t.usage.models = {}
|
|
498
|
+
var mk = rec.model || u.model || '(未知模型)'
|
|
499
|
+
t.usage.models[mk] = (t.usage.models[mk] || 0) + u.total
|
|
500
|
+
t.usage.updatedAt = new Date().toISOString()
|
|
501
|
+
return { ok: true }
|
|
502
|
+
})
|
|
503
|
+
} catch (e) { console.error('[task-board] usage accumulate failed (task ' + rec.taskId + '):', String(e)) }
|
|
289
504
|
}
|
|
290
505
|
|
|
291
506
|
async function settleWorker(sid, rec, output, failed, errText) {
|
|
@@ -345,6 +560,8 @@ export function apply(ctx) {
|
|
|
345
560
|
verifyApply(d, t, String(rec.run.id), approved ? 'approved' : 'rejected', trimmed.slice(0, 200))
|
|
346
561
|
if (!approved) {
|
|
347
562
|
t.rejectCount = (t.rejectCount || 0) + 1
|
|
563
|
+
// 学习飞轮 v1:Verifier 驳回 → 候选教训(场景/错误做法/来源),随 t.verification.at 判重
|
|
564
|
+
pushRejectLesson(d, t, vsecs.verifySummary || trimmed, t.verification.at)
|
|
348
565
|
if (t.rejectCount >= 3) { t.status = 'blocked'; ah(t, 'in-progress', 'blocked', 'system', 'verifier 驳回 x' + t.rejectCount + ',待人工裁决') }
|
|
349
566
|
else { t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; ah(t, 'in-progress', 'pending', 'system', '驳回重派:新 Worker 将携带驳回原因继续') }
|
|
350
567
|
}
|
|
@@ -360,12 +577,36 @@ export function apply(ctx) {
|
|
|
360
577
|
}
|
|
361
578
|
|
|
362
579
|
// 歧义上报通知:任何模式都通知主窗口(escalation 需要人工裁决,不能静默吞掉)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
580
|
+
// 25s 去抖投递:主窗口 turn 进行中时 followup 只在宿主侧排队,送达时任务常已被裁决/归档(过期回声);
|
|
581
|
+
// 排队无法撤回,插件侧唯一可行的方案就是延迟 + 投递前重查看板。
|
|
582
|
+
// escNotifyTimers 存每个任务最新一次调度:同一任务再次上报即顶替旧调度(旧回调身份不匹配 → 静默丢弃);
|
|
583
|
+
// 投递时才读 escalation.question,所以连续多次上报只会收到一条、且一定是最新疑问。
|
|
584
|
+
var escNotifyTimers = {}
|
|
585
|
+
function deliverEscalation(sid, taskId) {
|
|
586
|
+
rt(sid).then(function (d) {
|
|
587
|
+
var t = null
|
|
588
|
+
var list = (d && d.tasks) || []
|
|
589
|
+
for (var i = 0; i < list.length; i++) { if (list[i].id === taskId) { t = list[i]; break } }
|
|
590
|
+
// escalation 已消失(已被裁决)或任务已 resolved/archived → 通知已过期,静默跳过(history 不记)
|
|
591
|
+
if (!t || !t.escalation || t.status === 'resolved' || t.status === 'archived') return
|
|
592
|
+
var root = rootForSession(sid)
|
|
593
|
+
if (!root) return
|
|
594
|
+
try { root.followup(makeMsg('⚠️ [任务看板] Worker 上报歧义,等待裁决:\n\n任务: ' + t.title + ' (' + t.id + ')\n\n疑问:\n' + String(t.escalation.question || '').slice(0, 1500) + '\n\n请在看板详情页裁决,或直接回复指示。裁决后会有新 Worker 带着裁决答案接手。\n\n(若收到时任务已被裁决或归档,说明本通知投递晚于处理——先用 task_list/get-tasks 核对状态,勿重复裁决。)', 'notice', '任务待裁决: ' + t.title)) } catch (e) { console.error('[task-board] escalate notify failed:', String(e)) }
|
|
595
|
+
}).catch(function (e) { console.error('[task-board] escalate notify failed:', String(e)) })
|
|
596
|
+
}
|
|
597
|
+
function notifyMainWindow(sid, t) {
|
|
598
|
+
var tm = ctx.timer
|
|
599
|
+
if (!tm) { deliverEscalation(sid, t.id); return } // timer 不可用 → 直接投递(保持原即时行为)
|
|
600
|
+
var key = sid + ':' + t.id
|
|
601
|
+
var mine = tm.timeout(25000)
|
|
602
|
+
escNotifyTimers[key] = mine
|
|
603
|
+
mine.then(function () {
|
|
604
|
+
if (escNotifyTimers[key] !== mine) return // 已被该任务更新的一次上报顶替 → 丢弃,避免重复通知
|
|
605
|
+
delete escNotifyTimers[key]
|
|
606
|
+
deliverEscalation(sid, t.id)
|
|
607
|
+
}).catch(function () {}) // 插件销毁时 timeout 会 reject("Context has been disposed"),静默吞掉
|
|
367
608
|
}
|
|
368
|
-
function maybeNotify(sid, task) { if (task && task.escalation) { notifyMainWindow(sid, task
|
|
609
|
+
function maybeNotify(sid, task) { if (task && task.escalation) { notifyMainWindow(sid, task) } }
|
|
369
610
|
|
|
370
611
|
// ===== 任务回执通知(批量聚合 + 空闲门控):派发执行的任务在 完成/阻塞 时通知主窗口 =====
|
|
371
612
|
// 只通知派发执行的任务(isDispatched),主窗口自己手动处理的任务不回执(自己干的自己知道)。
|
|
@@ -397,9 +638,11 @@ export function apply(ctx) {
|
|
|
397
638
|
}
|
|
398
639
|
// 系统级异常通知队列(易失,随回执冲刷):模型熔断/spawn 失败/孤儿回收/看门狗标记
|
|
399
640
|
var sysNotesBuf = {}
|
|
400
|
-
|
|
641
|
+
// taskId 可选:告警类通知(软超时提醒等)语义只对「任务仍在执行中」成立,
|
|
642
|
+
// 带上任务 id 后 flush 投递前可重读看板校验,任务已落定的过期告警直接丢弃
|
|
643
|
+
function pushSysNote(sid, text, taskId) {
|
|
401
644
|
var arr = (sysNotesBuf[sid] = sysNotesBuf[sid] || [])
|
|
402
|
-
arr.push({ text: text, at: new Date().toISOString() })
|
|
645
|
+
arr.push({ text: text, at: new Date().toISOString(), taskId: taskId || null })
|
|
403
646
|
if (arr.length > 10) arr.splice(0, arr.length - 10)
|
|
404
647
|
}
|
|
405
648
|
function flushReceipts(sid) {
|
|
@@ -409,28 +652,64 @@ export function apply(ctx) {
|
|
|
409
652
|
receiptBuf[sid] = null
|
|
410
653
|
sysNotesBuf[sid] = []
|
|
411
654
|
var root = rootForSession(sid); if (!root) return
|
|
412
|
-
var
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
for (var
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
655
|
+
var items = (buf && buf.items) ? buf.items.slice() : []
|
|
656
|
+
function noteOf(status) { return status === 'verifying' ? '验证中' : '进行中' }
|
|
657
|
+
// 组装投递文本(入参已是过滤后的存活项,避免用已丢弃项的计数)
|
|
658
|
+
function composeText(items, notes) {
|
|
659
|
+
var done = [], blocked = []
|
|
660
|
+
for (var i = 0; i < items.length; i++) { (items[i].kind === 'resolved' ? done : blocked).push(items[i]) }
|
|
661
|
+
var lines = [done.length || blocked.length ? '📋 [任务看板] 回执摘要(' + items.length + ' 条)' : '📋 [任务看板] 系统通知', '']
|
|
662
|
+
if (done.length) {
|
|
663
|
+
lines.push('✅ 完成 ' + done.length + ' 个:')
|
|
664
|
+
for (var j = 0; j < done.length && j < 8; j++) lines.push(' · ' + done[j].title + ' (' + done[j].id + ')' + (done[j].summary ? ' — ' + done[j].summary.slice(0, 120) : ''))
|
|
665
|
+
}
|
|
666
|
+
if (blocked.length) {
|
|
667
|
+
lines.push('🛑 阻塞 ' + blocked.length + ' 个(需关注):')
|
|
668
|
+
for (var k = 0; k < blocked.length && k < 8; k++) lines.push(' · ' + blocked[k].title + ' (' + blocked[k].id + ')' + (blocked[k].note ? ' — ' + blocked[k].note.slice(0, 150) : ''))
|
|
669
|
+
}
|
|
670
|
+
if (notes.length) {
|
|
671
|
+
lines.push('', '⚠️ 系统异常 ' + notes.length + ' 条:')
|
|
672
|
+
for (var n = 0; n < notes.length && n < 8; n++) lines.push(' · ' + notes[n].text)
|
|
673
|
+
}
|
|
674
|
+
lines.push('', '可用 task_list 查看全部;阻塞项可在看板拖回待办重新投放。')
|
|
675
|
+
return lines.join('\n')
|
|
422
676
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
677
|
+
// 投递前状态过滤:入队到投递之间隔着 45s 聚合窗口 + 等主窗口空闲(最长 5 分钟),
|
|
678
|
+
// 期间任务可能已经完成/归档——过期告警与死回执会误报(实测软超时告警 6/6 全误报)。
|
|
679
|
+
// 所以 send 之前重读一次看板,按任务现状决定丢哪些项。
|
|
680
|
+
function deliver() {
|
|
681
|
+
return Promise.resolve().then(function () { return rt(sid) }).catch(function () { return null }).then(function (snap) {
|
|
682
|
+
var tasks = (snap && snap.tasks) || []
|
|
683
|
+
function findTask(id) { for (var i = 0; i < tasks.length; i++) { if (tasks[i].id === id) return tasks[i] } return null }
|
|
684
|
+
// a. 带 taskId 的告警项:任务状态不在 in-progress/verifying 即已落定 → 丢弃;
|
|
685
|
+
// 保留的项在文本前标注投递时状态(读到的是发送瞬间的真实状态,人可据此判断时效)
|
|
686
|
+
var keptNotes = []
|
|
687
|
+
for (var i = 0; i < notes.length; i++) {
|
|
688
|
+
var nt = notes[i]
|
|
689
|
+
if (nt.taskId) {
|
|
690
|
+
var t = findTask(nt.taskId)
|
|
691
|
+
if (!t || (t.status !== 'in-progress' && t.status !== 'verifying')) continue
|
|
692
|
+
keptNotes.push({ text: '(投递时状态:' + noteOf(t.status) + ')' + nt.text })
|
|
693
|
+
} else keptNotes.push(nt)
|
|
694
|
+
}
|
|
695
|
+
// b. 回执项:任务已 archived(人已手动归档 = 已知悉)→ 丢弃;
|
|
696
|
+
// resolved/blocked 保留(回执是主通道,任务查不到也保留,不能因读盘失败丢回执)
|
|
697
|
+
var keptItems = []
|
|
698
|
+
for (var j = 0; j < items.length; j++) {
|
|
699
|
+
var it = items[j]
|
|
700
|
+
var tt = findTask(it.id)
|
|
701
|
+
if (tt && tt.status === 'archived') continue
|
|
702
|
+
keptItems.push(it)
|
|
703
|
+
}
|
|
704
|
+
// c. 过滤后全空 → 不再打扰主窗口
|
|
705
|
+
if (!keptItems.length && !keptNotes.length) return
|
|
706
|
+
try { root.followup(makeMsg(composeText(keptItems, keptNotes), 'recall')) } catch (e) { console.error('[task-board] receipt flush failed:', String(e)) }
|
|
707
|
+
})
|
|
426
708
|
}
|
|
427
|
-
lines.push('', '可用 task_list 查看全部;阻塞项可在看板拖回待办重新投放。')
|
|
428
|
-
var text = lines.join('\n')
|
|
429
|
-
function send() { try { root.followup(makeMsg(text, 'recall')) } catch (e) { console.error('[task-board] receipt flush failed:', String(e)) } }
|
|
430
709
|
if (typeof root.whenIdle === 'function') {
|
|
431
710
|
var waited = withTimeout(root.whenIdle(), 300000, 'receipt-idle-wait') // 最多等 5 分钟,超时也发(不能丢回执)
|
|
432
|
-
Promise.resolve(waited).then(
|
|
433
|
-
} else
|
|
711
|
+
Promise.resolve(waited).then(deliver, deliver).catch(function (e) { console.error('[task-board] receipt flush failed:', String(e)) })
|
|
712
|
+
} else deliver().catch(function (e) { console.error('[task-board] receipt flush failed:', String(e)) })
|
|
434
713
|
}
|
|
435
714
|
|
|
436
715
|
// ===== 派发周期(15s 心跳 + 写入后 kickCycle 触发)=====
|
|
@@ -471,6 +750,15 @@ export function apply(ctx) {
|
|
|
471
750
|
var picked = pickDispatch(d, capW, Math.max(0, c.maxVerifiers - activeV), runs)
|
|
472
751
|
picked.pendings.forEach(function (t) { claimApply(d, t, 'spawn-pending', 'dispatch'); toSpawn.push({ role: 'worker', t: t }); info.push('dispatch ' + t.id) })
|
|
473
752
|
picked.verifs.forEach(function (t) { t.verifierRun = 'spawn-pending'; t.verifierRunAt = new Date().toISOString(); toSpawn.push({ role: 'verifier', t: t }); info.push('verify ' + t.id) })
|
|
753
|
+
// touches 文件级排他展示态:被拦候选写 t.waitingForTouches = [持有者任务id...],
|
|
754
|
+
// 未被拦/已派发/已落定的任务清除该字段(每心跳刷新的 UI 展示态,不参与任何派发逻辑,
|
|
755
|
+
// 但必须显式清——只在写入时报字段会留下"锁已释放仍显示 🔒 等待"的永久误导)。
|
|
756
|
+
var waitMap = {}
|
|
757
|
+
picked.blockedTouches.forEach(function (b) { waitMap[b.id] = b.conflicts })
|
|
758
|
+
d.tasks.forEach(function (t) {
|
|
759
|
+
if (waitMap[t.id]) { t.waitingForTouches = waitMap[t.id]; info.push('wait-touches ' + t.id + '<-' + waitMap[t.id].join(',')) }
|
|
760
|
+
else if (t.waitingForTouches) delete t.waitingForTouches
|
|
761
|
+
})
|
|
474
762
|
// UI 池状态:来自活跃 run(一次性模型:没有成员名册,只有在跑的任务)
|
|
475
763
|
d.poolStatus = { workers: [], verifiers: [] }
|
|
476
764
|
Object.keys(runs).forEach(function (k) { var rc = runs[k]; d.poolStatus[rc.role === 'worker' ? 'workers' : 'verifiers'].push({ id: k, num: '-', busy: true, taskId: rc.taskId, runId: String(rc.run.id), done: 0, queueLen: 0, suspect: false, model: rc.model || '' }) })
|
|
@@ -520,7 +808,7 @@ export function apply(ctx) {
|
|
|
520
808
|
// 不能走 resolveRoot:该函数在每个 agent 每次 prompt 组装时同步执行,
|
|
521
809
|
// resolveRoot 全表扫 agent 注册表会把宿主事件循环卡死(曾导致全局界面卡顿、用户消息延迟渲染)。
|
|
522
810
|
if (!teamModeCache[String(agent.id)]) return ''
|
|
523
|
-
return '【任务看板 Team 模式已开启】\n本会话的任务看板处于 Team 模式。请遵循以下工作方式:\n1. 涉及代码改动、文件创建、命令执行等实质性工作时,优先用 task_create 提交为看板任务(由一次性 Worker/Verifier 子代理执行与验收),不要自己直接动手实现。\n2. 你仍保有全部工具能力——调研、读代码、讨论方案、回答问题时直接进行,无需提交任务。\n3. 创建任务时,务必在 description 里写清任务目标和约束;调研结论/原始需求/思路用 contextNotes 带上,调研时读过的关键文件用 contextFiles 把路径带上——两者都会通过「上下文注入」通道传给子代理(独立注入区块,不占对话流)。子代理是全新会话、无你的会话记忆,上下文不够它需要从零自行调研,效率大打折扣甚至跑偏方向。\n4. Worker 上报歧义时会通过 task_arbitrate 等待你裁决,请及时响应。驳回重派时同样:新 Worker 没有上一轮的记忆,驳回原因会在 prompt 里,但额外上下文需你在 description 里补上。\n5.
|
|
811
|
+
return '【任务看板 Team 模式已开启】\n本会话的任务看板处于 Team 模式。请遵循以下工作方式:\n1. 涉及代码改动、文件创建、命令执行等实质性工作时,优先用 task_create 提交为看板任务(由一次性 Worker/Verifier 子代理执行与验收),不要自己直接动手实现。\n2. 你仍保有全部工具能力——调研、读代码、讨论方案、回答问题时直接进行,无需提交任务。\n3. 创建任务时,务必在 description 里写清任务目标和约束;调研结论/原始需求/思路用 contextNotes 带上,调研时读过的关键文件用 contextFiles 把路径带上——两者都会通过「上下文注入」通道传给子代理(独立注入区块,不占对话流)。子代理是全新会话、无你的会话记忆,上下文不够它需要从零自行调研,效率大打折扣甚至跑偏方向。\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,让子代理少踩重复的坑。' : '')
|
|
524
812
|
},
|
|
525
813
|
})
|
|
526
814
|
ctx.effect(function () { return disposeSection })
|
|
@@ -561,8 +849,8 @@ export function apply(ctx) {
|
|
|
561
849
|
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) }) } }))
|
|
562
850
|
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' }; return verifyApply(d, t, actor, args.verdict, args.comment) }) } }))
|
|
563
851
|
ctx.tools.register(defineTool({ name: 'task_archive', description: '归档已解决/已取消任务。', 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' }; if (t.status !== 'resolved' && t.status !== 'cancelled') return { ok: false, error: 'only resolved/cancelled' }; 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 }) } }))
|
|
564
|
-
ctx.tools.register(defineTool({ name: 'task_update', description: '更新任务字段,可选重置为 pending。dependsOn/pipeline 也可更新(环检测会拒绝成环依赖)。contextFiles 可更新预研文件清单。', 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' }, 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(); 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 (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') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; return { ok: true, task: t } }) } }))
|
|
565
|
-
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 可选:调研结论/原始需求/思路等非文件类上下文,同样走上下文注入。', 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'] }, draft: { type: 'boolean', description: 'true
|
|
852
|
+
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(); 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 (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') } 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; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; return { ok: true, task: t } }) } }))
|
|
853
|
+
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 可选:调研结论/原始需求/思路等非文件类上下文,同样走上下文注入。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); return withSplitHint({ ok: true, task: t }, t) }) } }))
|
|
566
854
|
|
|
567
855
|
// ===== RPC =====
|
|
568
856
|
// get-tasks 是纯读路径(rt 只读文件)——poolCycle 由 15s 心跳 + 写入后 kickCycle 驱动,
|
|
@@ -570,6 +858,13 @@ export function apply(ctx) {
|
|
|
570
858
|
handle('get-tasks', async function (args) { var sid = rpcSessionId(args); var d = await rt(sid); d.sessionId = sid; var __ag = ctx.agents; d.isRoot = true; if (__ag) { var __roots = __ag.roots(); var __rids = []; for (var __i = 0; __i < __roots.length; __i++) __rids.push(String(__roots[__i].id)); d.isRoot = __rids.indexOf(sid) >= 0 }
|
|
571
859
|
// poolStatus 防幽灵:只保留指向当前活跃任务的条目(重启后内存 runs 清空,文件快照可能残留)
|
|
572
860
|
if (d.poolStatus) { var __act = {}; (d.tasks || []).forEach(function (t) { if (t.status === 'in-progress' || t.status === 'verifying') __act[t.id] = true }); d.poolStatus.workers = (d.poolStatus.workers || []).filter(function (w) { return __act[w.taskId] }); d.poolStatus.verifiers = (d.poolStatus.verifiers || []).filter(function (v) { return __act[v.taskId] }) }
|
|
861
|
+
// 学习飞轮 v1:把 feedbackEnabled 显式放进返回体(normalizeBoard 已按老看板补默认 true),
|
|
862
|
+
// 客户端据此做能力检测——开关关闭时渲染层不生成候选、不显示「沉淀」按钮。
|
|
863
|
+
d.feedbackEnabled = cfg(d).feedbackEnabled
|
|
864
|
+
// 工作模式派生字段:UI 只读这一个字段决定三档选中态(不落盘,写入仍走 boardMode/teamMode)
|
|
865
|
+
d.workMode = deriveWorkMode(d)
|
|
866
|
+
// ===== board 级 token 消耗聚合(现算,不落盘):总量 + 输入/输出/缓存读 + 按模型 + 任务 Top8 =====
|
|
867
|
+
d.usageSummary = aggregateUsageSummary(d.tasks)
|
|
573
868
|
return d })
|
|
574
869
|
// 派发前上下文预览:主 agent 用它确认"我将注入给子代理的材料"是否足够(不发任务、不落盘)
|
|
575
870
|
handle('preview-context', async function (args) {
|
|
@@ -591,48 +886,31 @@ export function apply(ctx) {
|
|
|
591
886
|
// (不再从看板路径反推 workspace——那依赖进程 cwd 凑巧等于工作区,曾是隐性 bug)
|
|
592
887
|
// v0.1.7 起日志文件名带格式版本号(session.v4.jsonl.zstd),旧会话是
|
|
593
888
|
// session.v3.jsonl.zstd / session.jsonl.zstd——按新到旧逐个探测。
|
|
594
|
-
|
|
595
|
-
var log =
|
|
596
|
-
var LOG_NAMES = ['session.v4.jsonl.zstd', 'session.v3.jsonl.zstd', 'session.v2.jsonl.zstd', 'session.jsonl.zstd']
|
|
597
|
-
var buckets = fsNode.readdirSync(sessionsRoot)
|
|
598
|
-
for (var bi = 0; bi < buckets.length && !log; bi++) {
|
|
599
|
-
for (var ni = 0; ni < LOG_NAMES.length; ni++) {
|
|
600
|
-
var cand = path.join(sessionsRoot, buckets[bi], child, LOG_NAMES[ni])
|
|
601
|
-
if (fsNode.existsSync(cand)) { log = cand; break }
|
|
602
|
-
}
|
|
603
|
-
}
|
|
889
|
+
// 定位与分帧和 readRunUsage 共用同一套 helper(findRunLog / readLogBytes / readLogFrames)。
|
|
890
|
+
var log = findRunLog(child)
|
|
604
891
|
if (!log) return { ok: true, activity: null, reason: 'log not found' }
|
|
605
892
|
// 只同步读末尾 2MB(日志追加写,末帧必在尾部)——整文件 readFileSync 在大日志上
|
|
606
893
|
// 会造成数十毫秒级同步 I/O,多任务轮询时叠加成全局卡顿
|
|
607
|
-
var
|
|
608
|
-
|
|
609
|
-
var start = sz > TAIL ? sz - TAIL : 0
|
|
610
|
-
var buf = Buffer.alloc(sz - start)
|
|
611
|
-
var fd = fsNode.openSync(log, 'r')
|
|
612
|
-
try { fsNode.readSync(fd, buf, 0, buf.length, start) } finally { fsNode.closeSync(fd) }
|
|
613
|
-
var MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
894
|
+
var buf = readLogBytes(log, 2 * 1024 * 1024)
|
|
895
|
+
if (!buf) return { ok: true, activity: null, reason: 'log unreadable' }
|
|
614
896
|
// 追加写多帧格式:最新事件在末帧。但末帧可能只有 step/end、turn/end 这类
|
|
615
897
|
// 结算事件(v4 一帧只装一个step的增量),单解一帧经常捞不到动作 → 从新到旧
|
|
616
898
|
// 最多回扫 3 帧,找到第一个动作摘要即止(成本仍受控:每帧只是一次 zstd 解压)
|
|
899
|
+
var frames = readLogFrames(buf, 3)
|
|
617
900
|
var activity = null
|
|
618
|
-
for (var
|
|
619
|
-
var
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
var
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
var blocks = Array.isArray(msg.content) ? msg.content : []
|
|
632
|
-
for (var b = blocks.length - 1; b >= 0; b--) { if (blocks[b] && blocks[b].type === 'text' && String(blocks[b].text || '').trim()) { activity = '💬 ' + String(blocks[b].text).replace(/\s+/g, ' ').slice(0, 120); break } }
|
|
633
|
-
}
|
|
634
|
-
else if (e.type === 'assistant/chunk' && dta.block && dta.block.type === 'text' && dta.block.text && dta.block.text.trim()) activity = '💬 ' + dta.block.text.replace(/\s+/g, ' ').slice(0, 120)
|
|
635
|
-
} catch (_) {}
|
|
901
|
+
for (var fi = 0; fi < frames.length && !activity; fi++) {
|
|
902
|
+
var evs = frames[fi]
|
|
903
|
+
for (var i = evs.length - 1; i >= 0 && !activity; i--) {
|
|
904
|
+
var e = evs[i]
|
|
905
|
+
var dta = (e && e.data) || {}
|
|
906
|
+
if (e && e.type === 'tool/call' && dta.name) activity = '🔧 ' + dta.name + ' ' + String(dta.arguments || '').replace(/\s+/g, ' ').slice(0, 90)
|
|
907
|
+
// v4:助手文本在 assistant/message 的 content 块里;v3 及以前是 assistant/chunk 流片
|
|
908
|
+
else if (e && e.type === 'assistant/message') {
|
|
909
|
+
var msg = dta.message || {}
|
|
910
|
+
var blocks = Array.isArray(msg.content) ? msg.content : []
|
|
911
|
+
for (var b = blocks.length - 1; b >= 0; b--) { if (blocks[b] && blocks[b].type === 'text' && String(blocks[b].text || '').trim()) { activity = '💬 ' + String(blocks[b].text).replace(/\s+/g, ' ').slice(0, 120); break } }
|
|
912
|
+
}
|
|
913
|
+
else if (e && e.type === 'assistant/chunk' && dta.block && dta.block.type === 'text' && dta.block.text && dta.block.text.trim()) activity = '💬 ' + dta.block.text.replace(/\s+/g, ' ').slice(0, 120)
|
|
636
914
|
}
|
|
637
915
|
}
|
|
638
916
|
return { ok: true, activity: activity }
|
|
@@ -665,28 +943,68 @@ export function apply(ctx) {
|
|
|
665
943
|
})
|
|
666
944
|
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 } }) })
|
|
667
945
|
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) }) })
|
|
668
|
-
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;
|
|
946
|
+
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 }) })
|
|
669
947
|
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' }; if (t.status !== 'resolved' && t.status !== 'cancelled') return { ok: false, error: 'cannot archive' }; 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 }) })
|
|
670
|
-
handle('update-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 (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') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; return { ok: true, task: t } }) })
|
|
671
|
-
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 } }) })
|
|
672
|
-
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 } }) })
|
|
948
|
+
handle('update-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 (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') } 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; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; return { ok: true, task: t } }) })
|
|
949
|
+
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) } }) })
|
|
950
|
+
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) } }) })
|
|
951
|
+
// 工作模式三档单入口(v75 UI 收敛):一次写入 boardMode+teamMode 两个字段,
|
|
952
|
+
// 复用与老 RPC 完全相同的写入语义(team 档强制 auto;list 档关 team)——
|
|
953
|
+
// 老 RPC set-board-mode/set-team-mode 原样保留,旧客户端/脚本/E2E 不受影响。
|
|
954
|
+
handle('set-work-mode', async function (args) {
|
|
955
|
+
var sid = rpcSessionId(args)
|
|
956
|
+
var m = args && args.mode
|
|
957
|
+
var mode = (m === 'list' || m === 'team') ? m : 'auto' // 缺省/非法值兜底 auto
|
|
958
|
+
return mutateLocked(sid, function (d) {
|
|
959
|
+
if (mode === 'list') { d.boardMode = 'manual'; d.teamMode = false }
|
|
960
|
+
else if (mode === 'team') { d.boardMode = 'auto'; d.teamMode = true }
|
|
961
|
+
else { d.boardMode = 'auto'; d.teamMode = false }
|
|
962
|
+
teamModeCache[sid] = !!d.teamMode // 同步 systemPrompt 引导段的 teamMode 缓存
|
|
963
|
+
return { ok: true, mode: mode, workMode: deriveWorkMode(d), boardMode: d.boardMode, teamMode: !!d.teamMode }
|
|
964
|
+
})
|
|
965
|
+
})
|
|
673
966
|
// 裁决回流(v74 一次性模型):原 Worker 已结束,答案写入 history 后任务回 pending,
|
|
674
967
|
// 下个派发周期 spawn 新 Worker,裁决内容随 prompt 注入(histNotes 匹配"裁决")
|
|
675
|
-
|
|
968
|
+
// 结构化动作(裁决竞态保护):
|
|
969
|
+
// action='resume'(默认,保持现状)= 裁决后立即回到派发池,~50ms 内被自动重派;
|
|
970
|
+
// action='hold' = 任务置 frozen 冻结,不参与任何自动派发(pickDispatch 跳过),
|
|
971
|
+
// 留给主窗口补 dependsOn/补上下文的时间窗口,补完再显式解冻(unfreeze-task)。
|
|
972
|
+
// hold 时 mutateLocked 传 skipKick:冻结任务本就不该派发,省掉一次无意义的 poolCycle。
|
|
973
|
+
async function doResolveEscalation(sid, actor, taskId, answer, action) {
|
|
974
|
+
var act = action === 'hold' ? 'hold' : 'resume'
|
|
676
975
|
var result = await mutateLocked(sid, function (d) {
|
|
677
976
|
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
678
977
|
if (!t) return { ok: false, error: 'not found' }
|
|
679
978
|
if (!t.escalation) return { ok: false, error: 'not escalated' }
|
|
979
|
+
var escQ = String(t.escalation.question || '') // 先留档:下面 delete 后就没得取了
|
|
680
980
|
delete t.escalation
|
|
681
981
|
if (!Array.isArray(t.messages)) t.messages = []
|
|
682
|
-
|
|
683
|
-
|
|
982
|
+
var arbAt = new Date().toISOString()
|
|
983
|
+
t.messages.push({ kind: 'arbitration', text: answer || '', at: arbAt, by: actor, action: act })
|
|
984
|
+
// 学习飞轮 v1:主窗口的裁决结论是最值钱的教训来源(Worker 会在同一个坑里反复上报)
|
|
985
|
+
// → 生成候选教训(场景/疑问/裁决结论),由主窗口决定要不要沉淀进笔记/记忆工具。
|
|
986
|
+
pushArbitrationLesson(d, t, escQ, answer || '', arbAt)
|
|
987
|
+
ah(t, t.status, t.status, actor, (act === 'hold' ? '主窗口裁决(挂起冻结): ' : '主窗口裁决: ') + (answer || '').slice(0, 200))
|
|
684
988
|
// in-progress 的歧义任务:回 pending 重派(新 Worker 带裁决上下文);verifying 的 verifier 故障升级:保持待审,下轮派新 verifier
|
|
685
|
-
if (t.status === 'in-progress') { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; ah(t, ps, 'pending', 'system', '带裁决重新排队') }
|
|
686
|
-
|
|
687
|
-
|
|
989
|
+
if (t.status === 'in-progress') { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; ah(t, ps, 'pending', 'system', act === 'hold' ? '带裁决挂起冻结(不参与自动派发)' : '带裁决重新排队') }
|
|
990
|
+
if (act === 'hold') { t.frozen = true; t.frozenAt = new Date().toISOString(); t.frozenBy = actor; ah(t, t.status, t.status, actor, '冻结:不参与自动派发,待主窗口补完上下文后解冻') }
|
|
991
|
+
else { delete t.frozen; delete t.frozenAt; delete t.frozenBy }
|
|
992
|
+
return { ok: true, task: t, answer: answer || '', action: act, frozen: !!t.frozen }
|
|
993
|
+
}, act === 'hold')
|
|
688
994
|
return result
|
|
689
995
|
}
|
|
996
|
+
// 解冻(竞态保护配套通道):清 frozen + 记历史 + 触发派发(mutateLocked 默认 kickCycle)
|
|
997
|
+
async function doUnfreezeTask(sid, actor, taskId) {
|
|
998
|
+
return mutateLocked(sid, function (d) {
|
|
999
|
+
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
1000
|
+
if (!t) return { ok: false, error: 'not found' }
|
|
1001
|
+
if (!t.frozen) return { ok: false, error: 'not frozen' }
|
|
1002
|
+
delete t.frozen; delete t.frozenAt; delete t.frozenBy
|
|
1003
|
+
ah(t, t.status, t.status, actor, '解除冻结,重新进入派发池')
|
|
1004
|
+
return { ok: true, task: t, unfrozen: true }
|
|
1005
|
+
})
|
|
1006
|
+
}
|
|
1007
|
+
handle('unfreeze-task', async function (args) { return doUnfreezeTask(rpcSessionId(args), getActorId(), args.taskId) })
|
|
690
1008
|
// 高优介入(v74):有活跃 run 则直接打进其会话;无则只记录 history(下次派发随 prompt 注入)
|
|
691
1009
|
// 通道选择(v1.2.4):steer 优先——下一个 step 边界即消费;followup 要等整个 turn 结束,
|
|
692
1010
|
// 长 turn 下干预形同失联(实测:Worker 单 turn 跑 10+ 分钟,「口径重写」类干预到位时活已按旧口径干完)。
|
|
@@ -713,7 +1031,13 @@ export function apply(ctx) {
|
|
|
713
1031
|
async function doTerminate(sid, actor, taskId) {
|
|
714
1032
|
var rec = runsFor(sid)[taskId]
|
|
715
1033
|
var label = 'no-active-run'
|
|
716
|
-
if (rec) {
|
|
1034
|
+
if (rec) {
|
|
1035
|
+
delete runsFor(sid)[taskId]; label = rec.role + ':' + taskId
|
|
1036
|
+
try { await rec.run.dispose() } catch (_) {}
|
|
1037
|
+
// 手动终止的 run 不会走 settleRun(runsFor 已摘除,结算路径会早退),
|
|
1038
|
+
// 但它确实消耗了 token——在这里补一次结算,避免终止即丢账。
|
|
1039
|
+
await accumulateRunUsage(sid, rec)
|
|
1040
|
+
}
|
|
717
1041
|
await mutateLocked(sid, function (d) {
|
|
718
1042
|
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
719
1043
|
if (!t) return null
|
|
@@ -737,13 +1061,22 @@ export function apply(ctx) {
|
|
|
737
1061
|
handle('dismiss-suspect', async function (args) { return doDismiss(rpcSessionId(args), getActorId(), args.taskId) })
|
|
738
1062
|
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) } }))
|
|
739
1063
|
// 手动触发单任务派发(manual 模式下"派发给 Worker"按钮,或 auto 模式手动补派)
|
|
1064
|
+
// touches 文件级排他:默认尊重文件锁——候选与活动任务 touches 冲突时返回
|
|
1065
|
+
// { ok:false, error:'touches-conflict', conflicts:[...] }(客户端 confirm 后带 force:true 重发)。
|
|
1066
|
+
// force:true 是人工越权通道:明知会与在跑 Worker 改同一批文件,由人决定是否强行并行。
|
|
740
1067
|
handle('dispatch-task', async function (args) {
|
|
741
1068
|
var sid = rpcSessionId(args)
|
|
742
1069
|
var role = args.role === 'verifier' ? 'verifier' : 'worker'
|
|
1070
|
+
var force = args.force === true
|
|
743
1071
|
var claimed = await mutateLocked(sid, function (d) {
|
|
744
1072
|
var t = d.tasks.find(function (x) { return x.id === args.taskId })
|
|
745
1073
|
if (!t) return { ok: false, error: 'not found' }
|
|
746
|
-
if (role === 'worker') {
|
|
1074
|
+
if (role === 'worker') {
|
|
1075
|
+
if (t.status !== 'pending') return { ok: false, error: 'not pending (状态: ' + t.status + ')' }
|
|
1076
|
+
if (t.claimedBy) return { ok: false, error: 'already claimed' }
|
|
1077
|
+
if (!force) { var cf = touchesConflict(t, holdsFiles(d)); if (cf.length) return { ok: false, error: 'touches-conflict', conflicts: cf } }
|
|
1078
|
+
claimApply(d, t, 'spawn-pending', 'manual dispatch' + (force ? '(force 越权:忽略 touches 冲突)' : ''))
|
|
1079
|
+
}
|
|
747
1080
|
else { if (t.status !== 'verifying') return { ok: false, error: 'not verifying (状态: ' + t.status + ')' }; if (t.escalation) return { ok: false, error: 'escalated, 待裁决' } }
|
|
748
1081
|
return { ok: true }
|
|
749
1082
|
})
|
|
@@ -761,19 +1094,30 @@ export function apply(ctx) {
|
|
|
761
1094
|
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) }
|
|
762
1095
|
return { ok: false, error: 'spawn failed' }
|
|
763
1096
|
})
|
|
764
|
-
handle('resolve-escalation', async function (args) { return doResolveEscalation(rpcSessionId(args), getActorId(), args.taskId, args.answer) })
|
|
1097
|
+
handle('resolve-escalation', async function (args) { return doResolveEscalation(rpcSessionId(args), getActorId(), args.taskId, args.answer, args.action) })
|
|
765
1098
|
handle('intervene-agent', async function (args) { return doIntervene(rpcSessionId(args), getActorId(), args.taskId, args.message) })
|
|
766
1099
|
// 主 Agent 工具版(Team 模式下主 Agent 通过工具裁决/介入)
|
|
767
|
-
ctx.tools.register(defineTool({ name: 'task_arbitrate', description: 'Team 模式:裁决 Worker 上报的歧义(escalation
|
|
1100
|
+
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) } }))
|
|
768
1101
|
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) } }))
|
|
769
1102
|
// ===== 池中 Agent 结构化回报工具(双模:工具优先,文本分段为降级路径)=====
|
|
770
|
-
ctx.tools.register(defineTool({ name: 'board_report', description: '[任务看板 Worker 专用] 上报任务结果。kind=complete 时填 summary/changes/selfTest/diffStat(git 仓库内改动附 git diff --stat 概要);kind=escalate 时填 question
|
|
1103
|
+
ctx.tools.register(defineTool({ name: 'board_report', description: '[任务看板 Worker 专用] 上报任务结果。kind=complete 时填 summary/changes/selfTest/diffStat(git 仓库内改动附 git diff --stat 概要);kind=escalate 时填 question(歧义上报,等待主窗口裁决);kind=progress 时填 question(一行里程碑进展摘要,≤200 字符)——只在有实际产物/结论时报,禁止定时或表演式汇报,静默不通知主窗口。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, kind: { type: 'string', enum: ['complete', 'escalate', 'progress'] }, summary: { type: 'string' }, changes: { type: 'string' }, selfTest: { type: 'string' }, diffStat: { type: 'string', description: '变更概要:git diff --stat(含 git status --short)输出,≤1500 字符' }, question: { type: 'string', description: 'kind=escalate:歧义原文;kind=progress:一行里程碑进展摘要(≤200 字符)' } }, required: ['taskId', 'kind'] }, output: jo(), execute: async function (args) {
|
|
771
1104
|
var sid = toolSessionId(); var actor = getActorId()
|
|
772
1105
|
var result = await mutateLocked(sid, function (d) {
|
|
773
1106
|
var t = d.tasks.find(function (x) { return x.id === args.taskId })
|
|
774
1107
|
if (!t) return { ok: false, error: 'not found' }
|
|
775
1108
|
if (t.status !== 'in-progress') return { ok: false, error: 'not in-progress (状态: ' + t.status + ')' }
|
|
776
1109
|
if (!Array.isArray(t.messages)) t.messages = []
|
|
1110
|
+
// ===== 里程碑进展通道(kind=progress)=====
|
|
1111
|
+
// 定位:长任务执行期间的「还在正确路上」的轻量证明;不进回执聚合、不通知主窗口(纯静默可见)。
|
|
1112
|
+
// 防表演式汇报:只在有实际产物/结论时报(契约写在 buildWorkerPrompt 里),且不写 ah() 历史(避免刷屏)。
|
|
1113
|
+
if (args.kind === 'progress') {
|
|
1114
|
+
var ptext = String(args.question || '').trim().slice(0, 200)
|
|
1115
|
+
if (!ptext) return { ok: false, error: 'question required for progress (一行进展摘要)' }
|
|
1116
|
+
var pat = new Date().toISOString()
|
|
1117
|
+
t.messages.push({ kind: 'progress', text: ptext, at: pat, by: actor })
|
|
1118
|
+
t.lastProgress = { text: ptext, at: pat } // 覆盖式:卡片只展示最新一条
|
|
1119
|
+
return { ok: true, progress: t.lastProgress }
|
|
1120
|
+
}
|
|
777
1121
|
if (args.kind === 'escalate') {
|
|
778
1122
|
var at = new Date().toISOString()
|
|
779
1123
|
t.escalation = { question: (args.question || '').slice(0, 2000), at: at, by: actor }
|
|
@@ -804,6 +1148,8 @@ export function apply(ctx) {
|
|
|
804
1148
|
t.verification = { verdict: args.verdict, summary: args.summary || '', checks: args.checks || '', at: new Date().toISOString(), by: actor }
|
|
805
1149
|
verifyApply(d, t, actor, args.verdict, (args.summary || '').slice(0, 200))
|
|
806
1150
|
if (!approved) { t.rejectCount = (t.rejectCount || 0) + 1; if (t.rejectCount >= 3) { t.status = 'blocked'; ah(t, 'in-progress', 'blocked', 'system', 'verifier 驳回 x' + t.rejectCount + ',待人工裁决') } }
|
|
1151
|
+
// 学习飞轮 v1:工具通道驳回同样生成候选教训(与文本通道同一条判重口径,不会重复落)
|
|
1152
|
+
if (!approved) pushRejectLesson(d, t, (args.summary || '') + (args.checks ? '\n' + args.checks : ''), t.verification.at)
|
|
807
1153
|
return { ok: true, task: t }
|
|
808
1154
|
})
|
|
809
1155
|
if (result && result.ok && result.task) {
|
|
@@ -830,8 +1176,28 @@ export function apply(ctx) {
|
|
|
830
1176
|
}
|
|
831
1177
|
return { ok: true, models: out }
|
|
832
1178
|
})
|
|
833
|
-
|
|
834
|
-
|
|
1179
|
+
// ===== 学习飞轮 v1:沉淀推送通道(push-lesson)=====
|
|
1180
|
+
// 详情页「沉淀」按钮 → 把结构化候选教训 followup 给主窗口 agent,由它用自己可用的
|
|
1181
|
+
// 笔记/记忆工具(如 note_manage)决定沉淀到哪、或评估后忽略。
|
|
1182
|
+
// 这就是全部:看板不知道对方有没有记忆工具、也不知道最终存到哪(零耦合红线)。
|
|
1183
|
+
// feedbackEnabled 关闭 → 直接拒绝(UI 侧也不会渲染按钮,这里是双保险)。
|
|
1184
|
+
handle('push-lesson', async function (args) {
|
|
1185
|
+
var sid = rpcSessionId(args)
|
|
1186
|
+
var text = (typeof args.text === 'string') ? args.text.trim().slice(0, 4000) : ''
|
|
1187
|
+
if (!text) return { ok: false, error: 'text required' }
|
|
1188
|
+
var d = await rt(sid)
|
|
1189
|
+
if (!cfg(d).feedbackEnabled) return { ok: false, error: 'feedback disabled' }
|
|
1190
|
+
var root = rootForSession(sid)
|
|
1191
|
+
if (!root) return { ok: false, error: 'no root agent for session' }
|
|
1192
|
+
var delivered = false
|
|
1193
|
+
try {
|
|
1194
|
+
root.followup(makeMsg('📚 [任务看板] 候选教训沉淀请求\n\n' + text + '\n\n请用你可用的笔记/记忆工具(如 note_manage)沉淀,或评估后忽略。', 'notice', '教训沉淀: ' + String(args.taskId || '')))
|
|
1195
|
+
delivered = true
|
|
1196
|
+
} catch (e) { console.error('[task-board] push-lesson followup failed:', String(e)) }
|
|
1197
|
+
return { ok: true, delivered: delivered }
|
|
1198
|
+
})
|
|
1199
|
+
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 } }) })
|
|
1200
|
+
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); return withSplitHint({ ok: true, task: t }, t) }) })
|
|
835
1201
|
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) } } })
|
|
836
1202
|
// ===== #14 批量操作:archive(仅 resolved/cancelled)/ set-priority(全部)=====
|
|
837
1203
|
handle('batch-op', async function (args) {
|