thincoder 0.12.54 → 0.12.59
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/CHANGELOG.md +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* subagent-blocks.mjs — 子agent 活动区块缓冲(AGENT-LOOP.md §7.2 D4,消费端)。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* dropped, blockEpoch }
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 数据层职责:前缀路由、事件 token 解析(D1/D2)、kind 合并追加、N2 环形上限、
|
|
10
|
-
* N1 渲染节流(渲染调度节流,数据追加永不延迟)、完成冻结(freezeSubTaskLines
|
|
11
|
-
* 家族——_frozenSubTask 载体行,渲染端 render-conversation.mjs 识别)。
|
|
3
|
+
* state.subTasks[key] = { key, role, model, async(§19.5 D-M7b——⟦ev⟧async 标记——
|
|
4
|
+
* undefined = sync), started, done, doneAt, blocks:
|
|
5
|
+
* [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError,
|
|
6
|
+
* dropped, blockEpoch, awaitingDigest(§17), _freezeAt(冻结锚点), stopped(§19.5)}。
|
|
7
|
+
* 职责:前缀路由(parseRelayPath 嵌套段→子标)、事件 token 解析、kind 合并、
|
|
8
|
+
* N2 环形上限、N1 渲染节流、完成冻结(freezeSubTaskLines 家族——锚点 splice)。
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
11
|
import { C } from "./ansi.mjs"
|
|
@@ -16,9 +13,49 @@ import { describeToolArgs } from "./tool-args.mjs"
|
|
|
16
13
|
|
|
17
14
|
/** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
|
|
18
15
|
export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
|
|
19
|
-
/** ⟦ev⟧
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
/** ⟦ev⟧ token parser:`⟦ev⟧<name>\x1e<n>\x1e<max>\x1e<phase>\x1e<detail>`。phase done
|
|
17
|
+
* = async 完成即冻结(settle 时发);settled = 挂起期完成——冻结延迟至 digest 消化
|
|
18
|
+
* 完成(§17.5.5 freezeReclaimDigestedBlocks 逐条回收——不等池空)或池空退出兜底补发;
|
|
19
|
+
* stopped(§19.5 D-M6)= cancel 中止——interrupted 语义立即冻结(标题 "stopped");
|
|
20
|
+
* queued(§20 D-SD3b)= 排队 spawn 返回即建 waiting 块(round2 #2 事件通道——
|
|
21
|
+
* `⟦ev⟧queued\x1e<kind>\x1e<position>\x1equeued\x1e<detail>`——kind slot/wait/depc)——
|
|
22
|
+
* 启动后 ⟦ev⟧async 转 running(同 key 不重建);cancelled(§20)= 出队/取消——**零字段**
|
|
23
|
+
* 移除块(不冻结——无活动可冻结)——与 async 同型单独解析(不入本正则);
|
|
24
|
+
* async(§19.5 D-M7b + 处置 #4)= **零字段**标记(`⟦ev⟧async\x1e`——无 n/max/phase/detail 段)——
|
|
25
|
+
* routeSubToken 单独解析设 sub.async = true(不入本正则——本正则要求 4 字段);**缺失 key
|
|
26
|
+
* (块尚未创建)缓冲 `state._pendingAsyncKeys`——ensureSubTaskKey 块创建时应用(兜底)。 */
|
|
27
|
+
export const SUB_EVENT_RE = /^⟦ev⟧(turn|approval|done|settled|stopped|queued)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e?([\s\S]*)$/
|
|
28
|
+
|
|
29
|
+
/** §19.5 D-M8 嵌套 relay 前缀通用解析(循环解析任意深度):
|
|
30
|
+
* `eng-coder#2/explore#1/read` → { head(块路由), inner[], label(子标), rest }。
|
|
31
|
+
* 单层 = inner[]/label ""——与既有单段匹配语义零改;无前缀 → null。 */
|
|
32
|
+
export function parseRelayPath(text) {
|
|
33
|
+
const segments = []
|
|
34
|
+
let rest = String(text)
|
|
35
|
+
for (;;) {
|
|
36
|
+
const m = rest.match(SUB_PREFIX_RE)
|
|
37
|
+
if (!m) break
|
|
38
|
+
segments.push(`${m[1]}#${m[2]}`)
|
|
39
|
+
rest = rest.slice(m[0].length)
|
|
40
|
+
}
|
|
41
|
+
if (segments.length === 0) return null
|
|
42
|
+
return {
|
|
43
|
+
head: segments[0],
|
|
44
|
+
inner: segments.slice(1),
|
|
45
|
+
label: segments.slice(1).join("/"),
|
|
46
|
+
rest,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** §19.5 D-M8 子标行首变换(文本/think):parseRelayPath 已消费完整前缀段——此处只
|
|
51
|
+
* 判行首(块首或上一内容以 \n 收尾——chunk 边界碎片在前缀段消费后自然无声剥除,
|
|
52
|
+
* 防 explore#1/ 字样泄漏);行首 → 前置字面子标 `explore#1 · `(渲染端套 dim)。 */
|
|
53
|
+
export function sublabelLine(sub, text, label) {
|
|
54
|
+
if (!label) return String(text)
|
|
55
|
+
const last = sub.blocks.at(-1)
|
|
56
|
+
const atLineHead = !last || String(last.text).endsWith("\n")
|
|
57
|
+
return atLineHead ? `${label} · ${text}` : String(text)
|
|
58
|
+
}
|
|
22
59
|
/** N2: per-child display-line ring buffer cap — oldest lines drop with a marker. */
|
|
23
60
|
export const SUB_BLOCK_LINE_LIMIT = 500
|
|
24
61
|
/** N1: render-layer throttle for child tool-output appends (generation relays verbatim). */
|
|
@@ -26,6 +63,28 @@ export const SUB_RELAY_THROTTLE_MS = 250
|
|
|
26
63
|
/** Roles a subagent tool child can take (finishSubTask matches the block's role). */
|
|
27
64
|
export const SUBAGENT_ROLES = ["sub", "explore", "plan", "coder", "eng-coder"]
|
|
28
65
|
|
|
66
|
+
// ─── §19.6 D-P1 面板镜像(subagent panel 检查工具)───
|
|
67
|
+
// TUI 装配处(index.mjs startTUI)state._agent = agent——块级状态变更点经
|
|
68
|
+
// syncPanelSnapshot 刷新 agent._panelSnapshot(区块数组快照:{key, role,
|
|
69
|
+
// status: running|done|awaitingDigest, startedAt}——状态变更即刷——O(n) 小——
|
|
70
|
+
// 不逐 token——NF-P)。agent 层 subagent action:"panel" 读此镜像(视图与用户
|
|
71
|
+
// 所见一致)+ 冻结门控(D-P3)。未挂载(headless/mock——无 state._agent)=
|
|
72
|
+
// 无镜像——no-op(降级路径由 agent 侧判定)。
|
|
73
|
+
|
|
74
|
+
/** 刷新面板镜像(调用点 = 本模块全部 state.subTasks 块级变更点末尾——map 已稳定)。 */
|
|
75
|
+
function syncPanelSnapshot(state) {
|
|
76
|
+
const agent = state._agent
|
|
77
|
+
if (!agent) return // 未挂载(headless/mock 无 TUI 装配)——无镜像
|
|
78
|
+
const snap = []
|
|
79
|
+
for (const sub of Object.values(state.subTasks ?? {})) {
|
|
80
|
+
// §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(镜像与面板
|
|
81
|
+
// 所见一致——action:'panel' 视图 + freeze 门控读此态)。
|
|
82
|
+
const status = sub.done ? (sub.awaitingDigest ? "awaitingDigest" : "done") : (sub.queued ? "queued" : "running")
|
|
83
|
+
snap.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
|
|
84
|
+
}
|
|
85
|
+
agent._panelSnapshot = snap
|
|
86
|
+
}
|
|
87
|
+
|
|
29
88
|
let _subRenderLast = 0
|
|
30
89
|
let _subRenderTimer = null
|
|
31
90
|
|
|
@@ -48,36 +107,40 @@ export function throttleSubRender(scheduleRender) {
|
|
|
48
107
|
_subRenderTimer.unref?.()
|
|
49
108
|
}
|
|
50
109
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Tombstone guard
|
|
54
|
-
// can relay tail tokens AFTER its block was frozen — ensureSubTask must not
|
|
55
|
-
// resurrect it (the recreated running block had no one left to freeze it and
|
|
56
|
-
// sat pinned above the input box until the next turn).
|
|
110
|
+
/** §19.5: key/role 分拆版核心(routeSub* 经 parseRelayPath;兼容 ensureSubTask(match))。 */
|
|
111
|
+
export function ensureSubTaskKey(state, key, role) {
|
|
112
|
+
// Tombstone guard:abort 子代理冻结后晚到 token 不得复活区块(2026-08-30 残项)
|
|
57
113
|
state._frozenSubKeys ??= new Set()
|
|
58
114
|
if (state._frozenSubKeys.has(key)) return null
|
|
59
115
|
state.subTasks ??= {}
|
|
60
116
|
if (!state.subTasks[key]) {
|
|
61
117
|
state.subTasks[key] = {
|
|
62
|
-
key, role
|
|
118
|
+
key, role, model: undefined, started: Date.now(), done: false, doneAt: null,
|
|
63
119
|
blocks: [], currentTool: null, toolArgs: null, turn: 0, maxTurns: 0, approval: null,
|
|
64
120
|
lastError: null, dropped: 0,
|
|
121
|
+
stopped: false, // §19.5: ⟦ev⟧stopped 冻结标记(标题 "stopped")
|
|
65
122
|
}
|
|
123
|
+
// §19.5 D-M7b ①(处置 #4 兜底——2026-09-03):缺失 key 的 ⟦ev⟧async 事件已缓冲
|
|
124
|
+
// pending 标志(routeSubToken——async 先于块创建到达:排队条目补位启动的时序窗口)
|
|
125
|
+
// ——块创建(此处)即应用——sub.async 区块创建即知(⏹ 门控/头标判定源保真)。
|
|
126
|
+
if (state._pendingAsyncKeys?.delete(key)) state.subTasks[key].async = true
|
|
127
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 块创建 → running 入镜
|
|
66
128
|
}
|
|
67
129
|
return state.subTasks[key]
|
|
68
130
|
}
|
|
69
131
|
|
|
132
|
+
export function ensureSubTask(state, subMatch) {
|
|
133
|
+
return ensureSubTaskKey(state, `${subMatch[1]}#${subMatch[2]}`, subMatch[1])
|
|
134
|
+
}
|
|
135
|
+
|
|
70
136
|
const countBlockLines = (text) => text.split("\n").length
|
|
71
137
|
|
|
72
|
-
/** Append
|
|
73
|
-
* even when the previous block has the same kind (used per tool call). */
|
|
138
|
+
/** Append(kind 合并追加)+ N2 环形上限;fresh=true 强制新块(每工具调用)。 */
|
|
74
139
|
export function appendSubBlock(sub, kind, text, { fresh = false } = {}) {
|
|
75
140
|
if (!text) return
|
|
76
141
|
const last = sub.blocks.at(-1)
|
|
77
142
|
if (!fresh && last && last.kind === kind) {
|
|
78
|
-
//
|
|
79
|
-
// every single-line chunk (each ends with \n → 2 segments) would inflate the
|
|
80
|
-
// incremental total far above the block's real line count (P1 修, 2026-08-30).
|
|
143
|
+
// 合并只算净增行数(逐 chunk 全量 split 会把单行碎片虚计成 2 段——P1, 2026-08-30)
|
|
81
144
|
const before = countBlockLines(last.text)
|
|
82
145
|
last.text += text
|
|
83
146
|
sub._lineCount = (sub._lineCount ?? 0) + countBlockLines(last.text) - before
|
|
@@ -89,13 +152,9 @@ export function appendSubBlock(sub, kind, text, { fresh = false } = {}) {
|
|
|
89
152
|
trimSubBlocks(sub)
|
|
90
153
|
}
|
|
91
154
|
|
|
92
|
-
/** N2
|
|
93
|
-
* lines and leave one cumulative "…(已省略 N 行)" marker block. Done blocks
|
|
94
|
-
* are bounded by the same cap (called from every append). */
|
|
155
|
+
/** N2:超限丢最旧行 + 留一条累计省略标记行(done 块同受约束)。 */
|
|
95
156
|
export function trimSubBlocks(sub) {
|
|
96
|
-
//
|
|
97
|
-
// sub._lineCount; the old implementation recomputed the total with a full
|
|
98
|
-
// blocks.reduce on EVERY append — O(n) per token, O(n²) over a stream.
|
|
157
|
+
// 增量行记账(P1):appendSubBlock 维护 _lineCount——旧实现每 chunk 全量 reduce
|
|
99
158
|
sub._lineCount = (sub._lineCount ?? 0)
|
|
100
159
|
let over = sub._lineCount - SUB_BLOCK_LINE_LIMIT
|
|
101
160
|
if (over <= 0) return
|
|
@@ -136,46 +195,70 @@ export function finishSubTask(state, roles, lastError = null) {
|
|
|
136
195
|
sub.approval = null
|
|
137
196
|
if (lastError) sub.lastError = lastError
|
|
138
197
|
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
198
|
+
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** §7.2.3 sync spawn 完成精确冻结(2026-09-03):按 relay key 精确 settle。finishSubTask
|
|
202
|
+
* 的"最早 started"启发式只在面板单 running 块时成立——§15 async 化后 async eng-coder
|
|
203
|
+
* (先启动)与 sync explore 并存时 explore 完成会误冻先启动的 eng-coder 块(7.2.3.1)。
|
|
204
|
+
* dispatch 沿 ctx._subagentKey(relayPrefix 去尾 = `role#N`)把 key 传进 onToolResult——
|
|
205
|
+
* 有 key 即精确标 done(冻结载体进流 + 删条目由调用方 freezeDoneSubTasks 承接——与
|
|
206
|
+
* finishSubTask 同一契约);无匹配块(已冻结 tombstone/不存在)返回 null——调用方不得
|
|
207
|
+
* 落启发式兜底(精确 key 无匹配 = 无从归属——不误冻他块)。 */
|
|
208
|
+
export function finishSubTaskKey(state, key, lastError = null) {
|
|
209
|
+
const sub = state.subTasks?.[key]
|
|
210
|
+
if (!sub) return null
|
|
211
|
+
sub.done = true
|
|
212
|
+
sub.doneAt = Date.now()
|
|
213
|
+
sub.currentTool = null
|
|
214
|
+
sub.approval = null
|
|
215
|
+
if (lastError) sub.lastError = lastError
|
|
216
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
217
|
+
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
218
|
+
return sub
|
|
139
219
|
}
|
|
140
220
|
|
|
141
|
-
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* stays an independent collapsible block — the folded form is a single header
|
|
147
|
-
* summary line, the expanded form re-renders the full activity timeline from the
|
|
148
|
-
* SAME block source. The payload travels as a JSON line flagged with
|
|
149
|
-
* _frozenSubTask; render-conversation.mjs recognizes it and renders the ▶/▼
|
|
150
|
-
* interaction keyed by `sub-${key}` (user ruled 2026-08-30: clickable after
|
|
151
|
-
* freezing — full design interaction, not a dim-lines fallback). subTasks loses
|
|
152
|
-
* the entry on release; memory stays bounded by N2 (ring buffer already applied). */
|
|
221
|
+
/** 冻结完成/中断区块进 state.lines(§7.2 D4——_frozenSubTask 载体行,渲染端
|
|
222
|
+
* render-conversation 识别;折叠交互 key = sub-${key} 与运行面板同源跨冻结延续)。
|
|
223
|
+
* 锚点插入(2026-09-03 修复轮):settled 块带 _freezeAt(settle 时刻流位置)——
|
|
224
|
+
* splice 落位使挂起期补发冻结块位于其 digest 总览文本之前;无锚点尾推不变;
|
|
225
|
+
* 多锚点批量冻结按降序(绝对位置 splice——先插小锚点会移走大锚点目标)。 */
|
|
153
226
|
export function freezeSubTaskLines(state, sub) {
|
|
154
227
|
if (!sub) return
|
|
155
228
|
state._frozenSubKeys ??= new Set()
|
|
156
229
|
state._frozenSubKeys.add(sub.key)
|
|
157
|
-
if (!sub) return
|
|
158
230
|
sub.done = true
|
|
159
231
|
sub.doneAt = sub.doneAt ?? Date.now()
|
|
160
|
-
|
|
232
|
+
const anchor = sub._freezeAt ?? state.lines.length
|
|
233
|
+
state.lines.splice(Math.min(anchor, state.lines.length), 0, {
|
|
234
|
+
text: `subagent activity: ${sub.key}`, color: C.dim, _frozenSubTask: sub,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** 头裁锚点校正(index.mjs pushLine 调用):裁 removedCount 补 1 标记行 = 净位移
|
|
239
|
+
* removedCount−1(code review round1 #3);在途锚点前移,min 0 兜底。 */
|
|
240
|
+
export function shiftFreezeAnchors(state, removedCount) {
|
|
241
|
+
const shift = removedCount - 1
|
|
242
|
+
for (const sub of Object.values(state.subTasks ?? {})) {
|
|
243
|
+
if (sub._freezeAt !== undefined) sub._freezeAt = Math.max(0, sub._freezeAt - shift)
|
|
244
|
+
}
|
|
161
245
|
}
|
|
162
246
|
|
|
163
|
-
/**
|
|
164
|
-
*
|
|
247
|
+
/** 冻结 + 释放全部已 done 块(工具结果清扫路径)。锚点降序(后 settle 先插——
|
|
248
|
+
* 绝对位置 splice 语义);无锚点(-1)排末尾推;sort 稳定。 */
|
|
165
249
|
export function freezeDoneSubTasks(state) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
250
|
+
const subs = Object.values(state.subTasks ?? {})
|
|
251
|
+
.filter((s) => s.done)
|
|
252
|
+
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
|
|
253
|
+
for (const sub of subs) {
|
|
254
|
+
freezeSubTaskLines(state, sub)
|
|
255
|
+
delete state.subTasks[sub.key]
|
|
171
256
|
}
|
|
257
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 删除后刷镜(块移出面板)
|
|
172
258
|
}
|
|
173
259
|
|
|
174
|
-
/**
|
|
175
|
-
*
|
|
176
|
-
* finishSubTask only ever settled the earliest one, leaving N-1 running
|
|
177
|
-
* ghosts pinned above the input box until turn end (consult residual,
|
|
178
|
-
* 2026-08-30 consult review — 4/4 models converged on this). */
|
|
260
|
+
/** 按角色整组标记 done——consult N 并行 children 的会话级 settle(单发
|
|
261
|
+
* finishSubTask 只结最早一个——consult 残项 2026-08-30)。 */
|
|
179
262
|
export function finishSubTasksByRole(state, roles, lastError = null) {
|
|
180
263
|
state.subTasks ??= {}
|
|
181
264
|
const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
|
|
@@ -189,17 +272,13 @@ export function finishSubTasksByRole(state, roles, lastError = null) {
|
|
|
189
272
|
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
190
273
|
}
|
|
191
274
|
}
|
|
275
|
+
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
192
276
|
}
|
|
193
277
|
|
|
194
|
-
/**
|
|
195
|
-
*
|
|
196
|
-
* running heuristic froze the WRONG block when models settle out of order. */
|
|
278
|
+
/** 按 [model] 记录的 model 精确 settle(consult_check 返回 provider:model——
|
|
279
|
+
* 乱序 settle 时最早启发式冻错块)。比对尾段(bare ↔ provider:model)。 */
|
|
197
280
|
export function finishSubTaskByModel(state, role, model, lastError = null) {
|
|
198
281
|
state.subTasks ??= {}
|
|
199
|
-
// Model-string normalization (2026-08-30 follow-up consult): the [model]
|
|
200
|
-
// token carries the BARE model name (resolveChildProvider keeps mname),
|
|
201
|
-
// while consult_check's r.model is consultLabel = "provider:model". Compare
|
|
202
|
-
// tail segments — a full "provider:model" reply matches a bare-name block.
|
|
203
282
|
const want = String(model ?? "").includes(":") ? String(model).split(":").pop() : String(model ?? "")
|
|
204
283
|
for (const sub of Object.values(state.subTasks)) {
|
|
205
284
|
const have = String(sub.model ?? "")
|
|
@@ -211,35 +290,58 @@ export function finishSubTaskByModel(state, role, model, lastError = null) {
|
|
|
211
290
|
sub.approval = null
|
|
212
291
|
if (lastError) sub.lastError = lastError
|
|
213
292
|
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
293
|
+
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
214
294
|
return sub
|
|
215
295
|
}
|
|
216
296
|
}
|
|
217
297
|
return null
|
|
218
298
|
}
|
|
219
299
|
|
|
220
|
-
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
300
|
+
/** 回合尾/挂起退出清扫(runAgentTurn finally / suspensionSession finally):冻结全部
|
|
301
|
+
* 剩余块——中断(Ctrl+C/错误)不留下钉住输入框的 ghost;未 done 者
|
|
302
|
+
* lastError="interrupted"(Ready 态跳过)。§17.5.5:挂起自然退出时本函数只兜底
|
|
303
|
+
* **未消化残项**(已消化块由 freezeReclaimDigestedBlocks 逐条先行回收——块回收与
|
|
304
|
+
* 池空解耦)。锚点降序同 freezeDoneSubTasks(挂起期 settle 锚点交错批次各按其
|
|
305
|
+
* settle 位置落位)。 */
|
|
224
306
|
export function freezeAllSubTasks(state) {
|
|
225
|
-
|
|
226
|
-
|
|
307
|
+
const subs = Object.values(state.subTasks ?? {})
|
|
308
|
+
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
|
|
309
|
+
for (const sub of subs) {
|
|
227
310
|
if (!sub.done) {
|
|
228
311
|
sub.done = true
|
|
229
312
|
sub.doneAt = Date.now()
|
|
230
313
|
if (!sub.lastError && state.status !== "Ready") sub.lastError = "interrupted"
|
|
231
314
|
}
|
|
232
315
|
freezeSubTaskLines(state, sub)
|
|
233
|
-
delete state.subTasks[key]
|
|
316
|
+
delete state.subTasks[sub.key]
|
|
234
317
|
}
|
|
318
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 冻结回收后刷镜(块移出面板)
|
|
235
319
|
}
|
|
236
320
|
|
|
237
|
-
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
|
|
321
|
+
/** §17.5.5 消化完成逐条冻结回收(2026-09-03 实测修订 + round1 #1 位置裁定):digest/会话内
|
|
322
|
+
* 用户回合消化完 pending 条目(run 首行已注入)后调用——把"已消化但仍驻留面板"的
|
|
323
|
+
* awaitingDigest 块立即冻结进流(不等池空——块回收与池空解耦;池空 freezeAllSubTasks
|
|
324
|
+
* 仅兜底未消化残项)。归属不变式:会话内任何 run 开始前 pinned 块的条目必在 pending
|
|
325
|
+
* (settle 即移交);run 消费后条目不在 pending 的 pinned 块 = 本 run 消化者——无需
|
|
326
|
+
* 快照即精确归属。位置(round1 #1 裁定——与 17.5.5 早版文本的矛盾已消解,见
|
|
327
|
+
* AGENT-LOOP.md §17.5.5):**settle 锚点 splice 落位——digest 总览文本之前**——同
|
|
328
|
+
* §7.2 D4 修复轮/D-S8 锚点语义(T-S6/T-S14 位置断言同口径)——锚点降序逐块冻结
|
|
329
|
+
* (splice 绝对位互不位移)。@returns {number} 回收块数 */
|
|
330
|
+
export function freezeReclaimDigestedBlocks(state, pendingList) {
|
|
331
|
+
const pend = pendingList ?? []
|
|
332
|
+
const targets = Object.values(state.subTasks ?? {})
|
|
333
|
+
.filter((s) => s.awaitingDigest && !pend.some((e) => `${e.role}#${e.id}` === s.key))
|
|
334
|
+
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1)) // 锚点降序(同 freezeAllSubTasks)
|
|
335
|
+
for (const sub of targets) {
|
|
336
|
+
freezeSubTaskLines(state, sub) // splice 落 settle 锚点(digest 总览文本之前)
|
|
337
|
+
delete state.subTasks[sub.key]
|
|
338
|
+
}
|
|
339
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 逐条回收后刷镜(块移出面板)
|
|
340
|
+
return targets.length
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** 事件 token → 区块头部(turn/approval 更新 turn n/max + 等待态)。事件永不进
|
|
344
|
+
* blocks/主流——仅头部。@returns {boolean} 良构事件(已消费) */
|
|
243
345
|
export function applySubEvent(sub, payload) {
|
|
244
346
|
const ev = payload.match(SUB_EVENT_RE)
|
|
245
347
|
if (!ev) return false
|
|
@@ -255,20 +357,123 @@ export function applySubEvent(sub, payload) {
|
|
|
255
357
|
return true
|
|
256
358
|
}
|
|
257
359
|
|
|
258
|
-
// ─── Prefix routing
|
|
259
|
-
// Each router returns true when the name/token carried a role#id/ prefix and was
|
|
260
|
-
// consumed into the child's block buffer; false = not a child item (main path).
|
|
360
|
+
// ─── Prefix routing(agent-turn callbacks 委派):true = 带前缀已消费;false = 主路径
|
|
261
361
|
|
|
262
|
-
/**
|
|
362
|
+
/** onToken 分支:子文本 / [model] 元数据 / ⟦ev⟧ 事件。§19.5 D-M8 嵌套:head 段路由块,
|
|
363
|
+
* 内层内容走子标行首变换,内层 ⟦ev⟧/[model] 剥除不路由(防外层块头污染——round1 #4)。 */
|
|
263
364
|
export function routeSubToken(state, t, scheduleRender) {
|
|
264
|
-
const
|
|
265
|
-
if (!
|
|
266
|
-
const
|
|
365
|
+
const path = parseRelayPath(t)
|
|
366
|
+
if (!path) return false
|
|
367
|
+
const payload = path.rest
|
|
368
|
+
const nested = path.inner.length > 0
|
|
369
|
+
// §19.5 D-M7b ① + 处置 #4(2026-09-03 评审 #4——兜底):零字段 async 标记在
|
|
370
|
+
// ensureSubTaskKey **之前**单独消费(async spawn 实际启动即发——sync 不发——精确
|
|
371
|
+
// 匹配:async 后须 RS 或串尾——正文伪前缀形态不误吞;内容侧伪哨兵已由生成侧
|
|
372
|
+
// stripEventToken 先行剥除——本分支只见真事件)。live 区块直接置位 sub.async;
|
|
373
|
+
// **缺失 key(区块尚未创建——排队条目补位启动的时序窗口 / tombstone)→ 缓冲
|
|
374
|
+
// pending 标志**(state._pendingAsyncKeys——ensureSubTaskKey 块创建时应用——async
|
|
375
|
+
// 事件先于块存在到达不丢——queued→running ⏹ 可见性保真——⏹ 门控与头标 async 判定源)。
|
|
376
|
+
// 内层(嵌套 explore#M/)async 剥除不路由(防污染外层块头)。
|
|
377
|
+
// §20 D-SD3b:queued 等待块实际启动(补位/释放)→ **转 running——同 key 不重建**
|
|
378
|
+
// ——清 waiting 标注(sub.queued)+ started 归零(elapsed 从实际启动计时——与池
|
|
379
|
+
// startedAt 语义一致——排队等待不计 elapsed)。
|
|
380
|
+
if (!nested && /^⟦ev⟧async(\x1e|$)/.test(payload)) {
|
|
381
|
+
const live = state.subTasks?.[path.head]
|
|
382
|
+
if (live) {
|
|
383
|
+
live.async = true
|
|
384
|
+
if (live.queued) {
|
|
385
|
+
delete live.queued
|
|
386
|
+
live.started = Date.now()
|
|
387
|
+
syncPanelSnapshot(state) // §19.6 D-P1: queued → running 状态变更刷镜
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
state._pendingAsyncKeys ??= new Set()
|
|
392
|
+
state._pendingAsyncKeys.add(path.head)
|
|
393
|
+
}
|
|
394
|
+
scheduleRender()
|
|
395
|
+
return true
|
|
396
|
+
}
|
|
397
|
+
// §20 D-SD3b(round2 #2——cancelled 事件):取消/出队 → 移除等待块(**不冻结**——
|
|
398
|
+
// 从未启动无活动可冻结;running 取消走 ⟦ev⟧stopped 冻结——两者通道分明)。守卫:
|
|
399
|
+
// 仅 !async 块(waiting 块未启动——async 置位 = running——running 的取消以 stopped
|
|
400
|
+
// 通道表达,cancelled 永不合法指向 running 块——迟到/失序事件不误删)。缺失块
|
|
401
|
+
// (headless 竞态/已移除)→ no-op。零字段解析同 async(不入 SUB_EVENT_RE)。
|
|
402
|
+
if (!nested && /^⟦ev⟧cancelled(\x1e|$)/.test(payload)) {
|
|
403
|
+
const live = state.subTasks?.[path.head]
|
|
404
|
+
if (live && !live.done && live.async !== true) {
|
|
405
|
+
delete state.subTasks[path.head]
|
|
406
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 移除后刷镜(块出面板)
|
|
407
|
+
}
|
|
408
|
+
scheduleRender()
|
|
409
|
+
return true
|
|
410
|
+
}
|
|
411
|
+
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
267
412
|
if (!sub) return true // frozen tombstone — late token from an aborted child: drop
|
|
268
|
-
|
|
269
|
-
//
|
|
270
|
-
//
|
|
413
|
+
// ⟦ev⟧:turn/approval 进度 → 仅头部(D1——不进 blocks/主流);done(§15 D-A3)=
|
|
414
|
+
// 完成即冻结(settle 时发);settled(§17 D-S8)= 挂起期完成——驻留面板中间态
|
|
415
|
+
// "done · awaiting digestion",池空补发冻结;stopped(§19.5)= cancel——立即冻结;
|
|
416
|
+
// queued(§20 D-SD3b)= 排队 spawn 等待块(spawn 返回即建/状态变迁刷新——覆盖式
|
|
417
|
+
// 更新 sub.queued——块不可展开(无活动流);启动后 async 事件清标转 running)。
|
|
271
418
|
if (payload.startsWith("⟦ev⟧")) {
|
|
419
|
+
if (nested) return true // 内层事件剥除不路由(防 explore 进度污染外层块头)
|
|
420
|
+
const ev = payload.match(SUB_EVENT_RE)
|
|
421
|
+
if (ev?.[1] === "queued") {
|
|
422
|
+
// 防御:已启动块(async 标记已置)收到迟到的 queued 刷新 → 丢弃(事件序保证
|
|
423
|
+
// queued 恒先于 async——迟到即陈旧——不复活 waiting 标注盖住 running 态)。
|
|
424
|
+
if (sub.async === true) return true
|
|
425
|
+
sub.queued = {
|
|
426
|
+
kind: ev[2] === "wait" || ev[2] === "depc" ? ev[2] : "slot",
|
|
427
|
+
position: Number(ev[3]) > 0 ? Number(ev[3]) : undefined,
|
|
428
|
+
detail: ev[5] ?? "",
|
|
429
|
+
}
|
|
430
|
+
sub.currentTool = null
|
|
431
|
+
sub.approval = null
|
|
432
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
433
|
+
syncPanelSnapshot(state) // §19.6 D-P1: waiting 块建/刷新刷镜
|
|
434
|
+
scheduleRender()
|
|
435
|
+
return true
|
|
436
|
+
}
|
|
437
|
+
if (ev?.[1] === "settled") {
|
|
438
|
+
sub.done = true
|
|
439
|
+
sub.doneAt = Date.now()
|
|
440
|
+
sub.awaitingDigest = true
|
|
441
|
+
// 冻结锚点(2026-09-03 修复轮):settle 时刻流位置——§17.5.5 digest 完成逐条回收
|
|
442
|
+
// (freezeReclaimDigestedBlocks)与未消化残项的池空退出兜底(freezeAllSubTasks)
|
|
443
|
+
// 都按它 splice 落位——digest 总览文本之前(round1 #1 裁定——T-S6/T-S14 同口径)。
|
|
444
|
+
sub._freezeAt = state.lines?.length ?? 0
|
|
445
|
+
sub.currentTool = null
|
|
446
|
+
sub.approval = null
|
|
447
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
448
|
+
syncPanelSnapshot(state) // §19.6 D-P1: settled → awaitingDigest 状态变更刷镜
|
|
449
|
+
scheduleRender()
|
|
450
|
+
return true
|
|
451
|
+
}
|
|
452
|
+
if (ev?.[1] === "done") {
|
|
453
|
+
sub.done = true
|
|
454
|
+
sub.doneAt = Date.now()
|
|
455
|
+
sub.currentTool = null
|
|
456
|
+
sub.approval = null
|
|
457
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
458
|
+
freezeSubTaskLines(state, sub)
|
|
459
|
+
delete state.subTasks[sub.key]
|
|
460
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 冻结删除后刷镜
|
|
461
|
+
scheduleRender()
|
|
462
|
+
return true
|
|
463
|
+
}
|
|
464
|
+
if (ev?.[1] === "stopped") {
|
|
465
|
+
sub.done = true
|
|
466
|
+
sub.doneAt = Date.now()
|
|
467
|
+
sub.stopped = true
|
|
468
|
+
sub.currentTool = null
|
|
469
|
+
sub.approval = null
|
|
470
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
471
|
+
freezeSubTaskLines(state, sub)
|
|
472
|
+
delete state.subTasks[sub.key]
|
|
473
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 冻结删除后刷镜
|
|
474
|
+
scheduleRender()
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
272
477
|
applySubEvent(sub, payload)
|
|
273
478
|
scheduleRender()
|
|
274
479
|
return true
|
|
@@ -277,55 +482,143 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
277
482
|
// parent's) — shown in the block header, NOT appended to its content stream.
|
|
278
483
|
// Only treat as metadata when the model isn't set yet (it's always the FIRST token);
|
|
279
484
|
// a child content token that happens to start with "[model]" must not be swallowed.
|
|
280
|
-
if (payload.startsWith("[model]") && sub.model === undefined) {
|
|
281
|
-
|
|
282
|
-
|
|
485
|
+
if (payload.startsWith("[model]") && (nested || sub.model === undefined)) {
|
|
486
|
+
if (!nested) {
|
|
487
|
+
sub.model = payload.slice(7)
|
|
488
|
+
scheduleRender()
|
|
489
|
+
} // 内层 [model] 剥除不路由(防嵌套块头污染)——单层 model 已设时 [model] 开头视为内容
|
|
283
490
|
return true
|
|
284
491
|
}
|
|
285
492
|
// Child LLM text → text block (N2 cap inside appendSubBlock).
|
|
286
|
-
appendSubBlock(sub, "text", payload)
|
|
493
|
+
appendSubBlock(sub, "text", nested ? sublabelLine(sub, payload, path.label) : payload)
|
|
287
494
|
scheduleRender()
|
|
288
495
|
return true
|
|
289
496
|
}
|
|
290
497
|
|
|
291
|
-
/** Child reasoning token → think block (F2: same treatment as main reasoning).
|
|
498
|
+
/** Child reasoning token → think block (F2: same treatment as main reasoning).
|
|
499
|
+
* §19.5 D-M8 nested think: 同文本行规则——行首 dim 子标后接思考行。 */
|
|
292
500
|
export function routeSubReasoning(state, t, scheduleRender) {
|
|
293
|
-
const
|
|
294
|
-
if (!
|
|
295
|
-
const sub =
|
|
501
|
+
const path = parseRelayPath(t)
|
|
502
|
+
if (!path) return false
|
|
503
|
+
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
296
504
|
if (!sub) return true // frozen tombstone — drop late token
|
|
297
|
-
|
|
505
|
+
const nested = path.inner.length > 0
|
|
506
|
+
appendSubBlock(sub, "think", nested ? sublabelLine(sub, path.rest, path.label) : path.rest)
|
|
298
507
|
scheduleRender()
|
|
299
508
|
return true
|
|
300
509
|
}
|
|
301
510
|
|
|
302
|
-
/** Child tool call → fresh tool block + header currentTool.
|
|
511
|
+
/** Child tool call → fresh tool block + header currentTool。§19.5 D-M8 嵌套:
|
|
512
|
+
* `explore#1/read` → dim 子标 + 既有工具行形态;currentTool 存全路径
|
|
513
|
+
* (`explore#1/read`——输出归属判别 + 折叠头可辨嵌套)。 */
|
|
303
514
|
export function routeSubToolCall(state, name, args, scheduleRender) {
|
|
304
|
-
const
|
|
305
|
-
if (!
|
|
306
|
-
const sub =
|
|
515
|
+
const path = parseRelayPath(name)
|
|
516
|
+
if (!path) return false
|
|
517
|
+
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
307
518
|
if (!sub) return true // frozen tombstone — drop late token
|
|
308
|
-
|
|
519
|
+
const nested = path.inner.length > 0
|
|
520
|
+
sub.currentTool = nested ? `${path.label}/${path.rest}` : path.rest
|
|
309
521
|
sub.toolArgs = args
|
|
310
522
|
sub.approval = null
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const argsDesc = describeToolArgs(sub.currentTool, args)
|
|
314
|
-
appendSubBlock(sub, "tool", `❯ ${sub.currentTool}${argsDesc ? " " + argsDesc : ""}\n`, { fresh: true })
|
|
523
|
+
const argsDesc = describeToolArgs(path.rest, args)
|
|
524
|
+
appendSubBlock(sub, "tool", `${nested ? `${path.label} · ` : ""}❯ ${path.rest}${argsDesc ? " " + argsDesc : ""}\n`, { fresh: true })
|
|
315
525
|
scheduleRender()
|
|
316
526
|
return true
|
|
317
527
|
}
|
|
318
528
|
|
|
319
|
-
/** Child tool output
|
|
320
|
-
*
|
|
529
|
+
/** Child tool output(D1 前缀 name relay)→ 追加当前 tool block。RAW 拼接(2026-09-03
|
|
530
|
+
* 修复轮——relay chunk 是任意字节边界碎片,逐 chunk 补 \n 会把词拦腰断行 + 烧 N2
|
|
531
|
+
* 配额;emit 端自带换行结构无损还原)。§19.5 D-M8 嵌套:输出跟随最近工具行(不重复
|
|
532
|
+
* 前缀——块内顺序天然归属);fresh 判别用全路径——防内外同名工具串块。 */
|
|
321
533
|
export function routeSubToolOutput(state, name, part, scheduleRender) {
|
|
322
|
-
const
|
|
323
|
-
if (!
|
|
324
|
-
const sub =
|
|
534
|
+
const path = parseRelayPath(name)
|
|
535
|
+
if (!path) return false
|
|
536
|
+
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
325
537
|
if (!sub) return true // frozen tombstone — drop late token
|
|
326
|
-
const toolName =
|
|
327
|
-
appendSubBlock(sub, "tool", part.text
|
|
538
|
+
const toolName = path.inner.length > 0 ? `${path.label}/${path.rest}` : path.rest
|
|
539
|
+
appendSubBlock(sub, "tool", part.text, { fresh: sub.currentTool !== toolName })
|
|
328
540
|
if (sub.currentTool !== toolName) sub.currentTool = toolName
|
|
329
541
|
throttleSubRender(scheduleRender)
|
|
330
542
|
return true
|
|
331
543
|
}
|
|
544
|
+
|
|
545
|
+
// ─── Compression panel(CONTEXT-COMPACTION.md §7 D-C2):压缩会话复用子agent 区块机制——
|
|
546
|
+
// 运行期 role "compress" 条目驻留面板(状态/阶段/耗时——正文永不进面板),完成/降级
|
|
547
|
+
// 后 freezeSubTaskLines 冻结进流。
|
|
548
|
+
|
|
549
|
+
let _compressSeq = 0
|
|
550
|
+
|
|
551
|
+
/** Live compression panel (role "compress", not done) or null. */
|
|
552
|
+
function liveCompressPanel(state) {
|
|
553
|
+
return Object.values(state.subTasks ?? {}).find((s) => s.role === "compress" && !s.done) ?? null
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** 打开(或重试复位)压缩面板——onCompressStart 于摘要调用前触发。每次尝试重置
|
|
557
|
+
* started(elapsed 归零),失败行留在时间线(重试历史可见)。 */
|
|
558
|
+
export function ensureCompressPanel(state, info = {}) {
|
|
559
|
+
state.subTasks ??= {}
|
|
560
|
+
let panel = liveCompressPanel(state)
|
|
561
|
+
if (!panel) {
|
|
562
|
+
panel = {
|
|
563
|
+
key: `compress#${++_compressSeq}`,
|
|
564
|
+
role: "compress",
|
|
565
|
+
model: undefined,
|
|
566
|
+
started: Date.now(), done: false, doneAt: null,
|
|
567
|
+
blocks: [], currentTool: null, toolArgs: null,
|
|
568
|
+
turn: 0, maxTurns: 0, approval: null,
|
|
569
|
+
lastError: null, dropped: 0,
|
|
570
|
+
}
|
|
571
|
+
state.subTasks[panel.key] = panel
|
|
572
|
+
}
|
|
573
|
+
panel.done = false
|
|
574
|
+
panel.doneAt = null
|
|
575
|
+
panel.lastError = null
|
|
576
|
+
panel.started = Date.now()
|
|
577
|
+
panel.currentTool = "compressing context…"
|
|
578
|
+
const messages = Number.isInteger(info.messages) && info.messages >= 0 ? info.messages : "?"
|
|
579
|
+
appendSubBlock(panel, "status", "Compressing context…\n", { fresh: true })
|
|
580
|
+
appendSubBlock(panel, "meta", `summarizing ${messages} messages\n`, { fresh: true })
|
|
581
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 压缩面板建/重置刷镜
|
|
582
|
+
return panel
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** 失败态(onCompressFail):只记错误文本——降级说明与 3 连败绑定(D-C2)。 */
|
|
586
|
+
export function markCompressFailed(state, error) {
|
|
587
|
+
const panel = liveCompressPanel(state)
|
|
588
|
+
if (!panel) return
|
|
589
|
+
const text = error?.message ? String(error.message) : String(error ?? "unknown error")
|
|
590
|
+
panel.lastError = text
|
|
591
|
+
appendSubBlock(panel, "err", `Compression failed: ${text}\n`, { fresh: true })
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** 冻结压缩面板进流(同完成子代理的载体/折叠 key)+ 释放 live 条目。 */
|
|
595
|
+
function freezeCompressPanel(state, panel) {
|
|
596
|
+
freezeSubTaskLines(state, panel)
|
|
597
|
+
delete state.subTasks[panel.key]
|
|
598
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 压缩面板冻结删除后刷镜
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** 完成态(onCompress):"Compressed: N tokens freed → summary (Xs)"——冻结可折叠。 */
|
|
602
|
+
export function markCompressDone(state, info = {}) {
|
|
603
|
+
const panel = liveCompressPanel(state)
|
|
604
|
+
if (!panel) return
|
|
605
|
+
const tokensFreed = Number.isFinite(info.tokensFreed) ? Math.max(0, Math.round(info.tokensFreed)) : 0
|
|
606
|
+
const seconds = Number.isFinite(info.elapsedMs) ? Math.max(0, Math.round(info.elapsedMs / 1000)) : 0
|
|
607
|
+
panel.done = true
|
|
608
|
+
panel.doneAt = Date.now()
|
|
609
|
+
panel.currentTool = null
|
|
610
|
+
appendSubBlock(panel, "status", `Compressed: ${tokensFreed} tokens freed → summary (${seconds}s)\n`, { fresh: true })
|
|
611
|
+
freezeCompressPanel(state, panel)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** 降级态(onCompress mode:"fallback")——3 连败后 "truncated to N messages"。 */
|
|
615
|
+
export function markCompressFallback(state, info = {}) {
|
|
616
|
+
const panel = liveCompressPanel(state)
|
|
617
|
+
if (!panel) return
|
|
618
|
+
const tailMessages = Number.isFinite(info.tailMessages) ? Math.round(info.tailMessages) : "?"
|
|
619
|
+
panel.done = true
|
|
620
|
+
panel.doneAt = Date.now()
|
|
621
|
+
panel.currentTool = null
|
|
622
|
+
appendSubBlock(panel, "err", `Compression failed — fallback: truncated to ${tailMessages} messages\n`, { fresh: true })
|
|
623
|
+
freezeCompressPanel(state, panel)
|
|
624
|
+
}
|