thincoder 0.12.59 → 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 +38 -3
- package/README.md +2 -2
- package/bin/thincoder.mjs +80 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +7 -4
- package/src/advisor/messages.mjs +24 -4
- package/src/advisor/run.mjs +35 -33
- package/src/advisor.mjs +25 -6
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +102 -19
- package/src/agent/helpers.mjs +36 -0
- 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 +18 -2
- package/src/agent/spawn-child.mjs +29 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +110 -108
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +121 -102
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/read-history.mjs +155 -31
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +7 -17
- package/src/agent-tools/subagent-actions.mjs +168 -130
- package/src/agent-tools/subagent-async.mjs +129 -174
- 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 +45 -21
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +107 -555
- package/src/agent-tools/verify.mjs +118 -270
- package/src/agent.mjs +57 -190
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +3 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +56 -8
- package/src/context.mjs +5 -147
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +11 -11
- package/src/explore-distill.mjs +155 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +58 -48
- package/src/memory.mjs +3 -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 +9 -76
- package/src/prompts/advisor-round1.md +9 -68
- package/src/prompts/advisor-round2.md +7 -54
- package/src/prompts/advisor-round3.md +7 -54
- package/src/prompts/coder.md +7 -50
- package/src/prompts/consult-base.md +4 -24
- package/src/prompts/discipline.md +26 -44
- package/src/prompts/eng-coder.md +7 -32
- package/src/prompts/engineering-sub.md +3 -23
- package/src/prompts/engineering.md +53 -306
- package/src/prompts/explore.md +3 -12
- package/src/prompts/main.md +10 -32
- package/src/prompts/methodology-template.md +28 -48
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +16 -35
- package/src/provider/core.mjs +6 -67
- 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/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +11 -6
- 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/index.mjs +5 -6
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +3 -0
- package/src/tools/read.md +0 -1
- package/src/tools/shared.mjs +14 -13
- package/src/tools/system.mjs +44 -9
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +17 -228
- package/src/tui/cmd-config.mjs +48 -7
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +10 -10
- 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 +3 -2
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/mouse.mjs +7 -1
- package/src/tui/pickers.mjs +40 -22
- package/src/tui/render-segments.mjs +27 -10
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +95 -263
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +61 -23
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +3 -3
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +37 -173
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +12 -6
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
|
@@ -3,13 +3,25 @@
|
|
|
3
3
|
* state.subTasks[key] = { key, role, model, async(§19.5 D-M7b——⟦ev⟧async 标记——
|
|
4
4
|
* undefined = sync), started, done, doneAt, blocks:
|
|
5
5
|
* [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError,
|
|
6
|
-
* dropped, blockEpoch, awaitingDigest(§17), _freezeAt(冻结锚点), stopped(§19.5
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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)。
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import { C } from "./ansi.mjs"
|
|
12
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"
|
|
13
25
|
|
|
14
26
|
/** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
|
|
15
27
|
export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
|
|
@@ -26,8 +38,9 @@ export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
|
|
|
26
38
|
* (块尚未创建)缓冲 `state._pendingAsyncKeys`——ensureSubTaskKey 块创建时应用(兜底)。 */
|
|
27
39
|
export const SUB_EVENT_RE = /^⟦ev⟧(turn|approval|done|settled|stopped|queued)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e?([\s\S]*)$/
|
|
28
40
|
|
|
29
|
-
/** §19.5 D-M8 嵌套 relay
|
|
30
|
-
* `eng-coder#2/explore#1/read` → { head(块路由), inner[], label
|
|
41
|
+
/** §19.5 D-M8 嵌套 relay 前缀通用解析(循环解析任意深度——R23 保持为路由源):
|
|
42
|
+
* `eng-coder#2/explore#1/read` → { head(块路由), inner[], label(inner 链——R23:
|
|
43
|
+
* 子块折叠键/外层 currentTool 全路径用), rest }。
|
|
31
44
|
* 单层 = inner[]/label ""——与既有单段匹配语义零改;无前缀 → null。 */
|
|
32
45
|
export function parseRelayPath(text) {
|
|
33
46
|
const segments = []
|
|
@@ -47,44 +60,11 @@ export function parseRelayPath(text) {
|
|
|
47
60
|
}
|
|
48
61
|
}
|
|
49
62
|
|
|
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
|
-
}
|
|
59
|
-
/** N2: per-child display-line ring buffer cap — oldest lines drop with a marker. */
|
|
60
|
-
export const SUB_BLOCK_LINE_LIMIT = 500
|
|
61
63
|
/** N1: render-layer throttle for child tool-output appends (generation relays verbatim). */
|
|
62
64
|
export const SUB_RELAY_THROTTLE_MS = 250
|
|
63
65
|
/** Roles a subagent tool child can take (finishSubTask matches the block's role). */
|
|
64
66
|
export const SUBAGENT_ROLES = ["sub", "explore", "plan", "coder", "eng-coder"]
|
|
65
67
|
|
|
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
|
-
|
|
88
68
|
let _subRenderLast = 0
|
|
89
69
|
let _subRenderTimer = null
|
|
90
70
|
|
|
@@ -119,6 +99,7 @@ export function ensureSubTaskKey(state, key, role) {
|
|
|
119
99
|
blocks: [], currentTool: null, toolArgs: null, turn: 0, maxTurns: 0, approval: null,
|
|
120
100
|
lastError: null, dropped: 0,
|
|
121
101
|
stopped: false, // §19.5: ⟦ev⟧stopped 冻结标记(标题 "stopped")
|
|
102
|
+
children: [], // R23: 嵌套子代理子块载体(subagent-children.mjs——D-R23a)
|
|
122
103
|
}
|
|
123
104
|
// §19.5 D-M7b ①(处置 #4 兜底——2026-09-03):缺失 key 的 ⟦ev⟧async 事件已缓冲
|
|
124
105
|
// pending 标志(routeSubToken——async 先于块创建到达:排队条目补位启动的时序窗口)
|
|
@@ -133,213 +114,9 @@ export function ensureSubTask(state, subMatch) {
|
|
|
133
114
|
return ensureSubTaskKey(state, `${subMatch[1]}#${subMatch[2]}`, subMatch[1])
|
|
134
115
|
}
|
|
135
116
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
export function appendSubBlock(sub, kind, text, { fresh = false } = {}) {
|
|
140
|
-
if (!text) return
|
|
141
|
-
const last = sub.blocks.at(-1)
|
|
142
|
-
if (!fresh && last && last.kind === kind) {
|
|
143
|
-
// 合并只算净增行数(逐 chunk 全量 split 会把单行碎片虚计成 2 段——P1, 2026-08-30)
|
|
144
|
-
const before = countBlockLines(last.text)
|
|
145
|
-
last.text += text
|
|
146
|
-
sub._lineCount = (sub._lineCount ?? 0) + countBlockLines(last.text) - before
|
|
147
|
-
} else {
|
|
148
|
-
sub.blocks.push({ kind, text })
|
|
149
|
-
sub._lineCount = (sub._lineCount ?? 0) + countBlockLines(text)
|
|
150
|
-
}
|
|
151
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
152
|
-
trimSubBlocks(sub)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** N2:超限丢最旧行 + 留一条累计省略标记行(done 块同受约束)。 */
|
|
156
|
-
export function trimSubBlocks(sub) {
|
|
157
|
-
// 增量行记账(P1):appendSubBlock 维护 _lineCount——旧实现每 chunk 全量 reduce
|
|
158
|
-
sub._lineCount = (sub._lineCount ?? 0)
|
|
159
|
-
let over = sub._lineCount - SUB_BLOCK_LINE_LIMIT
|
|
160
|
-
if (over <= 0) return
|
|
161
|
-
let droppedNow = 0
|
|
162
|
-
while (over > 0 && sub.blocks.length > 0) {
|
|
163
|
-
const first = sub.blocks[0]
|
|
164
|
-
const lines = countBlockLines(first.text)
|
|
165
|
-
const take = Math.min(lines, over)
|
|
166
|
-
if (take >= lines) {
|
|
167
|
-
sub.blocks.shift()
|
|
168
|
-
droppedNow += lines
|
|
169
|
-
} else {
|
|
170
|
-
first.text = first.text.split("\n").slice(take).join("\n")
|
|
171
|
-
droppedNow += take
|
|
172
|
-
}
|
|
173
|
-
over -= take
|
|
174
|
-
}
|
|
175
|
-
sub._lineCount -= droppedNow
|
|
176
|
-
sub.dropped += droppedNow
|
|
177
|
-
const marker = `…(已省略 ${sub.dropped} 行)`
|
|
178
|
-
const first = sub.blocks[0]
|
|
179
|
-
if (first && first.kind === "meta") first.text = marker
|
|
180
|
-
else { sub.blocks.unshift({ kind: "meta", text: marker }); sub._lineCount += 1 }
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Mark the earliest running child of the given role(s) as done (✓ header, frozen elapsed). */
|
|
184
|
-
export function finishSubTask(state, roles, lastError = null) {
|
|
185
|
-
state.subTasks ??= {}
|
|
186
|
-
const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
|
|
187
|
-
const running = Object.values(state.subTasks)
|
|
188
|
-
.filter((s) => !s.done && roleSet.has(s.role))
|
|
189
|
-
.sort((a, b) => a.started - b.started)
|
|
190
|
-
if (running.length === 0) return
|
|
191
|
-
const sub = running[0]
|
|
192
|
-
sub.done = true
|
|
193
|
-
sub.doneAt = Date.now()
|
|
194
|
-
sub.currentTool = null
|
|
195
|
-
sub.approval = null
|
|
196
|
-
if (lastError) sub.lastError = lastError
|
|
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
|
|
219
|
-
}
|
|
220
|
-
|
|
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——先插小锚点会移走大锚点目标)。 */
|
|
226
|
-
export function freezeSubTaskLines(state, sub) {
|
|
227
|
-
if (!sub) return
|
|
228
|
-
state._frozenSubKeys ??= new Set()
|
|
229
|
-
state._frozenSubKeys.add(sub.key)
|
|
230
|
-
sub.done = true
|
|
231
|
-
sub.doneAt = sub.doneAt ?? Date.now()
|
|
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
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** 冻结 + 释放全部已 done 块(工具结果清扫路径)。锚点降序(后 settle 先插——
|
|
248
|
-
* 绝对位置 splice 语义);无锚点(-1)排末尾推;sort 稳定。 */
|
|
249
|
-
export function freezeDoneSubTasks(state) {
|
|
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]
|
|
256
|
-
}
|
|
257
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 删除后刷镜(块移出面板)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** 按角色整组标记 done——consult N 并行 children 的会话级 settle(单发
|
|
261
|
-
* finishSubTask 只结最早一个——consult 残项 2026-08-30)。 */
|
|
262
|
-
export function finishSubTasksByRole(state, roles, lastError = null) {
|
|
263
|
-
state.subTasks ??= {}
|
|
264
|
-
const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
|
|
265
|
-
for (const sub of Object.values(state.subTasks)) {
|
|
266
|
-
if (!sub.done && roleSet.has(sub.role)) {
|
|
267
|
-
sub.done = true
|
|
268
|
-
sub.doneAt = Date.now()
|
|
269
|
-
sub.currentTool = null
|
|
270
|
-
sub.approval = null
|
|
271
|
-
if (lastError) sub.lastError = lastError
|
|
272
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** 按 [model] 记录的 model 精确 settle(consult_check 返回 provider:model——
|
|
279
|
-
* 乱序 settle 时最早启发式冻错块)。比对尾段(bare ↔ provider:model)。 */
|
|
280
|
-
export function finishSubTaskByModel(state, role, model, lastError = null) {
|
|
281
|
-
state.subTasks ??= {}
|
|
282
|
-
const want = String(model ?? "").includes(":") ? String(model).split(":").pop() : String(model ?? "")
|
|
283
|
-
for (const sub of Object.values(state.subTasks)) {
|
|
284
|
-
const have = String(sub.model ?? "")
|
|
285
|
-
const haveTail = have.includes(":") ? have.split(":").pop() : have
|
|
286
|
-
if (!sub.done && sub.role === role && haveTail === want) {
|
|
287
|
-
sub.done = true
|
|
288
|
-
sub.doneAt = Date.now()
|
|
289
|
-
sub.currentTool = null
|
|
290
|
-
sub.approval = null
|
|
291
|
-
if (lastError) sub.lastError = lastError
|
|
292
|
-
sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
|
|
293
|
-
syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
|
|
294
|
-
return sub
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return null
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** 回合尾/挂起退出清扫(runAgentTurn finally / suspensionSession finally):冻结全部
|
|
301
|
-
* 剩余块——中断(Ctrl+C/错误)不留下钉住输入框的 ghost;未 done 者
|
|
302
|
-
* lastError="interrupted"(Ready 态跳过)。§17.5.5:挂起自然退出时本函数只兜底
|
|
303
|
-
* **未消化残项**(已消化块由 freezeReclaimDigestedBlocks 逐条先行回收——块回收与
|
|
304
|
-
* 池空解耦)。锚点降序同 freezeDoneSubTasks(挂起期 settle 锚点交错批次各按其
|
|
305
|
-
* settle 位置落位)。 */
|
|
306
|
-
export function freezeAllSubTasks(state) {
|
|
307
|
-
const subs = Object.values(state.subTasks ?? {})
|
|
308
|
-
.sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
|
|
309
|
-
for (const sub of subs) {
|
|
310
|
-
if (!sub.done) {
|
|
311
|
-
sub.done = true
|
|
312
|
-
sub.doneAt = Date.now()
|
|
313
|
-
if (!sub.lastError && state.status !== "Ready") sub.lastError = "interrupted"
|
|
314
|
-
}
|
|
315
|
-
freezeSubTaskLines(state, sub)
|
|
316
|
-
delete state.subTasks[sub.key]
|
|
317
|
-
}
|
|
318
|
-
syncPanelSnapshot(state) // §19.6 D-P1: 冻结回收后刷镜(块移出面板)
|
|
319
|
-
}
|
|
320
|
-
|
|
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
|
-
|
|
117
|
+
/** Append(kind 合并追加)+ N2 树级环形上限;fresh=true 强制新块(每工具调用)。
|
|
118
|
+
* R23(2026-09-07):appendSubBlock/trimSubBlocks 实现迁 subagent-children.mjs(树级
|
|
119
|
+
* 配额——子块计入外层 500 行环)——本文件 import + re-export 保 import 面(压缩面板等)。 */
|
|
343
120
|
/** 事件 token → 区块头部(turn/approval 更新 turn n/max + 等待态)。事件永不进
|
|
344
121
|
* blocks/主流——仅头部。@returns {boolean} 良构事件(已消费) */
|
|
345
122
|
export function applySubEvent(sub, payload) {
|
|
@@ -359,8 +136,10 @@ export function applySubEvent(sub, payload) {
|
|
|
359
136
|
|
|
360
137
|
// ─── Prefix routing(agent-turn callbacks 委派):true = 带前缀已消费;false = 主路径
|
|
361
138
|
|
|
362
|
-
/** onToken 分支:子文本 / [model] 元数据 / ⟦ev⟧ 事件。§19.5 D-M8
|
|
363
|
-
*
|
|
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)。 */
|
|
364
143
|
export function routeSubToken(state, t, scheduleRender) {
|
|
365
144
|
const path = parseRelayPath(t)
|
|
366
145
|
if (!path) return false
|
|
@@ -416,7 +195,22 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
416
195
|
// queued(§20 D-SD3b)= 排队 spawn 等待块(spawn 返回即建/状态变迁刷新——覆盖式
|
|
417
196
|
// 更新 sub.queued——块不可展开(无活动流);启动后 async 事件清标转 running)。
|
|
418
197
|
if (payload.startsWith("⟦ev⟧")) {
|
|
419
|
-
|
|
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
|
+
}
|
|
420
214
|
const ev = payload.match(SUB_EVENT_RE)
|
|
421
215
|
if (ev?.[1] === "queued") {
|
|
422
216
|
// 防御:已启动块(async 标记已置)收到迟到的 queued 刷新 → 丢弃(事件序保证
|
|
@@ -482,62 +276,100 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
482
276
|
// parent's) — shown in the block header, NOT appended to its content stream.
|
|
483
277
|
// Only treat as metadata when the model isn't set yet (it's always the FIRST token);
|
|
484
278
|
// a child content token that happens to start with "[model]" must not be swallowed.
|
|
279
|
+
// R23:内层 [model] 记到子块(D-R23a 子块状态字段——不再剥除丢弃);单层规则不变。
|
|
485
280
|
if (payload.startsWith("[model]") && (nested || sub.model === undefined)) {
|
|
486
|
-
if (
|
|
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) {
|
|
487
289
|
sub.model = payload.slice(7)
|
|
488
290
|
scheduleRender()
|
|
489
|
-
}
|
|
291
|
+
}
|
|
490
292
|
return true
|
|
491
293
|
}
|
|
492
|
-
// Child LLM text → text block (N2 cap inside appendSubBlock).
|
|
493
|
-
|
|
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)
|
|
494
302
|
scheduleRender()
|
|
495
303
|
return true
|
|
496
304
|
}
|
|
497
305
|
|
|
498
306
|
/** Child reasoning token → think block (F2: same treatment as main reasoning).
|
|
499
|
-
* §
|
|
307
|
+
* §27 R23 nested think:归属内层子块(supersede D-M8 子标渲染——D-R23a/b)。 */
|
|
500
308
|
export function routeSubReasoning(state, t, scheduleRender) {
|
|
501
309
|
const path = parseRelayPath(t)
|
|
502
310
|
if (!path) return false
|
|
503
311
|
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
504
312
|
if (!sub) return true // frozen tombstone — drop late token
|
|
505
313
|
const nested = path.inner.length > 0
|
|
506
|
-
|
|
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)
|
|
507
319
|
scheduleRender()
|
|
508
320
|
return true
|
|
509
321
|
}
|
|
510
322
|
|
|
511
|
-
/** Child tool call → fresh tool block + header currentTool。§
|
|
512
|
-
*
|
|
513
|
-
* (`explore#1/read
|
|
323
|
+
/** Child tool call → fresh tool block + header currentTool。§27 R23 嵌套(supersede
|
|
324
|
+
* D-M8 子标工具行):工具行归属内层子块(子块内 `❯ tool`);currentTool 外层存全路径
|
|
325
|
+
* (`explore#1/read`——评审 #8:外层块头状态区现状保持不缩改)+ 子块存工具名(子块内
|
|
326
|
+
* 输出归属判别)。 */
|
|
514
327
|
export function routeSubToolCall(state, name, args, scheduleRender) {
|
|
515
328
|
const path = parseRelayPath(name)
|
|
516
329
|
if (!path) return false
|
|
517
330
|
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
518
331
|
if (!sub) return true // frozen tombstone — drop late token
|
|
519
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
|
|
520
336
|
sub.currentTool = nested ? `${path.label}/${path.rest}` : path.rest
|
|
521
337
|
sub.toolArgs = args
|
|
522
338
|
sub.approval = null
|
|
523
339
|
const argsDesc = describeToolArgs(path.rest, args)
|
|
524
|
-
|
|
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
|
+
}
|
|
525
348
|
scheduleRender()
|
|
526
349
|
return true
|
|
527
350
|
}
|
|
528
351
|
|
|
529
352
|
/** Child tool output(D1 前缀 name relay)→ 追加当前 tool block。RAW 拼接(2026-09-03
|
|
530
353
|
* 修复轮——relay chunk 是任意字节边界碎片,逐 chunk 补 \n 会把词拦腰断行 + 烧 N2
|
|
531
|
-
* 配额;emit 端自带换行结构无损还原)。§
|
|
532
|
-
*
|
|
354
|
+
* 配额;emit 端自带换行结构无损还原)。§27 R23 嵌套:输出归属内层子块(T-R23a.2——
|
|
355
|
+
* 输出全在子块——外层流无混入);fresh 判别在子块层(子块 currentTool=工具名)——
|
|
356
|
+
* 内外/同层同名工具互不串块。 */
|
|
533
357
|
export function routeSubToolOutput(state, name, part, scheduleRender) {
|
|
534
358
|
const path = parseRelayPath(name)
|
|
535
359
|
if (!path) return false
|
|
536
360
|
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
537
361
|
if (!sub) return true // frozen tombstone — drop late token
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
+
}
|
|
541
373
|
throttleSubRender(scheduleRender)
|
|
542
374
|
return true
|
|
543
375
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-children.mjs — 嵌套子代理子块载体数据层(AGENT-LOOP.md §27 R23 D-R23a/c2/NFR)。
|
|
3
|
+
*
|
|
4
|
+
* 子块 = 外层 state.subTasks 块内的嵌套载体(外层 .children[]——每层一子块——D-R23d):
|
|
5
|
+
* carrier = { key(父内段标 role#N), role, model, started, done, doneAt, stopped,
|
|
6
|
+
* blocks, currentTool, toolArgs, approval, lastError, dropped,
|
|
7
|
+
* blockEpoch, children: [], _lineCount }——与外层块同形(渲染复用
|
|
8
|
+
* renderBlockTimeline/renderExpandedBlock 公共组件)。
|
|
9
|
+
* 内容/工具/模型全部归属子块(D-R23b——不再混外层 blocks——T-R23a.2 输出全在子块)。
|
|
10
|
+
*
|
|
11
|
+
* N2 共享配额(R23 NFR——评审 #6):子块行数计入**外层** 500 行环形上限——树级 trim
|
|
12
|
+
* (子块批量输出先丢、外层叙述保留优先——见 trimSubTree)——子块不独立扩容——面板
|
|
13
|
+
* 不无限增长。
|
|
14
|
+
*/
|
|
15
|
+
export const SUB_BLOCK_LINE_LIMIT = 500
|
|
16
|
+
|
|
17
|
+
const countBlockLines = (text) => text.split("\n").length
|
|
18
|
+
|
|
19
|
+
/** 载体树总行数(根自身 + 全部子孙块)——共享配额记账。 */
|
|
20
|
+
export function carrierTreeLines(carrier) {
|
|
21
|
+
let n = carrier._lineCount ?? 0
|
|
22
|
+
for (const c of carrier.children ?? []) n += carrierTreeLines(c)
|
|
23
|
+
return n
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 载体级丢行(原 trimSubBlocks 逐块丢弃数学——整块 shift、末块按行裁切);返回实际
|
|
27
|
+
* 丢弃行数。每载体独立 dropped 计数 + meta 省略标记(同既有 trimSubBlocks 语义)。 */
|
|
28
|
+
function dropCarrierLines(carrier, want) {
|
|
29
|
+
if (want <= 0 || carrier.blocks.length === 0) return 0
|
|
30
|
+
let dropped = 0
|
|
31
|
+
while (want > dropped && carrier.blocks.length > 0) {
|
|
32
|
+
const first = carrier.blocks[0]
|
|
33
|
+
const lines = countBlockLines(first.text)
|
|
34
|
+
const take = Math.min(lines, want - dropped)
|
|
35
|
+
if (take >= lines) {
|
|
36
|
+
carrier.blocks.shift()
|
|
37
|
+
dropped += lines
|
|
38
|
+
} else {
|
|
39
|
+
first.text = first.text.split("\n").slice(take).join("\n")
|
|
40
|
+
dropped += take
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
carrier._lineCount = (carrier._lineCount ?? 0) - dropped
|
|
44
|
+
carrier.dropped = (carrier.dropped ?? 0) + dropped
|
|
45
|
+
const marker = `…(已省略 ${carrier.dropped} 行)`
|
|
46
|
+
const first = carrier.blocks[0]
|
|
47
|
+
if (first && first.kind === "meta") first.text = marker
|
|
48
|
+
else { carrier.blocks.unshift({ kind: "meta", text: marker }); carrier._lineCount = (carrier._lineCount ?? 0) + 1 }
|
|
49
|
+
return dropped
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** R23 NFR:树级配额 trim——总行(根 + 子块树)超限 → 丢行。丢序 = **子树递归先于
|
|
53
|
+
* 根载体**(后序:叶子/子块先、根最后)——子块承载批量输出(explore 工具流),外层
|
|
54
|
+
* 叙述行(折叠头 tail/状态区的展示源)保留优先;每载体内部仍最旧行先行(dropCarrierLines
|
|
55
|
+
* 逐块 FIFO)。单载体(无子块)情形与既有 trimSubBlocks 行为逐字同效。
|
|
56
|
+
* §27.1 F1(2026-09-07 三缺陷修复批——缺陷③主修):丢行遍历**跳过 done 子块**——
|
|
57
|
+
* done = 定格快照——配额压力不再蚕食已定格子块行,丢行落点移到外层自身行(live
|
|
58
|
+
* 块「已省略 N 行」增长语义正确)。边界(评审 #6):live 行耗尽(全树皆 done)→
|
|
59
|
+
* 允许超限(定格快照优先于配额——N2 上限对 done 存量 best-effort)。 */
|
|
60
|
+
export function trimSubTree(root) {
|
|
61
|
+
let over = carrierTreeLines(root) - SUB_BLOCK_LINE_LIMIT
|
|
62
|
+
if (over <= 0) return
|
|
63
|
+
const walk = (carrier) => {
|
|
64
|
+
for (const c of carrier.children ?? []) {
|
|
65
|
+
if (over <= 0) return
|
|
66
|
+
if (c.done) continue // §27.1 F1: done 子块 = 定格快照——丢行遍历跳过(豁免)
|
|
67
|
+
walk(c)
|
|
68
|
+
}
|
|
69
|
+
if (over <= 0) return
|
|
70
|
+
over -= dropCarrierLines(carrier, over)
|
|
71
|
+
}
|
|
72
|
+
walk(root)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** kind 合并追加(载体级)——既有 appendSubBlock 数学(净增行记账——P1)。 */
|
|
76
|
+
function pushBlock(carrier, kind, text, fresh) {
|
|
77
|
+
if (!text) return
|
|
78
|
+
const last = carrier.blocks.at(-1)
|
|
79
|
+
if (!fresh && last && last.kind === kind) {
|
|
80
|
+
const before = countBlockLines(last.text)
|
|
81
|
+
last.text += text
|
|
82
|
+
carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(last.text) - before
|
|
83
|
+
} else {
|
|
84
|
+
carrier.blocks.push({ kind, text })
|
|
85
|
+
carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(text)
|
|
86
|
+
}
|
|
87
|
+
carrier.blockEpoch = (carrier.blockEpoch ?? 0) + 1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 追加到根载体(外层块/压缩面板——无父载体者自身即配额树根)。树级 trim——
|
|
91
|
+
* 无子块时与既有 trimSubBlocks 同效(N2 既有测试口径不变)。 */
|
|
92
|
+
export function appendSubBlock(sub, kind, text, { fresh = false } = {}) {
|
|
93
|
+
if (!text) return
|
|
94
|
+
pushBlock(sub, kind, text, fresh)
|
|
95
|
+
trimSubTree(sub)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** R23:追加到子块载体——行数计入根(外层)树配额(NFR——同配额不独立扩容)。
|
|
99
|
+
* §27.1 F2(2026-09-07):child.done 守卫——done 后完全定格,迟到 chunk 丢弃
|
|
100
|
+
* (与 §7.2 D4 完成态冻结语义统一——数据层 tombstone 拒绝)。 */
|
|
101
|
+
export function appendSubChild(root, child, kind, text, { fresh = false } = {}) {
|
|
102
|
+
if (!text) return
|
|
103
|
+
if (child.done) return
|
|
104
|
+
pushBlock(child, kind, text, fresh)
|
|
105
|
+
root.blockEpoch = (root.blockEpoch ?? 0) + 1
|
|
106
|
+
trimSubTree(root)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** R23 D-R23a:子块载体创建/定位——按段(role#N)在父载体 children 查找,缺失即建
|
|
110
|
+
* (每层一子块——任意 inner 深度同路径——D-R23d)。@returns 子块载体 */
|
|
111
|
+
export function ensureSubChild(carrier, seg) {
|
|
112
|
+
carrier.children ??= []
|
|
113
|
+
let c = carrier.children.find((x) => x.key === seg)
|
|
114
|
+
if (!c) {
|
|
115
|
+
c = {
|
|
116
|
+
key: seg,
|
|
117
|
+
role: seg.slice(0, seg.lastIndexOf("#")),
|
|
118
|
+
model: undefined,
|
|
119
|
+
started: Date.now(), done: false, doneAt: null, stopped: false,
|
|
120
|
+
blocks: [], currentTool: null, toolArgs: null,
|
|
121
|
+
approval: null, lastError: null, dropped: 0,
|
|
122
|
+
blockEpoch: 0, children: [], _lineCount: 0,
|
|
123
|
+
}
|
|
124
|
+
carrier.children.push(c)
|
|
125
|
+
}
|
|
126
|
+
return c
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 沿 inner 段逐层下降。create=false 时缺层返回 null(事件关闭路径——不建幻影空块)。 */
|
|
130
|
+
export function descendSubChild(root, inner, { create = true } = {}) {
|
|
131
|
+
let carrier = root
|
|
132
|
+
for (const seg of inner) {
|
|
133
|
+
if (create) carrier = ensureSubChild(carrier, seg)
|
|
134
|
+
else {
|
|
135
|
+
const next = (carrier.children ?? []).find((x) => x.key === seg)
|
|
136
|
+
if (!next) return null
|
|
137
|
+
carrier = next
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return carrier === root ? null : carrier
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** R23 D-R23c2 子块完成定格(内层 ⟦ev⟧done/stopped → 子块尾定格——不落 preview、不冻结
|
|
144
|
+
* 外层);stopped = interrupted(外层 abort 传播/内层错误收尾——T-R23a.3 不悬空)。
|
|
145
|
+
* 外层块头 currentTool 若指向该子块(innerPath 前缀——评审 #8 全路径现状保持)→ 清空
|
|
146
|
+
* (子块已定格——工具不再是 current)。 */
|
|
147
|
+
export function closeSubChild(root, child, stopped, innerPath) {
|
|
148
|
+
child.done = true
|
|
149
|
+
child.doneAt = Date.now()
|
|
150
|
+
if (stopped) child.stopped = true
|
|
151
|
+
child.currentTool = null
|
|
152
|
+
child.approval = null
|
|
153
|
+
child.blockEpoch = (child.blockEpoch ?? 0) + 1
|
|
154
|
+
if (root.currentTool && innerPath && String(root.currentTool).startsWith(`${innerPath}/`)) {
|
|
155
|
+
root.currentTool = null
|
|
156
|
+
root.toolArgs = null
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层未收尾(外层 abort/中断)→
|
|
161
|
+
* 内层子块随外层冻结时定格 stopped——不悬空。freezeSubTaskLines 冻结前调用。 */
|
|
162
|
+
export function closeOpenSubChildren(sub) {
|
|
163
|
+
const walk = (carrier) => {
|
|
164
|
+
for (const c of carrier.children ?? []) {
|
|
165
|
+
if (!c.done) {
|
|
166
|
+
c.done = true
|
|
167
|
+
c.doneAt = Date.now()
|
|
168
|
+
c.stopped = true
|
|
169
|
+
c.currentTool = null
|
|
170
|
+
c.approval = null
|
|
171
|
+
}
|
|
172
|
+
walk(c)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
walk(sub)
|
|
176
|
+
}
|