thincoder 0.12.58 → 0.12.60
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 +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- 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 +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -1,29 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* subagent-blocks.mjs — 子agent 活动区块缓冲(AGENT-LOOP.md §7.2 D4,消费端)。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* dropped, blockEpoch
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* N1
|
|
11
|
-
*
|
|
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
|
+
* children: [](R23——嵌套子代理子块载体——见 subagent-children.mjs)}。
|
|
8
|
+
* 职责:前缀路由(parseRelayPath——**R23:嵌套段不再子标渲染——建子块载体归属**)、
|
|
9
|
+
* 事件 token 解析(**R23:内层 ⟦ev⟧done/stopped 生成侧补发射(D-R23c1)路由到子块
|
|
10
|
+
* 定格;§27.1 F2:done 后完全定格——迟到 chunk 丢弃**)、kind 合并、N2 树级环形上限(R23 NFR——子块计入外层配额)、N1 渲染节流、
|
|
11
|
+
* 完成冻结(freezeSubTaskLines 家族——锚点 splice)。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { C } from "./ansi.mjs"
|
|
15
15
|
import { describeToolArgs } from "./tool-args.mjs"
|
|
16
|
+
import {
|
|
17
|
+
SUB_BLOCK_LINE_LIMIT, appendSubBlock, appendSubChild, ensureSubChild, descendSubChild,
|
|
18
|
+
closeSubChild,
|
|
19
|
+
} from "./subagent-children.mjs"
|
|
20
|
+
export { SUB_BLOCK_LINE_LIMIT, appendSubBlock } from "./subagent-children.mjs"
|
|
21
|
+
// 2026-09-05 module-split:finish/freeze 族 + §19.6 面板镜像迁 subagent-freeze.mjs——
|
|
22
|
+
// routeSub*/compression panel 内部用本地 import;re-export 保外部 import 面(index.mjs 等)
|
|
23
|
+
import { freezeSubTaskLines, syncPanelSnapshot, finishSubTaskKey, finishSubTask, freezeDoneSubTasks, finishSubTasksByRole, freezeAllSubTasks, freezeReclaimDigestedBlocks, shiftFreezeAnchors } from "./subagent-freeze.mjs"
|
|
24
|
+
export { syncPanelSnapshot, finishSubTask, finishSubTaskKey, freezeSubTaskLines, shiftFreezeAnchors, freezeDoneSubTasks, finishSubTasksByRole, freezeAllSubTasks, freezeReclaimDigestedBlocks } from "./subagent-freeze.mjs"
|
|
16
25
|
|
|
17
26
|
/** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
|
|
18
27
|
export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
|
|
19
|
-
/** ⟦ev⟧
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
/** ⟦ev⟧ token parser:`⟦ev⟧<name>\x1e<n>\x1e<max>\x1e<phase>\x1e<detail>`。phase done
|
|
29
|
+
* = async 完成即冻结(settle 时发);settled = 挂起期完成——冻结延迟至 digest 消化
|
|
30
|
+
* 完成(§17.5.5 freezeReclaimDigestedBlocks 逐条回收——不等池空)或池空退出兜底补发;
|
|
31
|
+
* stopped(§19.5 D-M6)= cancel 中止——interrupted 语义立即冻结(标题 "stopped");
|
|
32
|
+
* queued(§20 D-SD3b)= 排队 spawn 返回即建 waiting 块(round2 #2 事件通道——
|
|
33
|
+
* `⟦ev⟧queued\x1e<kind>\x1e<position>\x1equeued\x1e<detail>`——kind slot/wait/depc)——
|
|
34
|
+
* 启动后 ⟦ev⟧async 转 running(同 key 不重建);cancelled(§20)= 出队/取消——**零字段**
|
|
35
|
+
* 移除块(不冻结——无活动可冻结)——与 async 同型单独解析(不入本正则);
|
|
36
|
+
* async(§19.5 D-M7b + 处置 #4)= **零字段**标记(`⟦ev⟧async\x1e`——无 n/max/phase/detail 段)——
|
|
37
|
+
* routeSubToken 单独解析设 sub.async = true(不入本正则——本正则要求 4 字段);**缺失 key
|
|
38
|
+
* (块尚未创建)缓冲 `state._pendingAsyncKeys`——ensureSubTaskKey 块创建时应用(兜底)。 */
|
|
39
|
+
export const SUB_EVENT_RE = /^⟦ev⟧(turn|approval|done|settled|stopped|queued)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e?([\s\S]*)$/
|
|
40
|
+
|
|
41
|
+
/** §19.5 D-M8 嵌套 relay 前缀通用解析(循环解析任意深度——R23 保持为路由源):
|
|
42
|
+
* `eng-coder#2/explore#1/read` → { head(块路由), inner[], label(inner 链——R23:
|
|
43
|
+
* 子块折叠键/外层 currentTool 全路径用), rest }。
|
|
44
|
+
* 单层 = inner[]/label ""——与既有单段匹配语义零改;无前缀 → null。 */
|
|
45
|
+
export function parseRelayPath(text) {
|
|
46
|
+
const segments = []
|
|
47
|
+
let rest = String(text)
|
|
48
|
+
for (;;) {
|
|
49
|
+
const m = rest.match(SUB_PREFIX_RE)
|
|
50
|
+
if (!m) break
|
|
51
|
+
segments.push(`${m[1]}#${m[2]}`)
|
|
52
|
+
rest = rest.slice(m[0].length)
|
|
53
|
+
}
|
|
54
|
+
if (segments.length === 0) return null
|
|
55
|
+
return {
|
|
56
|
+
head: segments[0],
|
|
57
|
+
inner: segments.slice(1),
|
|
58
|
+
label: segments.slice(1).join("/"),
|
|
59
|
+
rest,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
27
63
|
/** N1: render-layer throttle for child tool-output appends (generation relays verbatim). */
|
|
28
64
|
export const SUB_RELAY_THROTTLE_MS = 250
|
|
29
65
|
/** Roles a subagent tool child can take (finishSubTask matches the block's role). */
|
|
@@ -51,198 +87,38 @@ export function throttleSubRender(scheduleRender) {
|
|
|
51
87
|
_subRenderTimer.unref?.()
|
|
52
88
|
}
|
|
53
89
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Tombstone guard
|
|
57
|
-
// can relay tail tokens AFTER its block was frozen — ensureSubTask must not
|
|
58
|
-
// resurrect it (the recreated running block had no one left to freeze it and
|
|
59
|
-
// sat pinned above the input box until the next turn).
|
|
90
|
+
/** §19.5: key/role 分拆版核心(routeSub* 经 parseRelayPath;兼容 ensureSubTask(match))。 */
|
|
91
|
+
export function ensureSubTaskKey(state, key, role) {
|
|
92
|
+
// Tombstone guard:abort 子代理冻结后晚到 token 不得复活区块(2026-08-30 残项)
|
|
60
93
|
state._frozenSubKeys ??= new Set()
|
|
61
94
|
if (state._frozenSubKeys.has(key)) return null
|
|
62
95
|
state.subTasks ??= {}
|
|
63
96
|
if (!state.subTasks[key]) {
|
|
64
97
|
state.subTasks[key] = {
|
|
65
|
-
key, role
|
|
98
|
+
key, role, model: undefined, started: Date.now(), done: false, doneAt: null,
|
|
66
99
|
blocks: [], currentTool: null, toolArgs: null, turn: 0, maxTurns: 0, approval: null,
|
|
67
100
|
lastError: null, dropped: 0,
|
|
101
|
+
stopped: false, // §19.5: ⟦ev⟧stopped 冻结标记(标题 "stopped")
|
|
102
|
+
children: [], // R23: 嵌套子代理子块载体(subagent-children.mjs——D-R23a)
|
|
68
103
|
}
|
|
104
|
+
// §19.5 D-M7b ①(处置 #4 兜底——2026-09-03):缺失 key 的 ⟦ev⟧async 事件已缓冲
|
|
105
|
+
// pending 标志(routeSubToken——async 先于块创建到达:排队条目补位启动的时序窗口)
|
|
106
|
+
// ——块创建(此处)即应用——sub.async 区块创建即知(⏹ 门控/头标判定源保真)。
|
|
107
|
+
if (state._pendingAsyncKeys?.delete(key)) state.subTasks[key].async = true
|
|
108
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 块创建 → running 入镜
|
|
69
109
|
}
|
|
70
110
|
return state.subTasks[key]
|
|
71
111
|
}
|
|
72
112
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/** Append (kind-merging) with the N2 ring-buffer cap. fresh=true starts a new block
|
|
76
|
-
* even when the previous block has the same kind (used per tool call). */
|
|
77
|
-
export function appendSubBlock(sub, kind, text, { fresh = false } = {}) {
|
|
78
|
-
if (!text) return
|
|
79
|
-
const last = sub.blocks.at(-1)
|
|
80
|
-
if (!fresh && last && last.kind === kind) {
|
|
81
|
-
// Merged append: account only the NET new lines. Counting the full split of
|
|
82
|
-
// every single-line chunk (each ends with \n → 2 segments) would inflate the
|
|
83
|
-
// incremental total far above the block's real line count (P1 修, 2026-08-30).
|
|
84
|
-
const before = countBlockLines(last.text)
|
|
85
|
-
last.text += text
|
|
86
|
-
sub._lineCount = (sub._lineCount ?? 0) + countBlockLines(last.text) - before
|
|
87
|
-
} else {
|
|
88
|
-
sub.blocks.push({ kind, text })
|
|
89
|
-
sub._lineCount = (sub._lineCount ?? 0) + countBlockLines(text)
|
|
90
|
-
}
|
|
91
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
92
|
-
trimSubBlocks(sub)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** N2: keep at most SUB_BLOCK_LINE_LIMIT display lines per child; drop oldest
|
|
96
|
-
* lines and leave one cumulative "…(已省略 N 行)" marker block. Done blocks
|
|
97
|
-
* are bounded by the same cap (called from every append). */
|
|
98
|
-
export function trimSubBlocks(sub) {
|
|
99
|
-
// Incremental line accounting (P1, 2026-08-30): appendSubBlock keeps
|
|
100
|
-
// sub._lineCount; the old implementation recomputed the total with a full
|
|
101
|
-
// blocks.reduce on EVERY append — O(n) per token, O(n²) over a stream.
|
|
102
|
-
sub._lineCount = (sub._lineCount ?? 0)
|
|
103
|
-
let over = sub._lineCount - SUB_BLOCK_LINE_LIMIT
|
|
104
|
-
if (over <= 0) return
|
|
105
|
-
let droppedNow = 0
|
|
106
|
-
while (over > 0 && sub.blocks.length > 0) {
|
|
107
|
-
const first = sub.blocks[0]
|
|
108
|
-
const lines = countBlockLines(first.text)
|
|
109
|
-
const take = Math.min(lines, over)
|
|
110
|
-
if (take >= lines) {
|
|
111
|
-
sub.blocks.shift()
|
|
112
|
-
droppedNow += lines
|
|
113
|
-
} else {
|
|
114
|
-
first.text = first.text.split("\n").slice(take).join("\n")
|
|
115
|
-
droppedNow += take
|
|
116
|
-
}
|
|
117
|
-
over -= take
|
|
118
|
-
}
|
|
119
|
-
sub._lineCount -= droppedNow
|
|
120
|
-
sub.dropped += droppedNow
|
|
121
|
-
const marker = `…(已省略 ${sub.dropped} 行)`
|
|
122
|
-
const first = sub.blocks[0]
|
|
123
|
-
if (first && first.kind === "meta") first.text = marker
|
|
124
|
-
else { sub.blocks.unshift({ kind: "meta", text: marker }); sub._lineCount += 1 }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Mark the earliest running child of the given role(s) as done (✓ header, frozen elapsed). */
|
|
128
|
-
export function finishSubTask(state, roles, lastError = null) {
|
|
129
|
-
state.subTasks ??= {}
|
|
130
|
-
const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
|
|
131
|
-
const running = Object.values(state.subTasks)
|
|
132
|
-
.filter((s) => !s.done && roleSet.has(s.role))
|
|
133
|
-
.sort((a, b) => a.started - b.started)
|
|
134
|
-
if (running.length === 0) return
|
|
135
|
-
const sub = running[0]
|
|
136
|
-
sub.done = true
|
|
137
|
-
sub.doneAt = Date.now()
|
|
138
|
-
sub.currentTool = null
|
|
139
|
-
sub.approval = null
|
|
140
|
-
if (lastError) sub.lastError = lastError
|
|
141
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Freeze a finished (or interrupted) child's activity block into state.lines
|
|
145
|
-
* (§7.2 D4 — the block is the child activity's only carrier; moved here from
|
|
146
|
-
* agent-turn.mjs 2026-08-30). Rendering it as a pinned conversation-tail section
|
|
147
|
-
* made every ✓ block a permanent "ghost" stuck above the input box (user report
|
|
148
|
-
* 2026-08-30); frozen into the stream it scrolls away with the conversation AND
|
|
149
|
-
* stays an independent collapsible block — the folded form is a single header
|
|
150
|
-
* summary line, the expanded form re-renders the full activity timeline from the
|
|
151
|
-
* SAME block source. The payload travels as a JSON line flagged with
|
|
152
|
-
* _frozenSubTask; render-conversation.mjs recognizes it and renders the ▶/▼
|
|
153
|
-
* interaction keyed by `sub-${key}` (user ruled 2026-08-30: clickable after
|
|
154
|
-
* freezing — full design interaction, not a dim-lines fallback). subTasks loses
|
|
155
|
-
* the entry on release; memory stays bounded by N2 (ring buffer already applied). */
|
|
156
|
-
export function freezeSubTaskLines(state, sub) {
|
|
157
|
-
if (!sub) return
|
|
158
|
-
state._frozenSubKeys ??= new Set()
|
|
159
|
-
state._frozenSubKeys.add(sub.key)
|
|
160
|
-
if (!sub) return
|
|
161
|
-
sub.done = true
|
|
162
|
-
sub.doneAt = sub.doneAt ?? Date.now()
|
|
163
|
-
state.lines.push({ text: `subagent activity: ${sub.key}`, color: C.dim, _frozenSubTask: sub })
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Freeze + release every already-done child block (tool-result sweep:
|
|
167
|
-
* subagent/escalate/consult-check completions each call this after finishSubTask). */
|
|
168
|
-
export function freezeDoneSubTasks(state) {
|
|
169
|
-
for (const key of Object.keys(state.subTasks ?? {})) {
|
|
170
|
-
if (state.subTasks[key].done) {
|
|
171
|
-
freezeSubTaskLines(state, state.subTasks[key])
|
|
172
|
-
delete state.subTasks[key]
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Mark ALL running blocks of the given role(s) done — session-level settle.
|
|
178
|
-
* A consult spawns N parallel children (consult#1..#N); the single-shot
|
|
179
|
-
* finishSubTask only ever settled the earliest one, leaving N-1 running
|
|
180
|
-
* ghosts pinned above the input box until turn end (consult residual,
|
|
181
|
-
* 2026-08-30 consult review — 4/4 models converged on this). */
|
|
182
|
-
export function finishSubTasksByRole(state, roles, lastError = null) {
|
|
183
|
-
state.subTasks ??= {}
|
|
184
|
-
const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
|
|
185
|
-
for (const sub of Object.values(state.subTasks)) {
|
|
186
|
-
if (!sub.done && roleSet.has(sub.role)) {
|
|
187
|
-
sub.done = true
|
|
188
|
-
sub.doneAt = Date.now()
|
|
189
|
-
sub.currentTool = null
|
|
190
|
-
sub.approval = null
|
|
191
|
-
if (lastError) sub.lastError = lastError
|
|
192
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Precise settle: mark the child of `role` whose [model] token recorded
|
|
198
|
-
* `model` as done. consult_check returns the reply's model — the earliest-
|
|
199
|
-
* running heuristic froze the WRONG block when models settle out of order. */
|
|
200
|
-
export function finishSubTaskByModel(state, role, model, lastError = null) {
|
|
201
|
-
state.subTasks ??= {}
|
|
202
|
-
// Model-string normalization (2026-08-30 follow-up consult): the [model]
|
|
203
|
-
// token carries the BARE model name (resolveChildProvider keeps mname),
|
|
204
|
-
// while consult_check's r.model is consultLabel = "provider:model". Compare
|
|
205
|
-
// tail segments — a full "provider:model" reply matches a bare-name block.
|
|
206
|
-
const want = String(model ?? "").includes(":") ? String(model).split(":").pop() : String(model ?? "")
|
|
207
|
-
for (const sub of Object.values(state.subTasks)) {
|
|
208
|
-
const have = String(sub.model ?? "")
|
|
209
|
-
const haveTail = have.includes(":") ? have.split(":").pop() : have
|
|
210
|
-
if (!sub.done && sub.role === role && haveTail === want) {
|
|
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
|
-
return sub
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return null
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Turn-end sweep (runAgentTurn finally): freeze ALL remaining blocks — interrupted
|
|
224
|
-
* runs (Ctrl+C abort / error mid-turn) would otherwise linger as pinned ghosts
|
|
225
|
-
* above the input box. Not-done children get done + lastError="interrupted"
|
|
226
|
-
* (skipped when the status already recovered to "Ready"). */
|
|
227
|
-
export function freezeAllSubTasks(state) {
|
|
228
|
-
for (const key of Object.keys(state.subTasks ?? {})) {
|
|
229
|
-
const sub = state.subTasks[key]
|
|
230
|
-
if (!sub.done) {
|
|
231
|
-
sub.done = true
|
|
232
|
-
sub.doneAt = Date.now()
|
|
233
|
-
if (!sub.lastError && state.status !== "Ready") sub.lastError = "interrupted"
|
|
234
|
-
}
|
|
235
|
-
freezeSubTaskLines(state, sub)
|
|
236
|
-
delete state.subTasks[key]
|
|
237
|
-
}
|
|
113
|
+
export function ensureSubTask(state, subMatch) {
|
|
114
|
+
return ensureSubTaskKey(state, `${subMatch[1]}#${subMatch[2]}`, subMatch[1])
|
|
238
115
|
}
|
|
239
116
|
|
|
240
|
-
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
|
|
244
|
-
*
|
|
245
|
-
*/
|
|
117
|
+
/** Append(kind 合并追加)+ N2 树级环形上限;fresh=true 强制新块(每工具调用)。
|
|
118
|
+
* R23(2026-09-07):appendSubBlock/trimSubBlocks 实现迁 subagent-children.mjs(树级
|
|
119
|
+
* 配额——子块计入外层 500 行环)——本文件 import + re-export 保 import 面(压缩面板等)。 */
|
|
120
|
+
/** 事件 token → 区块头部(turn/approval 更新 turn n/max + 等待态)。事件永不进
|
|
121
|
+
* blocks/主流——仅头部。@returns {boolean} 良构事件(已消费) */
|
|
246
122
|
export function applySubEvent(sub, payload) {
|
|
247
123
|
const ev = payload.match(SUB_EVENT_RE)
|
|
248
124
|
if (!ev) return false
|
|
@@ -258,22 +134,115 @@ export function applySubEvent(sub, payload) {
|
|
|
258
134
|
return true
|
|
259
135
|
}
|
|
260
136
|
|
|
261
|
-
// ─── Prefix routing
|
|
262
|
-
// Each router returns true when the name/token carried a role#id/ prefix and was
|
|
263
|
-
// consumed into the child's block buffer; false = not a child item (main path).
|
|
137
|
+
// ─── Prefix routing(agent-turn callbacks 委派):true = 带前缀已消费;false = 主路径
|
|
264
138
|
|
|
265
|
-
/**
|
|
139
|
+
/** onToken 分支:子文本 / [model] 元数据 / ⟦ev⟧ 事件。§27 R23(supersede §19.5 D-M8
|
|
140
|
+
* 子标方案):head 段路由块——inner 段**建子块载体归属内容**(D-R23a——subagent-children.mjs);
|
|
141
|
+
* 内层 [model] 记子块模型;内层 ⟦ev⟧ 除 done/stopped(生成侧补发射——D-R23c1——路由
|
|
142
|
+
* 子块定格)外剥除不路由(防 explore 进度污染外层块头——round1 #4)。 */
|
|
266
143
|
export function routeSubToken(state, t, scheduleRender) {
|
|
267
|
-
const
|
|
268
|
-
if (!
|
|
269
|
-
const
|
|
144
|
+
const path = parseRelayPath(t)
|
|
145
|
+
if (!path) return false
|
|
146
|
+
const payload = path.rest
|
|
147
|
+
const nested = path.inner.length > 0
|
|
148
|
+
// §19.5 D-M7b ① + 处置 #4(2026-09-03 评审 #4——兜底):零字段 async 标记在
|
|
149
|
+
// ensureSubTaskKey **之前**单独消费(async spawn 实际启动即发——sync 不发——精确
|
|
150
|
+
// 匹配:async 后须 RS 或串尾——正文伪前缀形态不误吞;内容侧伪哨兵已由生成侧
|
|
151
|
+
// stripEventToken 先行剥除——本分支只见真事件)。live 区块直接置位 sub.async;
|
|
152
|
+
// **缺失 key(区块尚未创建——排队条目补位启动的时序窗口 / tombstone)→ 缓冲
|
|
153
|
+
// pending 标志**(state._pendingAsyncKeys——ensureSubTaskKey 块创建时应用——async
|
|
154
|
+
// 事件先于块存在到达不丢——queued→running ⏹ 可见性保真——⏹ 门控与头标 async 判定源)。
|
|
155
|
+
// 内层(嵌套 explore#M/)async 剥除不路由(防污染外层块头)。
|
|
156
|
+
// §20 D-SD3b:queued 等待块实际启动(补位/释放)→ **转 running——同 key 不重建**
|
|
157
|
+
// ——清 waiting 标注(sub.queued)+ started 归零(elapsed 从实际启动计时——与池
|
|
158
|
+
// startedAt 语义一致——排队等待不计 elapsed)。
|
|
159
|
+
if (!nested && /^⟦ev⟧async(\x1e|$)/.test(payload)) {
|
|
160
|
+
const live = state.subTasks?.[path.head]
|
|
161
|
+
if (live) {
|
|
162
|
+
live.async = true
|
|
163
|
+
if (live.queued) {
|
|
164
|
+
delete live.queued
|
|
165
|
+
live.started = Date.now()
|
|
166
|
+
syncPanelSnapshot(state) // §19.6 D-P1: queued → running 状态变更刷镜
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
state._pendingAsyncKeys ??= new Set()
|
|
171
|
+
state._pendingAsyncKeys.add(path.head)
|
|
172
|
+
}
|
|
173
|
+
scheduleRender()
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
// §20 D-SD3b(round2 #2——cancelled 事件):取消/出队 → 移除等待块(**不冻结**——
|
|
177
|
+
// 从未启动无活动可冻结;running 取消走 ⟦ev⟧stopped 冻结——两者通道分明)。守卫:
|
|
178
|
+
// 仅 !async 块(waiting 块未启动——async 置位 = running——running 的取消以 stopped
|
|
179
|
+
// 通道表达,cancelled 永不合法指向 running 块——迟到/失序事件不误删)。缺失块
|
|
180
|
+
// (headless 竞态/已移除)→ no-op。零字段解析同 async(不入 SUB_EVENT_RE)。
|
|
181
|
+
if (!nested && /^⟦ev⟧cancelled(\x1e|$)/.test(payload)) {
|
|
182
|
+
const live = state.subTasks?.[path.head]
|
|
183
|
+
if (live && !live.done && live.async !== true) {
|
|
184
|
+
delete state.subTasks[path.head]
|
|
185
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 移除后刷镜(块出面板)
|
|
186
|
+
}
|
|
187
|
+
scheduleRender()
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
270
191
|
if (!sub) return true // frozen tombstone — late token from an aborted child: drop
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
192
|
+
// ⟦ev⟧:turn/approval 进度 → 仅头部(D1——不进 blocks/主流);done(§15 D-A3)=
|
|
193
|
+
// 完成即冻结(settle 时发);settled(§17 D-S8)= 挂起期完成——驻留面板中间态
|
|
194
|
+
// "done · awaiting digestion",池空补发冻结;stopped(§19.5)= cancel——立即冻结;
|
|
195
|
+
// queued(§20 D-SD3b)= 排队 spawn 等待块(spawn 返回即建/状态变迁刷新——覆盖式
|
|
196
|
+
// 更新 sub.queued——块不可展开(无活动流);启动后 async 事件清标转 running)。
|
|
275
197
|
if (payload.startsWith("⟦ev⟧")) {
|
|
198
|
+
// R23(supersede D-M8"内层事件剥除不路由"——评审 #1 🅰):done/stopped 完成信号 =
|
|
199
|
+
// 生成侧补发射(D-R23c1——sync 同步收尾段带完整嵌套前缀)→ 路由到子块定格
|
|
200
|
+
// (D-R23c2——closeSubChild——不冻结外层、不落 preview);缺失子块(迟到事件/
|
|
201
|
+
// 从未开块)→ 不建幻影(no-op)。其余内层事件(turn/approval/queued/settled/
|
|
202
|
+
// async/cancelled)剥除不路由(防 explore 进度污染外层块头——round1 #4)。
|
|
203
|
+
if (nested) {
|
|
204
|
+
const innerEv = payload.match(SUB_EVENT_RE)
|
|
205
|
+
if (innerEv && (innerEv[1] === "done" || innerEv[1] === "stopped")) {
|
|
206
|
+
const leaf = descendSubChild(sub, path.inner, { create: false })
|
|
207
|
+
// §27.1 F2: done 子块完全定格——迟到 done/stopped 丢弃(重复 done 不重放
|
|
208
|
+
// 定格;stopped 不打回 done 定格块)
|
|
209
|
+
if (leaf && !leaf.done) closeSubChild(sub, leaf, innerEv[1] === "stopped", path.label)
|
|
210
|
+
scheduleRender()
|
|
211
|
+
}
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
276
214
|
const ev = payload.match(SUB_EVENT_RE)
|
|
215
|
+
if (ev?.[1] === "queued") {
|
|
216
|
+
// 防御:已启动块(async 标记已置)收到迟到的 queued 刷新 → 丢弃(事件序保证
|
|
217
|
+
// queued 恒先于 async——迟到即陈旧——不复活 waiting 标注盖住 running 态)。
|
|
218
|
+
if (sub.async === true) return true
|
|
219
|
+
sub.queued = {
|
|
220
|
+
kind: ev[2] === "wait" || ev[2] === "depc" ? ev[2] : "slot",
|
|
221
|
+
position: Number(ev[3]) > 0 ? Number(ev[3]) : undefined,
|
|
222
|
+
detail: ev[5] ?? "",
|
|
223
|
+
}
|
|
224
|
+
sub.currentTool = null
|
|
225
|
+
sub.approval = null
|
|
226
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
227
|
+
syncPanelSnapshot(state) // §19.6 D-P1: waiting 块建/刷新刷镜
|
|
228
|
+
scheduleRender()
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
if (ev?.[1] === "settled") {
|
|
232
|
+
sub.done = true
|
|
233
|
+
sub.doneAt = Date.now()
|
|
234
|
+
sub.awaitingDigest = true
|
|
235
|
+
// 冻结锚点(2026-09-03 修复轮):settle 时刻流位置——§17.5.5 digest 完成逐条回收
|
|
236
|
+
// (freezeReclaimDigestedBlocks)与未消化残项的池空退出兜底(freezeAllSubTasks)
|
|
237
|
+
// 都按它 splice 落位——digest 总览文本之前(round1 #1 裁定——T-S6/T-S14 同口径)。
|
|
238
|
+
sub._freezeAt = state.lines?.length ?? 0
|
|
239
|
+
sub.currentTool = null
|
|
240
|
+
sub.approval = null
|
|
241
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
242
|
+
syncPanelSnapshot(state) // §19.6 D-P1: settled → awaitingDigest 状态变更刷镜
|
|
243
|
+
scheduleRender()
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
277
246
|
if (ev?.[1] === "done") {
|
|
278
247
|
sub.done = true
|
|
279
248
|
sub.doneAt = Date.now()
|
|
@@ -282,6 +251,20 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
282
251
|
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
283
252
|
freezeSubTaskLines(state, sub)
|
|
284
253
|
delete state.subTasks[sub.key]
|
|
254
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 冻结删除后刷镜
|
|
255
|
+
scheduleRender()
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
if (ev?.[1] === "stopped") {
|
|
259
|
+
sub.done = true
|
|
260
|
+
sub.doneAt = Date.now()
|
|
261
|
+
sub.stopped = true
|
|
262
|
+
sub.currentTool = null
|
|
263
|
+
sub.approval = null
|
|
264
|
+
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
265
|
+
freezeSubTaskLines(state, sub)
|
|
266
|
+
delete state.subTasks[sub.key]
|
|
267
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 冻结删除后刷镜
|
|
285
268
|
scheduleRender()
|
|
286
269
|
return true
|
|
287
270
|
}
|
|
@@ -293,68 +276,107 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
293
276
|
// parent's) — shown in the block header, NOT appended to its content stream.
|
|
294
277
|
// Only treat as metadata when the model isn't set yet (it's always the FIRST token);
|
|
295
278
|
// a child content token that happens to start with "[model]" must not be swallowed.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
279
|
+
// R23:内层 [model] 记到子块(D-R23a 子块状态字段——不再剥除丢弃);单层规则不变。
|
|
280
|
+
if (payload.startsWith("[model]") && (nested || sub.model === undefined)) {
|
|
281
|
+
if (nested) {
|
|
282
|
+
const leaf = descendSubChild(sub, path.inner)
|
|
283
|
+
// §27.1 F2: done 子块完全定格——迟到 [model] 不写定格块头
|
|
284
|
+
if (!leaf.done && leaf.model === undefined) {
|
|
285
|
+
leaf.model = payload.slice(7)
|
|
286
|
+
scheduleRender()
|
|
287
|
+
}
|
|
288
|
+
} else if (sub.model === undefined) {
|
|
289
|
+
sub.model = payload.slice(7)
|
|
290
|
+
scheduleRender()
|
|
291
|
+
}
|
|
299
292
|
return true
|
|
300
293
|
}
|
|
301
|
-
// Child LLM text → text block (N2 cap inside appendSubBlock).
|
|
302
|
-
|
|
294
|
+
// Child LLM text → text block (N2 cap inside appendSubBlock). R23:内层文本归属子块
|
|
295
|
+
// (D-R23b——子块行——不再混外层 blocks/子标)。§27.1 F2:done 子块完全定格——
|
|
296
|
+
// 迟到 chunk 丢弃(与 appendSubChild 数据层守卫同义——此处免无谓 render)。
|
|
297
|
+
if (nested) {
|
|
298
|
+
const leaf = descendSubChild(sub, path.inner)
|
|
299
|
+
if (leaf.done) return true
|
|
300
|
+
appendSubChild(sub, leaf, "text", payload)
|
|
301
|
+
} else appendSubBlock(sub, "text", payload)
|
|
303
302
|
scheduleRender()
|
|
304
303
|
return true
|
|
305
304
|
}
|
|
306
305
|
|
|
307
|
-
/** Child reasoning token → think block (F2: same treatment as main reasoning).
|
|
306
|
+
/** Child reasoning token → think block (F2: same treatment as main reasoning).
|
|
307
|
+
* §27 R23 nested think:归属内层子块(supersede D-M8 子标渲染——D-R23a/b)。 */
|
|
308
308
|
export function routeSubReasoning(state, t, scheduleRender) {
|
|
309
|
-
const
|
|
310
|
-
if (!
|
|
311
|
-
const sub =
|
|
309
|
+
const path = parseRelayPath(t)
|
|
310
|
+
if (!path) return false
|
|
311
|
+
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
312
312
|
if (!sub) return true // frozen tombstone — drop late token
|
|
313
|
-
|
|
313
|
+
const nested = path.inner.length > 0
|
|
314
|
+
if (nested) {
|
|
315
|
+
const leaf = descendSubChild(sub, path.inner)
|
|
316
|
+
if (leaf.done) return true // §27.1 F2: done 子块完全定格——迟到 think 丢弃
|
|
317
|
+
appendSubChild(sub, leaf, "think", path.rest)
|
|
318
|
+
} else appendSubBlock(sub, "think", path.rest)
|
|
314
319
|
scheduleRender()
|
|
315
320
|
return true
|
|
316
321
|
}
|
|
317
322
|
|
|
318
|
-
/** Child tool call → fresh tool block + header currentTool
|
|
323
|
+
/** Child tool call → fresh tool block + header currentTool。§27 R23 嵌套(supersede
|
|
324
|
+
* D-M8 子标工具行):工具行归属内层子块(子块内 `❯ tool`);currentTool 外层存全路径
|
|
325
|
+
* (`explore#1/read`——评审 #8:外层块头状态区现状保持不缩改)+ 子块存工具名(子块内
|
|
326
|
+
* 输出归属判别)。 */
|
|
319
327
|
export function routeSubToolCall(state, name, args, scheduleRender) {
|
|
320
|
-
const
|
|
321
|
-
if (!
|
|
322
|
-
const sub =
|
|
328
|
+
const path = parseRelayPath(name)
|
|
329
|
+
if (!path) return false
|
|
330
|
+
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
323
331
|
if (!sub) return true // frozen tombstone — drop late token
|
|
324
|
-
|
|
332
|
+
const nested = path.inner.length > 0
|
|
333
|
+
// §27.1 F2: done 子块完全定格——迟到 tool call 丢弃(不复活外层 currentTool/子块状态)
|
|
334
|
+
const leaf = nested ? descendSubChild(sub, path.inner) : null
|
|
335
|
+
if (leaf?.done) return true
|
|
336
|
+
sub.currentTool = nested ? `${path.label}/${path.rest}` : path.rest
|
|
325
337
|
sub.toolArgs = args
|
|
326
338
|
sub.approval = null
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
339
|
+
const argsDesc = describeToolArgs(path.rest, args)
|
|
340
|
+
if (nested) {
|
|
341
|
+
leaf.currentTool = path.rest
|
|
342
|
+
leaf.toolArgs = args
|
|
343
|
+
leaf.approval = null
|
|
344
|
+
appendSubChild(sub, leaf, "tool", `❯ ${path.rest}${argsDesc ? " " + argsDesc : ""}\n`, { fresh: true })
|
|
345
|
+
} else {
|
|
346
|
+
appendSubBlock(sub, "tool", `❯ ${path.rest}${argsDesc ? " " + argsDesc : ""}\n`, { fresh: true })
|
|
347
|
+
}
|
|
331
348
|
scheduleRender()
|
|
332
349
|
return true
|
|
333
350
|
}
|
|
334
351
|
|
|
335
|
-
/** Child tool output
|
|
336
|
-
*
|
|
352
|
+
/** Child tool output(D1 前缀 name relay)→ 追加当前 tool block。RAW 拼接(2026-09-03
|
|
353
|
+
* 修复轮——relay chunk 是任意字节边界碎片,逐 chunk 补 \n 会把词拦腰断行 + 烧 N2
|
|
354
|
+
* 配额;emit 端自带换行结构无损还原)。§27 R23 嵌套:输出归属内层子块(T-R23a.2——
|
|
355
|
+
* 输出全在子块——外层流无混入);fresh 判别在子块层(子块 currentTool=工具名)——
|
|
356
|
+
* 内外/同层同名工具互不串块。 */
|
|
337
357
|
export function routeSubToolOutput(state, name, part, scheduleRender) {
|
|
338
|
-
const
|
|
339
|
-
if (!
|
|
340
|
-
const sub =
|
|
358
|
+
const path = parseRelayPath(name)
|
|
359
|
+
if (!path) return false
|
|
360
|
+
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
341
361
|
if (!sub) return true // frozen tombstone — drop late token
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
362
|
+
if (path.inner.length > 0) {
|
|
363
|
+
const leaf = descendSubChild(sub, path.inner)
|
|
364
|
+
if (leaf.done) return true // §27.1 F2: done 子块完全定格——迟到输出丢弃(不复活 currentTool)
|
|
365
|
+
const leafTool = path.rest
|
|
366
|
+
appendSubChild(sub, leaf, "tool", part.text, { fresh: leaf.currentTool !== leafTool })
|
|
367
|
+
if (leaf.currentTool !== leafTool) leaf.currentTool = leafTool
|
|
368
|
+
} else {
|
|
369
|
+
const toolName = path.rest
|
|
370
|
+
appendSubBlock(sub, "tool", part.text, { fresh: sub.currentTool !== toolName })
|
|
371
|
+
if (sub.currentTool !== toolName) sub.currentTool = toolName
|
|
372
|
+
}
|
|
345
373
|
throttleSubRender(scheduleRender)
|
|
346
374
|
return true
|
|
347
375
|
}
|
|
348
376
|
|
|
349
|
-
// ─── Compression panel
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
// lives in state.subTasks (role "compress") and renders in the running subagent
|
|
353
|
-
// panel (subagent-panel.mjs) — the existing 1s turn ticker (agent-turn.mjs
|
|
354
|
-
// subRunning) makes the elapsed header tick; on completion/fallback it freezes
|
|
355
|
-
// into the stream as a collapsible block via freezeSubTaskLines, exactly like a
|
|
356
|
-
// finished child. The panel carries STATUS ONLY (D-C2): the summary body is a
|
|
357
|
-
// machine artifact and never enters the blocks.
|
|
377
|
+
// ─── Compression panel(CONTEXT-COMPACTION.md §7 D-C2):压缩会话复用子agent 区块机制——
|
|
378
|
+
// 运行期 role "compress" 条目驻留面板(状态/阶段/耗时——正文永不进面板),完成/降级
|
|
379
|
+
// 后 freezeSubTaskLines 冻结进流。
|
|
358
380
|
|
|
359
381
|
let _compressSeq = 0
|
|
360
382
|
|
|
@@ -363,13 +385,8 @@ function liveCompressPanel(state) {
|
|
|
363
385
|
return Object.values(state.subTasks ?? {}).find((s) => s.role === "compress" && !s.done) ?? null
|
|
364
386
|
}
|
|
365
387
|
|
|
366
|
-
/**
|
|
367
|
-
*
|
|
368
|
-
* the summary LLM call. info.messages = number of history messages being
|
|
369
|
-
* summarized ("summarizing N messages" stage label). Each attempt restarts the
|
|
370
|
-
* elapsed ticker (panel.started); the previous attempt's failure line stays in
|
|
371
|
-
* the block timeline so the retry history is visible.
|
|
372
|
-
*/
|
|
388
|
+
/** 打开(或重试复位)压缩面板——onCompressStart 于摘要调用前触发。每次尝试重置
|
|
389
|
+
* started(elapsed 归零),失败行留在时间线(重试历史可见)。 */
|
|
373
390
|
export function ensureCompressPanel(state, info = {}) {
|
|
374
391
|
state.subTasks ??= {}
|
|
375
392
|
let panel = liveCompressPanel(state)
|
|
@@ -385,8 +402,6 @@ export function ensureCompressPanel(state, info = {}) {
|
|
|
385
402
|
}
|
|
386
403
|
state.subTasks[panel.key] = panel
|
|
387
404
|
}
|
|
388
|
-
// Per-attempt reset (D-C2 state machine): retries return to "Compressing…",
|
|
389
|
-
// the elapsed ticker restarts from this attempt's start.
|
|
390
405
|
panel.done = false
|
|
391
406
|
panel.doneAt = null
|
|
392
407
|
panel.lastError = null
|
|
@@ -395,12 +410,11 @@ export function ensureCompressPanel(state, info = {}) {
|
|
|
395
410
|
const messages = Number.isInteger(info.messages) && info.messages >= 0 ? info.messages : "?"
|
|
396
411
|
appendSubBlock(panel, "status", "Compressing context…\n", { fresh: true })
|
|
397
412
|
appendSubBlock(panel, "meta", `summarizing ${messages} messages\n`, { fresh: true })
|
|
413
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 压缩面板建/重置刷镜
|
|
398
414
|
return panel
|
|
399
415
|
}
|
|
400
416
|
|
|
401
|
-
/**
|
|
402
|
-
* fallback note is bound to 3 CONSECUTIVE failures (markCompressFallback) and
|
|
403
|
-
* must not appear on a single failure (D-C2 state machine). */
|
|
417
|
+
/** 失败态(onCompressFail):只记错误文本——降级说明与 3 连败绑定(D-C2)。 */
|
|
404
418
|
export function markCompressFailed(state, error) {
|
|
405
419
|
const panel = liveCompressPanel(state)
|
|
406
420
|
if (!panel) return
|
|
@@ -409,16 +423,14 @@ export function markCompressFailed(state, error) {
|
|
|
409
423
|
appendSubBlock(panel, "err", `Compression failed: ${text}\n`, { fresh: true })
|
|
410
424
|
}
|
|
411
425
|
|
|
412
|
-
/**
|
|
413
|
-
* same carrier/fold-key as a finished child) and release the live entry. */
|
|
426
|
+
/** 冻结压缩面板进流(同完成子代理的载体/折叠 key)+ 释放 live 条目。 */
|
|
414
427
|
function freezeCompressPanel(state, panel) {
|
|
415
428
|
freezeSubTaskLines(state, panel)
|
|
416
429
|
delete state.subTasks[panel.key]
|
|
430
|
+
syncPanelSnapshot(state) // §19.6 D-P1: 压缩面板冻结删除后刷镜
|
|
417
431
|
}
|
|
418
432
|
|
|
419
|
-
/**
|
|
420
|
-
* summary (Xs)" — N = pre-compression estimate − post-compression estimate,
|
|
421
|
-
* Xs = elapsed seconds. Block frozen + collapsible (T2). */
|
|
433
|
+
/** 完成态(onCompress):"Compressed: N tokens freed → summary (Xs)"——冻结可折叠。 */
|
|
422
434
|
export function markCompressDone(state, info = {}) {
|
|
423
435
|
const panel = liveCompressPanel(state)
|
|
424
436
|
if (!panel) return
|
|
@@ -431,9 +443,7 @@ export function markCompressDone(state, info = {}) {
|
|
|
431
443
|
freezeCompressPanel(state, panel)
|
|
432
444
|
}
|
|
433
445
|
|
|
434
|
-
/**
|
|
435
|
-
* degradation note "Compression failed — fallback: truncated to N messages"
|
|
436
|
-
* (N = tail messages retained). Frozen + collapsible (T3b). */
|
|
446
|
+
/** 降级态(onCompress mode:"fallback")——3 连败后 "truncated to N messages"。 */
|
|
437
447
|
export function markCompressFallback(state, info = {}) {
|
|
438
448
|
const panel = liveCompressPanel(state)
|
|
439
449
|
if (!panel) return
|