thincoder 0.12.60 → 0.12.62

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.
Files changed (171) hide show
  1. package/CHANGELOG.md +49 -2
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +38 -124
  4. package/package.json +3 -2
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +38 -17
  7. package/src/acp.mjs +19 -6
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +36 -134
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +124 -329
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +3 -2
  17. package/src/agent/completion.mjs +1 -1
  18. package/src/agent/dispatch.mjs +47 -12
  19. package/src/agent/helpers.mjs +71 -13
  20. package/src/agent/record-results.mjs +13 -5
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +24 -7
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +43 -11
  26. package/src/agent-tools/advisor-async.mjs +70 -180
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +69 -20
  29. package/src/agent-tools/async-settle.mjs +13 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +28 -10
  32. package/src/agent-tools/design-token.mjs +14 -1
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +3 -3
  35. package/src/agent-tools/escalate-async.mjs +22 -13
  36. package/src/agent-tools/read-history.mjs +31 -6
  37. package/src/agent-tools/review-streak.mjs +93 -0
  38. package/src/agent-tools/settings.mjs +130 -17
  39. package/src/agent-tools/subagent-actions.mjs +18 -6
  40. package/src/agent-tools/subagent-async.mjs +66 -14
  41. package/src/agent-tools/subagent-panel.mjs +22 -15
  42. package/src/agent-tools/subagent-run.mjs +10 -7
  43. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  44. package/src/agent-tools/subagent-spawn.mjs +69 -16
  45. package/src/agent-tools/subagent.mjs +175 -49
  46. package/src/agent-tools/verify.mjs +13 -34
  47. package/src/agent-tools.mjs +1 -0
  48. package/src/agent.mjs +42 -21
  49. package/src/cli/distill-command.mjs +2 -2
  50. package/src/cli/make-agent.mjs +23 -7
  51. package/src/cli/memory-command.mjs +2 -2
  52. package/src/cli/setup-wizard.mjs +29 -9
  53. package/src/completions.mjs +114 -0
  54. package/src/config-migrate.mjs +70 -0
  55. package/src/config.mjs +132 -63
  56. package/src/context.mjs +12 -1
  57. package/src/conventions.mjs +223 -0
  58. package/src/crash-reports.mjs +35 -8
  59. package/src/expand-home.mjs +16 -0
  60. package/src/generate-title.mjs +9 -4
  61. package/src/heap-watch.mjs +88 -0
  62. package/src/hooks.mjs +7 -3
  63. package/src/ledger.mjs +227 -0
  64. package/src/memory/code-index.mjs +9 -3
  65. package/src/memory/code-sync.mjs +77 -36
  66. package/src/memory/core.mjs +8 -9
  67. package/src/memory/delete.mjs +2 -0
  68. package/src/memory/docs.mjs +17 -11
  69. package/src/memory/file-walk.mjs +109 -0
  70. package/src/memory/scan.mjs +95 -0
  71. package/src/memory/schema.mjs +15 -3
  72. package/src/model-ref.mjs +66 -0
  73. package/src/model-specs.mjs +42 -8
  74. package/src/prompt-overlays.mjs +73 -16
  75. package/src/prompts/advisor-design.md +19 -9
  76. package/src/prompts/advisor-round1.md +8 -2
  77. package/src/prompts/advisor-round2.md +14 -3
  78. package/src/prompts/advisor-round3.md +14 -3
  79. package/src/prompts/common.md +115 -0
  80. package/src/prompts/consult-base.md +2 -0
  81. package/src/prompts/discipline-engineering.md +258 -0
  82. package/src/prompts/discipline-normal.md +185 -0
  83. package/src/prompts/persona-coder.md +21 -0
  84. package/src/prompts/persona-eng-coder.md +37 -0
  85. package/src/prompts/persona-eng-designer.md +60 -0
  86. package/src/prompts/persona-engineering.md +55 -0
  87. package/src/prompts/persona-explore.md +15 -0
  88. package/src/prompts/persona-normal.md +27 -0
  89. package/src/prompts/persona-plan.md +26 -0
  90. package/src/provider/anthropic.mjs +4 -4
  91. package/src/provider/core.mjs +13 -32
  92. package/src/provider/errors.mjs +26 -1
  93. package/src/provider/google.mjs +5 -6
  94. package/src/provider/index.mjs +2 -1
  95. package/src/provider/list-models.mjs +93 -0
  96. package/src/provider/rate.mjs +2 -1
  97. package/src/provider/responses.mjs +5 -3
  98. package/src/provider/sse.mjs +3 -4
  99. package/src/proxy.mjs +9 -14
  100. package/src/session-gc.mjs +9 -2
  101. package/src/session-guard.mjs +12 -0
  102. package/src/session-segments.mjs +100 -0
  103. package/src/session-slots.mjs +10 -2
  104. package/src/session-store.mjs +441 -0
  105. package/src/session.mjs +137 -99
  106. package/src/text-budget.mjs +46 -0
  107. package/src/token-ttl.mjs +2 -1
  108. package/src/tools/{system.mjs → bash.mjs} +6 -243
  109. package/src/tools/file.mjs +30 -10
  110. package/src/tools/git.md +1 -1
  111. package/src/tools/git.mjs +15 -34
  112. package/src/tools/index.mjs +4 -2
  113. package/src/tools/ops.mjs +20 -7
  114. package/src/tools/question.md +1 -0
  115. package/src/tools/question.mjs +26 -0
  116. package/src/tools/read.md +1 -1
  117. package/src/tools/read_image.md +1 -1
  118. package/src/tools/search.mjs +236 -0
  119. package/src/traces/trace-store.mjs +195 -64
  120. package/src/tui/agent-turn.mjs +32 -13
  121. package/src/tui/ansi.mjs +2 -0
  122. package/src/tui/clipboard.mjs +7 -1
  123. package/src/tui/cmd-advisor.mjs +3 -2
  124. package/src/tui/cmd-clear.mjs +2 -0
  125. package/src/tui/cmd-config.mjs +108 -37
  126. package/src/tui/cmd-eng.mjs +11 -27
  127. package/src/tui/cmd-exit.mjs +6 -8
  128. package/src/tui/cmd-model.mjs +14 -12
  129. package/src/tui/cmd-new.mjs +5 -1
  130. package/src/tui/cmd-reindex.mjs +7 -0
  131. package/src/tui/cmd-session.mjs +8 -4
  132. package/src/tui/cmd-submodel.mjs +8 -5
  133. package/src/tui/cmd-undo.mjs +4 -3
  134. package/src/tui/display-budget.mjs +184 -0
  135. package/src/tui/index.mjs +69 -40
  136. package/src/tui/key-handler-search.mjs +9 -1
  137. package/src/tui/key-handler.mjs +61 -17
  138. package/src/tui/key-modes.mjs +86 -8
  139. package/src/tui/layout.mjs +18 -10
  140. package/src/tui/ledger-surface.mjs +69 -0
  141. package/src/tui/model-catalog.mjs +89 -0
  142. package/src/tui/model-picker.mjs +498 -0
  143. package/src/tui/mouse.mjs +47 -10
  144. package/src/tui/pickers.mjs +28 -410
  145. package/src/tui/render-frame.mjs +38 -16
  146. package/src/tui/render-loop.mjs +2 -0
  147. package/src/tui/render-segments.mjs +5 -19
  148. package/src/tui/render.mjs +37 -5
  149. package/src/tui/slash-commands.mjs +2 -2
  150. package/src/tui/startup.mjs +45 -13
  151. package/src/tui/subagent-blocks.mjs +70 -90
  152. package/src/tui/subagent-children.mjs +125 -67
  153. package/src/tui/subagent-freeze.mjs +48 -45
  154. package/src/tui/subagent-panel.mjs +21 -66
  155. package/src/tui/suspension-drive.mjs +31 -83
  156. package/src/tui/tool-args.mjs +9 -4
  157. package/src/tui/tool-display.mjs +20 -5
  158. package/src/tui/tool-events.mjs +76 -30
  159. package/src/tui/tui-lifecycle.mjs +27 -7
  160. package/src/tui/wizard.mjs +52 -18
  161. package/src/tui/wrapped-spawn.mjs +54 -0
  162. package/src/prompts/coder.md +0 -13
  163. package/src/prompts/discipline.md +0 -84
  164. package/src/prompts/eng-coder.md +0 -19
  165. package/src/prompts/engineering-sub.md +0 -14
  166. package/src/prompts/engineering.md +0 -87
  167. package/src/prompts/explore.md +0 -12
  168. package/src/prompts/main.md +0 -34
  169. package/src/prompts/methodology-template.md +0 -38
  170. package/src/prompts/plan.md +0 -9
  171. package/src/prompts/system.md +0 -44
@@ -161,14 +161,19 @@ export function layoutInput(chars, cursor, width) {
161
161
  const PROMPT = "\u25b8 " // first-line prefix (display width 2)
162
162
  const CONT = " " // continuation prefix (width 2) — keeps left edge aligned
163
163
  const lines = []
164
+ const starts = [] // lineStarts so far(第 31 批 additive):每个可视行的行首下标
165
+ let lineStart = 0 // 当前行的行首下标(chars 索引空间)
164
166
  let cursorLine = 0
165
167
  let cursorCol = 0
166
168
  let cur = ""
167
169
  let col = 0
168
170
  let firstLine = true
169
171
  const avail = () => width - 2 // every line reserves 2 cols for its prefix
170
- const flush = () => {
172
+ // next = 下一行的行首下标(折行 = 当前字符 i;行尾 \n 之后 = i + 1)
173
+ const flush = (next) => {
171
174
  lines.push((firstLine ? PROMPT : CONT) + cur)
175
+ starts.push(lineStart)
176
+ lineStart = next
172
177
  firstLine = false
173
178
  cur = ""
174
179
  col = 0
@@ -177,7 +182,7 @@ export function layoutInput(chars, cursor, width) {
177
182
  const ch = chars[i]
178
183
  if (ch !== undefined && ch !== "\n") {
179
184
  const w = charWidth(ch.codePointAt(0))
180
- if (col + w > avail()) flush()
185
+ if (col + w > avail()) flush(i)
181
186
  if (i === cursor) {
182
187
  cursorLine = lines.length
183
188
  cursorCol = 2 + col
@@ -189,12 +194,39 @@ export function layoutInput(chars, cursor, width) {
189
194
  cursorLine = lines.length
190
195
  cursorCol = 2 + col
191
196
  }
192
- if (ch === "\n") flush()
197
+ if (ch === "\n") flush(i + 1)
193
198
  }
194
199
  }
195
200
  const endsWithNewline = chars.length > 0 && chars[chars.length - 1] === "\n"
196
- if (cur || lines.length === 0 || endsWithNewline) flush()
197
- return { lines, cursorLine, cursorCol }
201
+ if (cur || lines.length === 0 || endsWithNewline) flush(chars.length)
202
+ // lineStarts[k] = 第 k 行行首;行 k 区间 = [lineStarts[k], lineStarts[k+1])(含行尾 \n);
203
+ // 末项哨兵 = chars.length(上界、不指向行)——竖移定位用(TUI-INPUT-BOX.md §1 不变量 5)。
204
+ return { lines, cursorLine, cursorCol, lineStarts: [...starts, chars.length] }
205
+ }
206
+
207
+ /** 竖直移动定位(第 31 批——TUI-INPUT-BOX.md §3 规则 2 / §9.3 #2):纯函数,
208
+ * `(chars, cursor, width, dir) → number | null`(null = 该方向无邻行)。
209
+ * 目标行显示列 = 当前光标显示列(列保持——`layoutInput` 同源行列;行前缀 2 列等宽故整体
210
+ * 相减);目标行更短则钳制到行尾(行尾含 \n → 落在 \n 前)。逐键现算——不记忆列(D-31.2)。
211
+ * 不读不写任何 state。 */
212
+ export function moveCursorVertical(chars, cursor, width, dir) {
213
+ const { cursorLine, cursorCol, lineStarts } = layoutInput(chars, cursor, width)
214
+ const lineCount = lineStarts.length - 1
215
+ const targetLine = dir === "up" ? cursorLine - 1 : cursorLine + 1
216
+ if (targetLine < 0 || targetLine >= lineCount) return null
217
+ const targetCol = Math.max(0, cursorCol - 2) // 2 列行前缀(`▸ ` / 续行空格)——各行等宽
218
+ const start = lineStarts[targetLine]
219
+ let end = lineStarts[targetLine + 1]
220
+ if (end > start && chars[end - 1] === "\n") end -= 1 // 行尾 \n 不占显示列
221
+ let col = 0
222
+ let idx = start
223
+ while (idx < end) {
224
+ const w = charWidth(chars[idx].codePointAt(0))
225
+ if (col + w > targetCol) break // 宽字符列中不落点——落在该字符起点
226
+ col += w
227
+ idx += 1
228
+ }
229
+ return idx
198
230
  }
199
231
 
200
232
  /**
@@ -42,7 +42,7 @@ export const SLASH_COMMANDS = [
42
42
  { name: "/eng", group: "Agent", desc: "toggle engineering mode — strict methodology enforcement" },
43
43
  { name: "/advisor", group: "Agent", desc: "advisor settings (model, thinking, review gate)" },
44
44
  { name: "/model", group: "Agent", desc: "select model & manage providers" },
45
- { name: "/submodel", group: "Agent", desc: "subagent model per type (explore/plan/coder/eng-coder)" },
45
+ { name: "/submodel", group: "Agent", desc: "subagent model per type (explore/plan/coder/eng-coder/eng-designer)" },
46
46
  { name: "/shell", group: "System", desc: "bash tool shell (git-bash/pwsh path; win11 cmd encoding fix)" },
47
47
  { name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
48
48
  { name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
@@ -144,7 +144,7 @@ export function createSlashCommands(ctx) {
144
144
  const match = (cands) => cands.filter((c) => c.startsWith(last)).map((c) => `${head} ${c}`)
145
145
  if (cmd === "/model" && argIndex === 0) return match(agent.providers.map((p) => p.name))
146
146
  if (cmd === "/submodel") {
147
- if (argIndex === 0) return match(["explore", "plan", "coder", "eng-coder", "reset"])
147
+ if (argIndex === 0) return match(["explore", "plan", "coder", "eng-coder", "eng-designer", "reset"])
148
148
  if (parts[1] && !["reset"].includes(parts[1]) && argIndex === 1) return match(agent.providers.map((p) => p.name))
149
149
  }
150
150
  if (cmd === "/think") {
@@ -3,6 +3,9 @@ import { ansi, C } from "./ansi.mjs"
3
3
  import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
4
4
  import { slimToolResultForDisplay } from "./tool-events.mjs"
5
5
  import { countConvLines } from "./render-conversation.mjs"
6
+ // TUI-OOM-ROOTCAUSE(TUI.md §15.3.3):恢复/翻页行过额度 + state.lines 总量对账。
7
+ import { capLine, accountLine, accountAll, syncLineBudget } from "./display-budget.mjs"
8
+ import { shiftFreezeAnchors } from "./subagent-blocks.mjs"
6
9
 
7
10
  /** Lazy history window (parity with VS Code HISTORY_PAGE_SIZE): first paint loads
8
11
  * the latest INITIAL_HISTORY_MESSAGES, then PgUp-at-top loads HISTORY_PAGE_MESSAGES
@@ -115,24 +118,34 @@ export function historyToLines(history, startIdx, endIdx) {
115
118
  * INITIAL_HISTORY_MESSAGES, set the _history* counters the loadOlder closure
116
119
  * reads, and prepend the "… N more earlier messages" placeholder. Shared by
117
120
  * startup restore and /session switching (the display snapshot is deprecated).
121
+ * TUI-OOM-ROOTCAUSE(SESSION.md §14.3.6):desc = `{ history, total, base? }`——
122
+ * history = 尾窗(≤200,含 ±1 页沿头一条——跨页回标判定用;不渲染)、total = 绝对总条数
123
+ * (「N messages」标签口径)、base = history[0] 的绝对序号(缺省由 total − len 推导)。
118
124
  */
119
- export function restoreLines(state, history) {
120
- const total = Array.isArray(history) ? history.length : 0
121
- if (total === 0) return
122
- const start = Math.max(0, total - INITIAL_HISTORY_MESSAGES)
125
+ export function restoreLines(state, desc) {
126
+ const window = Array.isArray(desc?.history) ? desc.history : (Array.isArray(desc) ? desc : [])
127
+ const total = Number.isFinite(desc?.total) ? desc.total : window.length
128
+ if (window.length === 0 || total === 0) return
129
+ const base = Number.isFinite(desc?.base) ? desc.base : Math.max(0, total - window.length)
130
+ // 已载入量 = 渲染窗口大小(±1 页沿头一条不计数),绝对起点 = total − loaded
131
+ const loaded = Math.min(INITIAL_HISTORY_MESSAGES, total)
132
+ const start = total - loaded
123
133
  state._lineIdCounter = state._lineIdCounter ?? 0
124
- const fresh = historyToLines(history, start, total)
134
+ const fresh = historyToLines(window, Math.max(0, start - base), window.length)
135
+ for (const l of fresh) l.text = capLine(l.text) // 行额度(§15.3.3 恢复行过 capText)
125
136
  // Stable per-line ids (P1, 2026-08-30): fold keys for tool blocks derive from
126
137
  // _lineId so loadOlder's head-unshift cannot re-bind an expanded block to a
127
138
  // different tool (positional tool-{i} keys drift under unshift).
128
139
  for (const l of fresh) l._lineId = ++state._lineIdCounter
129
140
  state.lines.push(...fresh)
130
- state._historyLoaded = total - start
141
+ state._historyLoaded = loaded
131
142
  state._historyTotal = total
132
143
  state._hasOlder = start > 0
133
144
  if (state._hasOlder) {
134
145
  state.lines.unshift({ text: `… ${start} more earlier messages (PgUp at top to load)`, color: C.dim })
135
146
  }
147
+ accountAll(state) // 行集重建 → 总量直算重对账(§15.3.4)
148
+ syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
136
149
  }
137
150
  /** 懒加载更早历史(2026-08-31 用户约定:"滚动到头自动加载"——滚轮/PgUp 到顶皆触发;
138
151
  * 2026-08-31 前只挂 PgUp 键 = 违约,滚轮到头无反应)。加载后滚动补偿保持锚定。
@@ -142,10 +155,12 @@ export function restoreLines(state, history) {
142
155
  export function createLoadOlder({ agent, state, render }) {
143
156
  return () => {
144
157
  if (!state._hasOlder) return
145
- const full = agent._fullHistory ?? []
158
+ // 翻页锚 = 恢复时点 total(state._historyTotal)——活消息增长不再使页码漂移
159
+ // (§14.1 事实 4 / §14.3.6 顺带修复)。
160
+ const total = state._historyTotal ?? 0
146
161
  const loaded = state._historyLoaded
147
- const start = Math.max(0, full.length - loaded - HISTORY_PAGE_MESSAGES)
148
- const end = full.length - loaded
162
+ const start = Math.max(0, total - loaded - HISTORY_PAGE_MESSAGES)
163
+ const end = total - loaded
149
164
  if (start >= end) return
150
165
 
151
166
  const d = state.dims ? state.dims.get() : {}
@@ -154,14 +169,29 @@ export function createLoadOlder({ agent, state, render }) {
154
169
 
155
170
  if (state.lines[0]?.text?.startsWith("… ")) state.lines.shift()
156
171
  state._lineIdCounter = state._lineIdCounter ?? 0
157
- const older = historyToLines(full, start, end)
172
+ // 页源(§14.3.6):绑定态 → store.page 绝对区间 + ±1 页沿(页前一消息供跨页回合
173
+ // 标签判定、页后一消息供 tool_result 配对);未绑定(模式 F)→ 内存全量数组回退。
174
+ const store = agent._recordStore
175
+ let older
176
+ if (store?.page) {
177
+ const { messages, base } = store.page(start, end, { margin: 1 })
178
+ older = historyToLines(messages, Math.max(0, start - base), Math.min(messages.length, end - base))
179
+ } else {
180
+ older = historyToLines(agent._fullHistory ?? [], start, end)
181
+ }
182
+ for (const l of older) l.text = capLine(l.text) // 行额度(§15.3.3 翻页行过 capText)
158
183
  for (const l of older) l._lineId = ++state._lineIdCounter
159
184
  state.lines.unshift(...older)
185
+ for (const l of older) accountLine(state, l)
160
186
  state._historyLoaded += end - start
161
187
  state._hasOlder = start > 0
162
188
  if (state._hasOlder) {
163
- state.lines.unshift({ text: `… ${start} more earlier messages (scroll to top to load)`, color: C.dim })
189
+ const ph = { text: `… ${start} more earlier messages (scroll to top to load)`, color: C.dim }
190
+ state.lines.unshift(ph)
191
+ accountLine(state, ph)
164
192
  }
193
+ // 页内不裁头(保锚定)——§15.3.3:总量的裁头由 syncLineBudget 兜(此处只对账字符预算)
194
+ syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
165
195
 
166
196
  const after = countConvLines(state, cols, (state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24))
167
197
  state.scroll += Math.max(0, after - before)
@@ -194,8 +224,10 @@ export function showStartup(ctx) {
194
224
  // Recover previous session: rebuild from history (lazy — display snapshot is
195
225
  // deprecated; it drifted out of sync with history on VS Code writes).
196
226
  if (opts.restored?.history?.length) {
197
- restoreLines(state, opts.restored.history)
198
- pushLabel(`── Restored previous session (${opts.restored.history.length} messages); /new for a fresh session ──`, C.warn)
227
+ restoreLines(state, opts.restored)
228
+ // 「N messages」标签口径 = total(§14.3.6——非窗口长度)
229
+ const total = Number.isFinite(opts.restored.total) ? opts.restored.total : opts.restored.history.length
230
+ pushLabel(`── Restored previous session (${total} messages); /new for a fresh session ──`, C.warn)
199
231
  }
200
232
 
201
233
  // Hint when multiple sessions exist
@@ -4,27 +4,34 @@
4
4
  * undefined = sync), started, done, doneAt, blocks:
5
5
  * [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError,
6
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)。
7
+ * children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容并入本块 blocks——见
8
+ * subagent-children.mjs)}。
9
+ * 职责:前缀路由(parseRelayPath——**SUBAGENT-TAIL(D-ST1):嵌套段内容行并入外层
10
+ * blocks——append 目标上移,子块载体只余守护元数据**)、
11
+ * 事件 token 解析(**内层 ⟦ev⟧done/stopped 生成侧补发射(D-R23c1)路由到子块
12
+ * 定格;F2:done 后完全定格——迟到 chunk 丢弃**)、kind 合并、N2 单环 500 显示行上限
13
+ * (SUBAGENT-TAIL D-ST4——内层行同环计数、单载体最旧先行)、N1 渲染节流、
14
+ * 完成冻结(freezeSubTaskLines 家族——锚点 splice)。显示契约 = docs/design/TUI.md §6。
12
15
  */
13
16
 
14
17
  import { C } from "./ansi.mjs"
18
+ // 第 27 批 §12.3①/②:relay 前缀文法单一权威 = src/agent/relay-prefix.mjs——本文件
19
+ // 不再自持前缀正则/解析副本(防第二套平行正则再漂移)。
20
+ import { parseRelayPath } from "../agent/relay-prefix.mjs"
15
21
  import { describeToolArgs } from "./tool-args.mjs"
16
22
  import {
17
- SUB_BLOCK_LINE_LIMIT, appendSubBlock, appendSubChild, ensureSubChild, descendSubChild,
23
+ appendSubBlock, descendSubChild,
18
24
  closeSubChild,
19
25
  } from "./subagent-children.mjs"
20
26
  export { SUB_BLOCK_LINE_LIMIT, appendSubBlock } from "./subagent-children.mjs"
21
- // 2026-09-05 module-split:finish/freeze 族 + §19.6 面板镜像迁 subagent-freeze.mjs——
27
+ // 2026-09-05 module-split:finish/freeze 族 + §19.6 面板现算迁 subagent-freeze.mjs——
22
28
  // 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"
29
+ // CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):面板手工镜像退役——re-export 面换现算导出
30
+ // computePanelBlocks(读时现算),本文件全部刷镜调用点删除(单账本——变更点不再手动同步)。
31
+ import { freezeSubTaskLines, finishSubTaskKey, finishSubTask, freezeDoneSubTasks, finishSubTasksByRole, freezeAllSubTasks, freezeReclaimDigestedBlocks, shiftFreezeAnchors } from "./subagent-freeze.mjs"
32
+ export { computePanelBlocks, finishSubTask, finishSubTaskKey, freezeSubTaskLines, shiftFreezeAnchors, freezeDoneSubTasks, finishSubTasksByRole, freezeAllSubTasks, freezeReclaimDigestedBlocks } from "./subagent-freeze.mjs"
25
33
 
26
- /** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
27
- export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
34
+ // 前缀文法/解析已迁 src/agent/relay-prefix.mjs(第 27 批 §12.3②)——本文件不自持副本。
28
35
  /** ⟦ev⟧ token parser:`⟦ev⟧<name>\x1e<n>\x1e<max>\x1e<phase>\x1e<detail>`。phase done
29
36
  * = async 完成即冻结(settle 时发);settled = 挂起期完成——冻结延迟至 digest 消化
30
37
  * 完成(§17.5.5 freezeReclaimDigestedBlocks 逐条回收——不等池空)或池空退出兜底补发;
@@ -38,32 +45,14 @@ export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
38
45
  * (块尚未创建)缓冲 `state._pendingAsyncKeys`——ensureSubTaskKey 块创建时应用(兜底)。 */
39
46
  export const SUB_EVENT_RE = /^⟦ev⟧(turn|approval|done|settled|stopped|queued)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e?([\s\S]*)$/
40
47
 
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
- }
48
+ // parseRelayPath 自 src/agent/relay-prefix.mjs import(第 27 批 §12.3①——文法单一权威,
49
+ // 语义与迁移前逐字一致;“嵌套解析任意深度/无前缀 → null”见该模块)。
62
50
 
63
51
  /** N1: render-layer throttle for child tool-output appends (generation relays verbatim). */
64
52
  export const SUB_RELAY_THROTTLE_MS = 250
65
- /** Roles a subagent tool child can take (finishSubTask matches the block's role). */
66
- export const SUBAGENT_ROLES = ["sub", "explore", "plan", "coder", "eng-coder"]
53
+ /** Roles a subagent tool child can take(⏹ 门控/渲染角色判据;F-2 后角色匹配完成面 =
54
+ * finishSubTasksByRole——finishSubTask 已收窄 no-op)。 */
55
+ export const SUBAGENT_ROLES = ["sub", "explore", "plan", "coder", "eng-coder", "eng-designer"]
67
56
 
68
57
  let _subRenderLast = 0
69
58
  let _subRenderTimer = null
@@ -98,14 +87,14 @@ export function ensureSubTaskKey(state, key, role) {
98
87
  key, role, model: undefined, started: Date.now(), done: false, doneAt: null,
99
88
  blocks: [], currentTool: null, toolArgs: null, turn: 0, maxTurns: 0, approval: null,
100
89
  lastError: null, dropped: 0,
90
+ _lineCount: 0, _charCount: 0, // 双维记账(TUI.md §15.3.4——行数维 + 字符维,subagent-children.mjs)
101
91
  stopped: false, // §19.5: ⟦ev⟧stopped 冻结标记(标题 "stopped")
102
- children: [], // R23: 嵌套子代理子块载体(subagent-children.mjs——D-R23a)
92
+ children: [], // SUBAGENT-TAIL: 嵌套子代理守护载体(内容并入本块——subagent-children.mjs)
103
93
  }
104
94
  // §19.5 D-M7b ①(处置 #4 兜底——2026-09-03):缺失 key 的 ⟦ev⟧async 事件已缓冲
105
95
  // pending 标志(routeSubToken——async 先于块创建到达:排队条目补位启动的时序窗口)
106
96
  // ——块创建(此处)即应用——sub.async 区块创建即知(⏹ 门控/头标判定源保真)。
107
97
  if (state._pendingAsyncKeys?.delete(key)) state.subTasks[key].async = true
108
- syncPanelSnapshot(state) // §19.6 D-P1: 块创建 → running 入镜
109
98
  }
110
99
  return state.subTasks[key]
111
100
  }
@@ -114,9 +103,10 @@ export function ensureSubTask(state, subMatch) {
114
103
  return ensureSubTaskKey(state, `${subMatch[1]}#${subMatch[2]}`, subMatch[1])
115
104
  }
116
105
 
117
- /** Append(kind 合并追加)+ N2 树级环形上限;fresh=true 强制新块(每工具调用)。
118
- * R23(2026-09-07):appendSubBlock/trimSubBlocks 实现迁 subagent-children.mjs(树级
119
- * 配额——子块计入外层 500 行环)——本文件 import + re-export 保 import 面(压缩面板等)。 */
106
+ /** Append(kind 合并追加)+ N2 单环 500 显示行上限;fresh=true 强制新块(每工具调用)。
107
+ * R23(2026-09-07):appendSubBlock/trimSubBlocks 实现迁 subagent-children.mjs——
108
+ * SUBAGENT-TAIL(D-ST4)后为单载体最旧先行(内层并入行同环计数);本文件 import +
109
+ * re-export 保 import 面(压缩面板等)。 */
120
110
  /** 事件 token → 区块头部(turn/approval 更新 turn n/max + 等待态)。事件永不进
121
111
  * blocks/主流——仅头部。@returns {boolean} 良构事件(已消费) */
122
112
  export function applySubEvent(sub, payload) {
@@ -136,10 +126,12 @@ export function applySubEvent(sub, payload) {
136
126
 
137
127
  // ─── Prefix routing(agent-turn callbacks 委派):true = 带前缀已消费;false = 主路径
138
128
 
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)。 */
129
+ /** onToken 分支:子文本 / [model] 元数据 / ⟦ev⟧ 事件。SUBAGENT-TAIL(D-ST1——supersede
130
+ * R23 子块段方案,显示契约 docs/design/TUI.md §6):head 段路由块——inner 段**内容行
131
+ * 并入外层块 blocks**(append 目标上移),子块载体只余守护元数据(done 守卫——F2
132
+ * 迟到丢弃;currentTool——fresh 判别);内层 [model] 记子块模型;内层 ⟦ev⟧ 除
133
+ * done/stopped(生成侧补发射——D-R23c1——路由子块定格)外剥除不路由(防 explore
134
+ * 进度污染外层块头——round1 #4)。 */
143
135
  export function routeSubToken(state, t, scheduleRender) {
144
136
  const path = parseRelayPath(t)
145
137
  if (!path) return false
@@ -163,7 +155,6 @@ export function routeSubToken(state, t, scheduleRender) {
163
155
  if (live.queued) {
164
156
  delete live.queued
165
157
  live.started = Date.now()
166
- syncPanelSnapshot(state) // §19.6 D-P1: queued → running 状态变更刷镜
167
158
  }
168
159
  }
169
160
  else {
@@ -182,7 +173,6 @@ export function routeSubToken(state, t, scheduleRender) {
182
173
  const live = state.subTasks?.[path.head]
183
174
  if (live && !live.done && live.async !== true) {
184
175
  delete state.subTasks[path.head]
185
- syncPanelSnapshot(state) // §19.6 D-P1: 移除后刷镜(块出面板)
186
176
  }
187
177
  scheduleRender()
188
178
  return true
@@ -195,16 +185,17 @@ export function routeSubToken(state, t, scheduleRender) {
195
185
  // queued(§20 D-SD3b)= 排队 spawn 等待块(spawn 返回即建/状态变迁刷新——覆盖式
196
186
  // 更新 sub.queued——块不可展开(无活动流);启动后 async 事件清标转 running)。
197
187
  if (payload.startsWith("⟦ev⟧")) {
198
- // R23(supersede D-M8"内层事件剥除不路由"——评审 #1 🅰):done/stopped 完成信号 =
199
- // 生成侧补发射(D-R23c1——sync 同步收尾段带完整嵌套前缀)→ 路由到子块定格
200
- // (D-R23c2——closeSubChild——不冻结外层、不落 preview);缺失子块(迟到事件/
188
+ // 内层完成信号(沿革:D-M8 子标 → R23 子块段 → SUBAGENT-TAIL 并入——显示契约
189
+ // docs/design/TUI.md §6):done/stopped = 生成侧补发射(D-R23c1——sync 同步收尾段带
190
+ // 完整嵌套前缀)→ 路由到子块定格(closeSubChild——不冻结外层、不落 preview);
191
+ // 缺失子块(迟到事件/
201
192
  // 从未开块)→ 不建幻影(no-op)。其余内层事件(turn/approval/queued/settled/
202
193
  // async/cancelled)剥除不路由(防 explore 进度污染外层块头——round1 #4)。
203
194
  if (nested) {
204
195
  const innerEv = payload.match(SUB_EVENT_RE)
205
196
  if (innerEv && (innerEv[1] === "done" || innerEv[1] === "stopped")) {
206
197
  const leaf = descendSubChild(sub, path.inner, { create: false })
207
- // §27.1 F2: done 子块完全定格——迟到 done/stopped 丢弃(重复 done 不重放
198
+ // F2 保全:done 子块完全定格——迟到 done/stopped 丢弃(重复 done 不重放
208
199
  // 定格;stopped 不打回 done 定格块)
209
200
  if (leaf && !leaf.done) closeSubChild(sub, leaf, innerEv[1] === "stopped", path.label)
210
201
  scheduleRender()
@@ -224,7 +215,6 @@ export function routeSubToken(state, t, scheduleRender) {
224
215
  sub.currentTool = null
225
216
  sub.approval = null
226
217
  sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
227
- syncPanelSnapshot(state) // §19.6 D-P1: waiting 块建/刷新刷镜
228
218
  scheduleRender()
229
219
  return true
230
220
  }
@@ -239,7 +229,6 @@ export function routeSubToken(state, t, scheduleRender) {
239
229
  sub.currentTool = null
240
230
  sub.approval = null
241
231
  sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
242
- syncPanelSnapshot(state) // §19.6 D-P1: settled → awaitingDigest 状态变更刷镜
243
232
  scheduleRender()
244
233
  return true
245
234
  }
@@ -251,7 +240,6 @@ export function routeSubToken(state, t, scheduleRender) {
251
240
  sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
252
241
  freezeSubTaskLines(state, sub)
253
242
  delete state.subTasks[sub.key]
254
- syncPanelSnapshot(state) // §19.6 D-P1: 冻结删除后刷镜
255
243
  scheduleRender()
256
244
  return true
257
245
  }
@@ -264,7 +252,6 @@ export function routeSubToken(state, t, scheduleRender) {
264
252
  sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
265
253
  freezeSubTaskLines(state, sub)
266
254
  delete state.subTasks[sub.key]
267
- syncPanelSnapshot(state) // §19.6 D-P1: 冻结删除后刷镜
268
255
  scheduleRender()
269
256
  return true
270
257
  }
@@ -276,11 +263,12 @@ export function routeSubToken(state, t, scheduleRender) {
276
263
  // parent's) — shown in the block header, NOT appended to its content stream.
277
264
  // Only treat as metadata when the model isn't set yet (it's always the FIRST token);
278
265
  // a child content token that happens to start with "[model]" must not be swallowed.
279
- // R23:内层 [model] 记到子块(D-R23a 子块状态字段——不再剥除丢弃);单层规则不变。
266
+ // SUBAGENT-TAIL:内层 [model] 记到守护载体(类目 B 字面量——不渲染、无正读者;
267
+ // docs/design/TUI.md §6);单层规则不变。
280
268
  if (payload.startsWith("[model]") && (nested || sub.model === undefined)) {
281
269
  if (nested) {
282
270
  const leaf = descendSubChild(sub, path.inner)
283
- // §27.1 F2: done 子块完全定格——迟到 [model] 不写定格块头
271
+ // F2: done 子块完全定格——迟到 [model] 不写定格块头
284
272
  if (!leaf.done && leaf.model === undefined) {
285
273
  leaf.model = payload.slice(7)
286
274
  scheduleRender()
@@ -291,82 +279,76 @@ export function routeSubToken(state, t, scheduleRender) {
291
279
  }
292
280
  return true
293
281
  }
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)
282
+ // Child LLM text → text block (N2 cap inside appendSubBlock). SUBAGENT-TAIL(D-ST1):
283
+ // 内层文本**并入外层 blocks**(append 目标上移——子块不再承载内容行,取代小节)。
284
+ // F2:done 子块完全定格——迟到 chunk 丢弃(leaf.done 守卫——免无谓 render)。
285
+ const leaf = nested ? descendSubChild(sub, path.inner) : null
286
+ if (leaf?.done) return true
287
+ appendSubBlock(sub, "text", payload)
302
288
  scheduleRender()
303
289
  return true
304
290
  }
305
291
 
306
292
  /** Child reasoning token → think block (F2: same treatment as main reasoning).
307
- * §27 R23 nested think:归属内层子块(supersede D-M8 子标渲染——D-R23a/b)。 */
293
+ * SUBAGENT-TAIL:内层 think 并入外层 blocks(append 目标上移——显示契约 TUI.md §6)。 */
308
294
  export function routeSubReasoning(state, t, scheduleRender) {
309
295
  const path = parseRelayPath(t)
310
296
  if (!path) return false
311
297
  const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
312
298
  if (!sub) return true // frozen tombstone — drop late token
313
299
  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)
300
+ const leaf = nested ? descendSubChild(sub, path.inner) : null
301
+ if (leaf?.done) return true // F2: done 子块完全定格——迟到 think 丢弃
302
+ appendSubBlock(sub, "think", path.rest)
319
303
  scheduleRender()
320
304
  return true
321
305
  }
322
306
 
323
- /** Child tool call → fresh tool block + header currentTool。§27 R23 嵌套(supersede
324
- * D-M8 子标工具行):工具行归属内层子块(子块内 `❯ tool`);currentTool 外层存全路径
325
- * (`explore#1/read`——评审 #8:外层块头状态区现状保持不缩改)+ 子块存工具名(子块内
326
- * 输出归属判别)。 */
307
+ /** Child tool call → fresh tool block + header currentTool。SUBAGENT-TAIL(D-ST1):内层
308
+ * 工具行并入外层 blocks(同款 `❯ tool` 行——取代小节);currentTool 外层存全路径
309
+ * (`explore#1/read`——评审 #8:外层块头状态区现状保持不缩改)+ 子块存工具名(守护
310
+ * 元数据——fresh 判别源,routeSubToolOutput 读)。 */
327
311
  export function routeSubToolCall(state, name, args, scheduleRender) {
328
312
  const path = parseRelayPath(name)
329
313
  if (!path) return false
330
314
  const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
331
315
  if (!sub) return true // frozen tombstone — drop late token
332
316
  const nested = path.inner.length > 0
333
- // §27.1 F2: done 子块完全定格——迟到 tool call 丢弃(不复活外层 currentTool/子块状态)
317
+ // F2: done 子块完全定格——迟到 tool call 丢弃(不复活外层 currentTool/子块状态)
334
318
  const leaf = nested ? descendSubChild(sub, path.inner) : null
335
319
  if (leaf?.done) return true
336
320
  sub.currentTool = nested ? `${path.label}/${path.rest}` : path.rest
337
321
  sub.toolArgs = args
338
322
  sub.approval = null
339
323
  const argsDesc = describeToolArgs(path.rest, args)
340
- if (nested) {
324
+ if (leaf) {
341
325
  leaf.currentTool = path.rest
342
326
  leaf.toolArgs = args
343
327
  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
328
  }
329
+ appendSubBlock(sub, "tool", `❯ ${path.rest}${argsDesc ? " " + argsDesc : ""}\n`, { fresh: true })
348
330
  scheduleRender()
349
331
  return true
350
332
  }
351
333
 
352
334
  /** Child tool output(D1 前缀 name relay)→ 追加当前 tool block。RAW 拼接(2026-09-03
353
335
  * 修复轮——relay chunk 是任意字节边界碎片,逐 chunk 补 \n 会把词拦腰断行 + 烧 N2
354
- * 配额;emit 端自带换行结构无损还原)。§27 R23 嵌套:输出归属内层子块(T-R23a.2——
355
- * 输出全在子块——外层流无混入);fresh 判别在子块层(子块 currentTool=工具名)——
356
- * 内外/同层同名工具互不串块。 */
336
+ * 配额;emit 端自带换行结构无损还原)。SUBAGENT-TAIL(D-ST1):内层输出并入外层
337
+ * blocks(append 目标上移);fresh 判别照旧在子块层(守护元数据 leaf.currentTool=
338
+ * 工具名)——内外/同层同名工具互不串块。 */
357
339
  export function routeSubToolOutput(state, name, part, scheduleRender) {
358
340
  const path = parseRelayPath(name)
359
341
  if (!path) return false
360
342
  const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
361
343
  if (!sub) return true // frozen tombstone — drop late token
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
344
+ const nested = path.inner.length > 0
345
+ const leaf = nested ? descendSubChild(sub, path.inner) : null
346
+ if (leaf?.done) return true // F2: done 子块完全定格——迟到输出丢弃(不复活 currentTool)
347
+ const toolName = path.rest
348
+ if (leaf) {
349
+ appendSubBlock(sub, "tool", part.text, { fresh: leaf.currentTool !== toolName })
350
+ if (leaf.currentTool !== toolName) leaf.currentTool = toolName
368
351
  } else {
369
- const toolName = path.rest
370
352
  appendSubBlock(sub, "tool", part.text, { fresh: sub.currentTool !== toolName })
371
353
  if (sub.currentTool !== toolName) sub.currentTool = toolName
372
354
  }
@@ -410,7 +392,6 @@ export function ensureCompressPanel(state, info = {}) {
410
392
  const messages = Number.isInteger(info.messages) && info.messages >= 0 ? info.messages : "?"
411
393
  appendSubBlock(panel, "status", "Compressing context…\n", { fresh: true })
412
394
  appendSubBlock(panel, "meta", `summarizing ${messages} messages\n`, { fresh: true })
413
- syncPanelSnapshot(state) // §19.6 D-P1: 压缩面板建/重置刷镜
414
395
  return panel
415
396
  }
416
397
 
@@ -427,7 +408,6 @@ export function markCompressFailed(state, error) {
427
408
  function freezeCompressPanel(state, panel) {
428
409
  freezeSubTaskLines(state, panel)
429
410
  delete state.subTasks[panel.key]
430
- syncPanelSnapshot(state) // §19.6 D-P1: 压缩面板冻结删除后刷镜
431
411
  }
432
412
 
433
413
  /** 完成态(onCompress):"Compressed: N tokens freed → summary (Xs)"——冻结可折叠。 */