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
@@ -1,113 +1,168 @@
1
1
  /**
2
- * subagent-children.mjs — 嵌套子代理子块载体数据层(AGENT-LOOP.md §27 R23 D-R23a/c2/NFR)。
2
+ * subagent-children.mjs — 嵌套子代理子块载体数据层(显示契约:docs/design/TUI.md §6
3
+ * 「内层活动并入外层流(SUBAGENT-TAIL)」节——2026-09-11 批)。
3
4
  *
4
5
  * 子块 = 外层 state.subTasks 块内的嵌套载体(外层 .children[]——每层一子块——D-R23d):
5
6
  * carrier = { key(父内段标 role#N), role, model, started, done, doneAt, stopped,
6
7
  * blocks, currentTool, toolArgs, approval, lastError, dropped,
7
- * blockEpoch, children: [], _lineCount }——与外层块同形(渲染复用
8
- * renderBlockTimeline/renderExpandedBlock 公共组件)。
9
- * 内容/工具/模型全部归属子块(D-R23b——不再混外层 blocks——T-R23a.2 输出全在子块)。
8
+ * blockEpoch, children: [], _lineCount }
9
+ * SUBAGENT-TAIL(D-ST1)后内层 relay 行**不再存子块**——append 目标上移外层块 blocks
10
+ * (单存储单账本;子块 blocks 恒空)。子块载体降为**守护元数据**,职责 = ① done 后
11
+ * 迟到 chunk 丢弃(F2 语义不回退)② 外层冻结时未收尾子块不悬空(closeOpenSubChildren
12
+ * ——写路径保留,无正读者)③ 内层工具 fresh 判别(leaf.currentTool)。
10
13
  *
11
- * N2 共享配额(R23 NFR——评审 #6):子块行数计入**外层** 500 行环形上限——树级 trim
12
- * (子块批量输出先丢、外层叙述保留优先——见 trimSubTree)——子块不独立扩容——面板
13
- * 不无限增长。
14
+ * N2 单环配额(D-ST4):全部内容行(含内层并入行)就地计入外层 500 **显示行**环——
15
+ * trim = 单载体最旧先行(原树级 trim「子块先丢 / done 豁免」随子块内容层退役)。
16
+ * 省略计数真值(N6——D-ST5):`…(已省略 N 行)` 的 N 只随内容移除增长;标记自身
17
+ * 不占额度、不被丢弃、不计入 N。
18
+ *
19
+ * TUI-OOM-ROOTCAUSE(TUI.md §15.1 事实 3 / §15.3.3):原环按 `\n` 计数——无换行巨 chunk
20
+ * 净增 0 行 → 环永不触发、文本无界。本模块加**字符维**(第二维——行数维逐字不动,D-TB5):
21
+ * `carrier._charCount` 与 `SUB_BLOCK_CHAR_LIMIT` 双维同时满足(先到先裁);字符维裁剪
22
+ * 按被裁文本行数折算 N(无幽灵计数——N6)。
14
23
  */
24
+ import { SUB_BLOCK_CHAR_LIMIT, MIDDLE_TRUNC_MARKER, capText } from "./display-budget.mjs"
25
+
15
26
  export const SUB_BLOCK_LINE_LIMIT = 500
16
27
 
17
- const countBlockLines = (text) => text.split("\n").length
28
+ /** 单块自身额度(TUI-OOM-ROOTCAUSE §15.1 事实 3:无换行巨 chunk —— 行数维净增 0、
29
+ * 环永不触发;入块文本先过本条(头 3/4 + 尾 1/4 + 中段标记)——单行内容不因字符维
30
+ * 裁剪被整行丢光(环总量裁剪仍按最旧先行)。 */
31
+ const SUB_BLOCK_CAP_OPTS = {
32
+ max: SUB_BLOCK_CHAR_LIMIT,
33
+ keepHead: 90_000,
34
+ keepTail: 30_000,
35
+ marker: MIDDLE_TRUNC_MARKER,
36
+ }
18
37
 
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
38
+ /** 显示行计数(N6 口径):块尾 `\n` 的空元素不计(行结束符非空行)。 */
39
+ const countBlockLines = (text) => {
40
+ const lines = String(text ?? "").split("\n")
41
+ return lines[lines.length - 1] === "" ? lines.length - 1 : lines.length
24
42
  }
25
43
 
26
- /** 载体级丢行(原 trimSubBlocks 逐块丢弃数学——整块 shift、末块按行裁切);返回实际
27
- * 丢弃行数。每载体独立 dropped 计数 + meta 省略标记(同既有 trimSubBlocks 语义)。 */
44
+ /** 省略标记块判定(trim 产物——不占额度/不可被丢;N6)。 */
45
+ const isTrimMarker = (b) => b?.kind === "meta" && b._trimMarker === true
46
+
47
+ /** 省略标记幂等更新(不占额度/不计入字符账——N6)。 */
48
+ function updateTrimMarker(carrier) {
49
+ const marker = `…(已省略 ${carrier.dropped} 行)`
50
+ const head = carrier.blocks[0]
51
+ if (head && isTrimMarker(head)) head.text = marker // 既有标记原位更新(不重复 unshift)
52
+ else carrier.blocks.unshift({ kind: "meta", text: marker, _trimMarker: true })
53
+ }
54
+
55
+ /** 单行丢弃(最旧内容块首行——字符维/行维共用):返回释放字符数(行级元数据同步)。 */
56
+ function dropFirstContentLine(carrier) {
57
+ for (;;) {
58
+ const idx = carrier.blocks.findIndex((b) => !isTrimMarker(b))
59
+ if (idx === -1) return 0 // 仅余省略标记(内容耗尽)——不再产生幽灵行
60
+ const first = carrier.blocks[idx]
61
+ const before = countBlockLines(first.text)
62
+ if (before <= 0) { carrier.blocks.splice(idx, 1); continue }
63
+ const parts = String(first.text).split("\n")
64
+ const droppedLine = parts.shift() ?? ""
65
+ const kept = parts.join("\n")
66
+ if (kept === "") carrier.blocks.splice(idx, 1)
67
+ else first.text = kept
68
+ const lineDelta = before - (kept === "" ? 0 : countBlockLines(kept))
69
+ carrier._lineCount = Math.max(0, (carrier._lineCount ?? 0) - lineDelta)
70
+ carrier.dropped = (carrier.dropped ?? 0) + lineDelta
71
+ // 释放字符数 = 本行正文 + 行分隔符(parts.join("\n") 同步移除——代码评审 #6:少记 1
72
+ // 字符会使 _charCount 偏高、触发早裁)
73
+ return droppedLine.length + (kept === "" ? 0 : 1)
74
+ }
75
+ }
76
+
77
+ /** 载体级丢行(D-ST4 单环最旧先行):从头部内容块 FIFO 丢行;省略标记跳过(不占额度/
78
+ * 不被丢弃/不计入 N——N6)。返回实际丢弃行数(= N 增量——无幽灵行)。字符账同步扣减。 */
28
79
  function dropCarrierLines(carrier, want) {
29
- if (want <= 0 || carrier.blocks.length === 0) return 0
80
+ if (want <= 0) return 0
30
81
  let dropped = 0
31
- while (want > dropped && carrier.blocks.length > 0) {
32
- const first = carrier.blocks[0]
82
+ let freedChars = 0
83
+ while (want > dropped) {
84
+ const idx = carrier.blocks.findIndex((b) => !isTrimMarker(b))
85
+ if (idx === -1) break // 仅余省略标记(内容耗尽)——不再产生幽灵行
86
+ const first = carrier.blocks[idx]
33
87
  const lines = countBlockLines(first.text)
88
+ if (lines <= 0) { carrier.blocks.splice(idx, 1); continue } // 空块无显示行:移除不计数
34
89
  const take = Math.min(lines, want - dropped)
35
90
  if (take >= lines) {
36
- carrier.blocks.shift()
91
+ carrier.blocks.splice(idx, 1)
37
92
  dropped += lines
93
+ freedChars += String(first.text).length
38
94
  } else {
39
- first.text = first.text.split("\n").slice(take).join("\n")
95
+ const kept = String(first.text).split("\n").slice(take).join("\n")
96
+ freedChars += String(first.text).length - kept.length
97
+ first.text = kept
40
98
  dropped += take
41
99
  }
42
100
  }
43
- carrier._lineCount = (carrier._lineCount ?? 0) - dropped
101
+ carrier._lineCount = Math.max(0, (carrier._lineCount ?? 0) - dropped)
44
102
  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 }
103
+ carrier._charCount = Math.max(0, (carrier._charCount ?? 0) - freedChars)
104
+ if (dropped > 0) updateTrimMarker(carrier)
49
105
  return dropped
50
106
  }
51
107
 
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)
108
+ /** 字符维裁剪(§15.3.3——无换行巨 chunk 的堵口):逐行丢最旧直至释放 ≥ want 字符;
109
+ * 行数按实际被丢行折算 N(无幽灵计数——N6);标记不占额度。返回释放字符数。 */
110
+ function dropCarrierChars(carrier, want) {
111
+ if (!(want > 0)) return 0
112
+ let freed = 0
113
+ while (freed < want) {
114
+ const got = dropFirstContentLine(carrier)
115
+ if (got <= 0) break // 内容耗尽(不再产生幽灵行)
116
+ freed += got
117
+ }
118
+ if (freed > 0) {
119
+ carrier._charCount = Math.max(0, (carrier._charCount ?? 0) - freed)
120
+ updateTrimMarker(carrier)
71
121
  }
72
- walk(root)
122
+ return freed
73
123
  }
74
124
 
75
- /** kind 合并追加(载体级)——既有 appendSubBlock 数学(净增行记账——P1)。 */
125
+ /** 单载体 trim(原树级 trimSubTree 收窄——D-ST4):超 500 显示行 → 最旧先行丢行;
126
+ * 超 SUB_BLOCK_CHAR_LIMIT 字符 → 同款最旧先行(双维先到先裁——TUI.md §15.3.4)。 */
127
+ function trimSubCarrier(carrier) {
128
+ const over = (carrier._lineCount ?? 0) - SUB_BLOCK_LINE_LIMIT
129
+ if (over > 0) dropCarrierLines(carrier, over)
130
+ const overChars = (carrier._charCount ?? 0) - SUB_BLOCK_CHAR_LIMIT
131
+ if (overChars > 0) dropCarrierChars(carrier, overChars)
132
+ }
133
+
134
+ /** kind 合并追加(载体级)——净增行记账(显示行口径;省略标记不参与合并——N6);
135
+ * 字符账同步累加(第二维——§15.3.4)。 */
76
136
  function pushBlock(carrier, kind, text, fresh) {
77
137
  if (!text) return
138
+ const capped = capText(text, SUB_BLOCK_CAP_OPTS) // 单块自身额度(无换行巨 chunk 堵口)
78
139
  const last = carrier.blocks.at(-1)
79
- if (!fresh && last && last.kind === kind) {
140
+ if (!fresh && last && last.kind === kind && !isTrimMarker(last)) {
80
141
  const before = countBlockLines(last.text)
81
- last.text += text
142
+ const beforeChars = last.text.length
143
+ last.text = capText(last.text + capped, SUB_BLOCK_CAP_OPTS)
82
144
  carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(last.text) - before
145
+ carrier._charCount = (carrier._charCount ?? 0) + (last.text.length - beforeChars)
83
146
  } else {
84
- carrier.blocks.push({ kind, text })
85
- carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(text)
147
+ carrier.blocks.push({ kind, text: capped })
148
+ carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(capped)
149
+ carrier._charCount = (carrier._charCount ?? 0) + capped.length
86
150
  }
87
151
  carrier.blockEpoch = (carrier.blockEpoch ?? 0) + 1
88
152
  }
89
153
 
90
- /** 追加到根载体(外层块/压缩面板——无父载体者自身即配额树根)。树级 trim——
91
- * 无子块时与既有 trimSubBlocks 同效(N2 既有测试口径不变)。 */
154
+ /** 追加到块载体(外层块/压缩面板——自身即配额环):单载体 trim——无子块时与既有
155
+ * trimSubBlocks 同效(N2 既有测试口径不变)。SUBAGENT-TAIL:内层行经 routeSub* 也
156
+ * 走本函数(append 目标上移——单一配额环)。 */
92
157
  export function appendSubBlock(sub, kind, text, { fresh = false } = {}) {
93
158
  if (!text) return
94
159
  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)
160
+ trimSubCarrier(sub)
107
161
  }
108
162
 
109
163
  /** R23 D-R23a:子块载体创建/定位——按段(role#N)在父载体 children 查找,缺失即建
110
- * (每层一子块——任意 inner 深度同路径——D-R23d)。@returns 子块载体 */
164
+ * (每层一子块——任意 inner 深度同路径——D-R23d)。SUBAGENT-TAIL 后子块只承载守护
165
+ * 元数据(done/currentTool/children——内容行归外层 blocks)。@returns 子块载体 */
111
166
  export function ensureSubChild(carrier, seg) {
112
167
  carrier.children ??= []
113
168
  let c = carrier.children.find((x) => x.key === seg)
@@ -119,7 +174,7 @@ export function ensureSubChild(carrier, seg) {
119
174
  started: Date.now(), done: false, doneAt: null, stopped: false,
120
175
  blocks: [], currentTool: null, toolArgs: null,
121
176
  approval: null, lastError: null, dropped: 0,
122
- blockEpoch: 0, children: [], _lineCount: 0,
177
+ blockEpoch: 0, children: [], _lineCount: 0, _charCount: 0,
123
178
  }
124
179
  carrier.children.push(c)
125
180
  }
@@ -142,6 +197,7 @@ export function descendSubChild(root, inner, { create = true } = {}) {
142
197
 
143
198
  /** R23 D-R23c2 子块完成定格(内层 ⟦ev⟧done/stopped → 子块尾定格——不落 preview、不冻结
144
199
  * 外层);stopped = interrupted(外层 abort 传播/内层错误收尾——T-R23a.3 不悬空)。
200
+ * SUBAGENT-TAIL 后 done 为 F2 正读者(迟到 chunk 丢弃守卫);currentTool 为 fresh 判别源。
145
201
  * 外层块头 currentTool 若指向该子块(innerPath 前缀——评审 #8 全路径现状保持)→ 清空
146
202
  * (子块已定格——工具不再是 current)。 */
147
203
  export function closeSubChild(root, child, stopped, innerPath) {
@@ -158,7 +214,9 @@ export function closeSubChild(root, child, stopped, innerPath) {
158
214
  }
159
215
 
160
216
  /** R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层未收尾(外层 abort/中断)→
161
- * 内层子块随外层冻结时定格 stopped——不悬空。freezeSubTaskLines 冻结前调用。 */
217
+ * 内层子块随外层冻结时定格 stopped——不悬空。SUBAGENT-TAIL:② 语义照旧保留(防御性
218
+ * 写路径——定格后无正读者、不设断言;见 docs/design/TUI.md §6 守护字段类目 A)。
219
+ * freezeSubTaskLines 冻结前调用。 */
162
220
  export function closeOpenSubChildren(sub) {
163
221
  const walk = (carrier) => {
164
222
  for (const c of carrier.children ?? []) {
@@ -1,51 +1,54 @@
1
1
  /**
2
2
  * subagent-freeze.mjs — 子agent 区块完成/冻结族(2026-09-05 module-split:
3
- * subagent-blocks.mjs 625 > 500 硬限——§19.6 面板镜像 syncPanelSnapshot + finish/
4
- * freeze 家族 verbatim 迁入,语义零变;subagent-blocks.mjs import 回(routeSub* 与
5
- * compression panel 调用点)+ re-export 保外部 import 面(index.mjs 等)。
3
+ * subagent-blocks.mjs 625 > 500 硬限——§19.6 面板现算 computePanelBlocks + finish/
4
+ * freeze 家族迁入;subagent-blocks.mjs import 回(routeSub* 与 compression panel
5
+ * 调用点)+ re-export 保外部 import 面(index.mjs 等)。
6
+ * CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):§19.6 面板**手工镜像退役**(镜像读写
7
+ * 全删)——改读时现算 computePanelBlocks(state):四字段 key/
8
+ * role/status/startedAt 均为 state.subTasks 活值纯推导(评审 #2 确证),读时现算
9
+ * 与 sync 时刻镜像等价——双账本结构性漂移根治(单账本)。完成/冻结族不再逐点刷镜
10
+ * (无镜像可刷——调用点全清)。
6
11
  */
7
12
 
8
13
  import { C } from "./ansi.mjs"
9
14
  import { closeOpenSubChildren } from "./subagent-children.mjs"
15
+ // TUI-OOM-ROOTCAUSE(TUI.md §15.3.3 落点表末行):state.lines 总量账——splice 插入路径过账。
16
+ import { accountLine } from "./display-budget.mjs"
10
17
 
11
- // ─── §19.6 D-P1 面板镜像(subagent panel 检查工具)───
12
- // TUI 装配处(index.mjs startTUI)state._agent = agent——块级状态变更点经
13
- // syncPanelSnapshot 刷新 agent._panelSnapshot(区块数组快照:{key, role,
14
- // status: running|done|awaitingDigest, startedAt}——状态变更即刷——O(n) 小——
15
- // 不逐 token——NF-P)。agent 层 subagent action:"panel" 读此镜像(视图与用户
16
- // 所见一致)+ 冻结门控(D-P3)。未挂载(headless/mock——无 state._agent)=
17
- // 无镜像——no-op(降级路径由 agent 侧判定)。
18
+ // ─── §19.6 D-P1 面板视图(subagent panel 检查工具)───
19
+ // CLI-ACTIVITY-DEBLOAT F-3:面板区块列表由消费面**读时现算**(action:"panel" 经
20
+ // ctx.state = agent._tuiState(index.mjs startTUI 装配)取 TUI state——subTasks
21
+ // 活值纯推导——视图与用户所见一致(同一数据源单账本))。未挂载(headless/VSC/
22
+ // 子代理——agent._tuiState 缺省)→ 返 null = 无面板——消费面降级池视图/freeze 报
23
+ // 不可用(T-P5 语义不变)。status 三态映射(纯函数):done+awaitingDigest →
24
+ // "awaitingDigest";done → "done";queued(waiting/等位未启动)→ "queued";
25
+ // 其余 → "running"(§20 D-SD3b queued 态入镜口径保留)。
18
26
 
19
- /** 刷新面板镜像(调用点 = 本模块全部 state.subTasks 块级变更点末尾——map 已稳定)。 */
20
- export function syncPanelSnapshot(state) {
21
- const agent = state._agent
22
- if (!agent) return // 未挂载(headless/mock 无 TUI 装配)——无镜像
23
- const snap = []
24
- for (const sub of Object.values(state.subTasks ?? {})) {
25
- // §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(镜像与面板
27
+ /** 现算面板块列表(state.subTasks 活值纯推导——无时点快照语义)。 */
28
+ export function computePanelBlocks(state) {
29
+ const subs = state?.subTasks
30
+ if (!subs) return null // 未挂载(headless/mock 无 TUI state)——无面板
31
+ const blocks = []
32
+ for (const sub of Object.values(subs)) {
33
+ // §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(视图与面板
26
34
  // 所见一致——action:'panel' 视图 + freeze 门控读此态)。
27
35
  const status = sub.done ? (sub.awaitingDigest ? "awaitingDigest" : "done") : (sub.queued ? "queued" : "running")
28
- snap.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
36
+ blocks.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
29
37
  }
30
- agent._panelSnapshot = snap
38
+ return blocks
31
39
  }
32
40
 
33
- /** Mark the earliest running child of the given role(s) as done (✓ header, frozen elapsed). */
41
+ /** §7.2.3 完成冻结——CLI-ACTIVITY-DEBLOAT F-2(2026-09-10)收窄:本函数是历史启发式
42
+ * 入口("最早 started"角色匹配——§15 async 化后的 7.2.3.1 实测误冻源:async eng-coder
43
+ * 先启动时 explore 完成会误冻其块)。评审 #4 路线定死:签名保留、函数体收窄为精确
44
+ * 匹配校验——本签名无 key 参数,无从精确归属 → 恒 no-op 返 null,不再猜任何块
45
+ * (宁可 no-op 不误冻——被否决备选"保留兜底加告警日志":冻错块是错动作,日志救不回)。
46
+ * finishSubTaskKey 为唯一完成路径;roles/lastError 仅签名兼容保留(老调用面 import
47
+ * 不断——误冻面归零,无 key 窗口的块由回合尾 freezeAllSubTasks 兜底清场)。
48
+ * @returns {null} 恒 null(无精确 key 命中面——无从归属不归属)。 */
34
49
  export function finishSubTask(state, roles, lastError = null) {
35
- state.subTasks ??= {}
36
- const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
37
- const running = Object.values(state.subTasks)
38
- .filter((s) => !s.done && roleSet.has(s.role))
39
- .sort((a, b) => a.started - b.started)
40
- if (running.length === 0) return
41
- const sub = running[0]
42
- sub.done = true
43
- sub.doneAt = Date.now()
44
- sub.currentTool = null
45
- sub.approval = null
46
- if (lastError) sub.lastError = lastError
47
- sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
48
- syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
50
+ void state; void roles; void lastError // 收窄后零副作用——参数仅签名兼容
51
+ return null
49
52
  }
50
53
 
51
54
  /** §7.2.3 sync spawn 完成精确冻结(2026-09-03):按 relay key 精确 settle。finishSubTask
@@ -64,28 +67,32 @@ export function finishSubTaskKey(state, key, lastError = null) {
64
67
  sub.approval = null
65
68
  if (lastError) sub.lastError = lastError
66
69
  sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
67
- syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
68
70
  return sub
69
71
  }
70
72
 
71
73
  /** 冻结完成/中断区块进 state.lines(§7.2 D4——_frozenSubTask 载体行,渲染端
72
74
  * render-conversation 识别;折叠交互 key = sub-${key} 与运行面板同源跨冻结延续)。
73
75
  * §27 R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层子块未收尾(外层 abort/
74
- * 中断)→ 子块随外层冻结定格 stopped(closeOpenSubChildren——不悬空)。
76
+ * 中断)→ 子块随外层冻结定格 stopped(closeOpenSubChildren——不悬空)。SUBAGENT-TAIL:
77
+ * ② 语义照旧(防御性保留——无正读者、不设断言;显示契约 docs/design/TUI.md §6)。
75
78
  * 锚点插入(2026-09-03 修复轮):settled 块带 _freezeAt(settle 时刻流位置)——
76
79
  * splice 落位使挂起期补发冻结块位于其 digest 总览文本之前;无锚点尾推不变;
77
80
  * 多锚点批量冻结按降序(绝对位置 splice——先插小锚点会移走大锚点目标)。 */
78
81
  export function freezeSubTaskLines(state, sub) {
79
82
  if (!sub) return
80
- closeOpenSubChildren(sub) // R23 D-R23c2——开子块随外层冻结定格 stopped
83
+ closeOpenSubChildren(sub) // R23 D-R23c2——开子块随外层冻结定格 stopped(SUBAGENT-TAIL:② 防御性保留)
81
84
  state._frozenSubKeys ??= new Set()
82
85
  state._frozenSubKeys.add(sub.key)
83
86
  sub.done = true
84
87
  sub.doneAt = sub.doneAt ?? Date.now()
85
88
  const anchor = sub._freezeAt ?? state.lines.length
86
- state.lines.splice(Math.min(anchor, state.lines.length), 0, {
89
+ const line = {
87
90
  text: `subagent activity: ${sub.key}`, color: C.dim, _frozenSubTask: sub,
88
- })
91
+ }
92
+ state.lines.splice(Math.min(anchor, state.lines.length), 0, line)
93
+ // TUI-OOM-ROOTCAUSE(TUI.md §15.3.3 落点表末行「冻结子代理 splice 插入——经
94
+ // syncLineBudget 同款对账」):冻结行进 state.lines 总量账(splice 插入路径)。
95
+ accountLine(state, line)
89
96
  }
90
97
 
91
98
  /** 头裁锚点校正(index.mjs pushLine 调用):裁 removedCount 补 1 标记行 = 净位移
@@ -107,11 +114,10 @@ export function freezeDoneSubTasks(state) {
107
114
  freezeSubTaskLines(state, sub)
108
115
  delete state.subTasks[sub.key]
109
116
  }
110
- syncPanelSnapshot(state) // §19.6 D-P1: 删除后刷镜(块移出面板)
111
117
  }
112
118
 
113
- /** 按角色整组标记 done——consult N 并行 children 的会话级 settle(单发
114
- * finishSubTask 只结最早一个——consult 残项 2026-08-30)。 */
119
+ /** 按角色整组标记 done——consult N 并行 children 的会话级 settle(2026-08-30;
120
+ * F-2 收窄后 finishSubTask 已退役 no-op——本函数为唯一按角色完成面)。 */
115
121
  export function finishSubTasksByRole(state, roles, lastError = null) {
116
122
  state.subTasks ??= {}
117
123
  const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
@@ -125,7 +131,6 @@ export function finishSubTasksByRole(state, roles, lastError = null) {
125
131
  sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
126
132
  }
127
133
  }
128
- syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
129
134
  }
130
135
 
131
136
  /** 回合尾/挂起退出清扫(runAgentTurn finally / suspensionSession finally):冻结全部
@@ -146,7 +151,6 @@ export function freezeAllSubTasks(state) {
146
151
  freezeSubTaskLines(state, sub)
147
152
  delete state.subTasks[sub.key]
148
153
  }
149
- syncPanelSnapshot(state) // §19.6 D-P1: 冻结回收后刷镜(块移出面板)
150
154
  }
151
155
 
152
156
  /** §17.5.5 消化完成逐条冻结回收(2026-09-03 实测修订 + round1 #1 位置裁定):digest/会话内
@@ -167,6 +171,5 @@ export function freezeReclaimDigestedBlocks(state, pendingList) {
167
171
  freezeSubTaskLines(state, sub) // splice 落 settle 锚点(digest 总览文本之前)
168
172
  delete state.subTasks[sub.key]
169
173
  }
170
- syncPanelSnapshot(state) // §19.6 D-P1: 逐条回收后刷镜(块移出面板)
171
174
  return targets.length
172
175
  }
@@ -12,8 +12,10 @@
12
12
  *
13
13
  * §19.5 D-M7 ⏹:运行中(非 done)折叠头右缘停止标记(dim,仅折叠头)——点击 =
14
14
  * cancel(mouse.mjs 列级命中 _stopCol——不触发折叠翻转)。
15
- * §27 R23(supersede §19.5 D-M8 子标方案):嵌套子代理渲染为子块段(renderSubChildSections
16
- * ——子块头 + 独立折叠键——面板与冻结渲染共用——styleSubLabelRow 已随子标机制退役)。
15
+ * SUBAGENT-TAIL(supersede §27 R23 子块段方案;显示契约 docs/design/TUI.md §6):嵌套
16
+ * 子代理内容行并入本块 blocks(数据层——append 目标上移)——面板与冻结渲染只读
17
+ * sub.blocks(无子块段/无子块头/无每层折叠键);折叠键只剩 `sub-${key}`(面板 ↔
18
+ * 冻结同键——D5 无缝衔接不变)。
17
19
  *
18
20
  * 中立模块(D1 评审 #6):layout.mjs 调 renderSubagentPanel 预计算面板高度
19
21
  * (subagentLines → subagentH),render-frame.mjs 直接 put 预计算行(不重复
@@ -31,61 +33,6 @@ import { sliceByWidth, stringWidth } from "./render.mjs"
31
33
  import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } from "./fold-block.mjs"
32
34
  import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
33
35
 
34
- /**
35
- * §27 R23 子块(嵌套子代理)段渲染——面板与冻结渲染共用(中立纯函数模块):
36
- * 每子块 = 工具式头行(`❯ explore#N · model · elapsed`——D-R23b,dim ❯ + role#N 亮 +
37
- * model/elapsed dim——与 C 工具头同排式;done/stopped 定格动词——D-R23c2)+ 折叠态
38
- * tail 2 / 展开态全量窗口(60% 封顶 + 块内滚动——fold-block 公共组件);孙块递归
39
- * (任意 inner 深度——D-R23d——每层独立折叠键)。折叠键 = `sub-{outerKey}/{innerPath}`
40
- * (D-R23e——与既有 sub-{key} 同族——_foldToggle 命中映射复用 fold-block 通用通道——
41
- * mouse.mjs 零改动)。行数配额在数据层(子块计入外层 500 行环——NFR)。
42
- */
43
-
44
- /** 子块折叠键(D-R23e):sub-{outerKey}/{innerPath}。 */
45
- export function subChildFoldKey(rootKey, innerPath) {
46
- return `sub-${rootKey}/${innerPath}`
47
- }
48
-
49
- /** 子块头行(D-R23b/c2)——返回带 _foldToggle 的行(可点击折叠/展开——fold-block 通道)。 */
50
- function subChildHeadRow(rootKey, child, innerPath, cols) {
51
- const foldKey = subChildFoldKey(rootKey, innerPath)
52
- const end = child.done ? (child.doneAt ?? Date.now()) : Date.now()
53
- const secs = Math.max(0, Math.floor((end - (child.started ?? end)) / 1000))
54
- const verb = child.done ? (child.stopped ? `stopped ${secs}s` : `done ${secs}s`) : `${secs}s`
55
- const raw = `❯ ${child.key}${child.model ? ` · ${child.model}` : ""} · ${verb}`
56
- let text = sliceByWidth(raw, Math.max(1, cols - 2))
57
- // dim 注入(截断后——自闭合:行末由打印端 reset 兜底;灰色段间 restore 恢复行基色)。
58
- const restore = ansi.fg(6) // 行基色 C.tool(cyan)
59
- if (text.startsWith("❯")) text = `${ansi.gray}❯${restore}${text.slice(1)}`
60
- const sep = text.indexOf(" · ")
61
- if (sep > 0) text = `${text.slice(0, sep)}${ansi.gray}${text.slice(sep)}`
62
- return { text, color: C.tool, _foldToggle: foldKey }
63
- }
64
-
65
- /** 子块段行(head + 折叠 tail2 / 展开窗口)——递归孙块。root = 外层块(key 为折叠前缀源)。 */
66
- export function renderSubChildSections(state, root, cols, maxRows) {
67
- const out = []
68
- const walk = (carrier, path) => {
69
- for (const child of carrier.children ?? []) {
70
- const innerPath = path ? `${path}/${child.key}` : child.key
71
- out.push(subChildHeadRow(root.key, child, innerPath, cols))
72
- const foldKey = subChildFoldKey(root.key, innerPath)
73
- if (isExpanded(state, foldKey)) {
74
- // 展开:全量时间线窗口化(per-kind 着色,60% 封顶 + 块内滚动——公共组件)
75
- const body = renderBlockTimeline(child.blocks, cols)
76
- out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: `${child.key} activity` }))
77
- } else {
78
- // 折叠:tail 2 非空行(最近活动)——dim(与既有外层折叠 tail 同型)
79
- for (const line of foldTailLines(child.blocks, 2)) {
80
- out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim, _skipDimFold: true })
81
- }
82
- }
83
- walk(child, innerPath)
84
- }
85
- }
86
- walk(root, "")
87
- return out
88
- }
89
36
  /**
90
37
  * 面板行构建(纯函数):顶部分隔线 `─` + 各运行中区块(折叠头 + tail 3 /
91
38
  * 展开全量)。maxRows = 终端行数(展开态 60% 封顶窗口化);省略 = 不封顶
@@ -109,7 +56,7 @@ export function renderSubagentPanel(state, cols, maxRows) {
109
56
  // §19.5 D-M7b ②: sync/async 显式头标(B 形态——不靠"没标推断")——async 由
110
57
  // ⟦ev⟧async 标记置位;sync 区块(无标记)显式标 sync。真实 subagent 角色
111
58
  // (escalate/consult/compress 等复用面板槽的条目无语义——非 spawn 角色豁免);
112
- // §24 D-24b:role "advisor" 伪角色同面板(块/⏹/冻结复用——ruling ②-4 A)。
59
+ // §11.2 D-24b:role "advisor" 伪角色同面板(块/⏹/冻结复用——ruling ②-4 A)。
113
60
  // 冻结后保留(与 model 标识同生命周期——render-conversation frozenSubTaskLines
114
61
  // 同款 modePart)。**颜色后置注入**(code review 🔵#4):bracket 宽度预算用纯文
115
62
  // 本(dim ANSI 内嵌会被 sliceByWidth 截断在 restore 之前 → 行尾残留 dim)——
@@ -119,7 +66,9 @@ export function renderSubagentPanel(state, cols, maxRows) {
119
66
  // §20 D-SD3b waiting 块(sub.queued——排队 spawn 返回即建——未启动无 relay 流):
120
67
  // 括号状态词 = waiting(依赖/域冲突等位——detail 即原因)或 queued(槽满等位——
121
68
  // 状态区显示 position);不显示 sync/async 词(尚未启动——无 async 标记可言——
122
- // sync 词会误导:async spawn 排队的块不是 sync);⏹ 门控不变(async 启动后才置)。
69
+ // sync 词会误导:async spawn 排队的块不是 sync);⏹ 门控(QUEUED-VISIBILITY F-2——
70
+ // 2026-09-09):queued 块头即置取消 ⏹(slot/wait/depc 三态——用户可撤销排队决策——
71
+ // 点击 = 出队——覆盖 SESSION-ACTIVITY-REVISED F-6“接受无取消”旧裁)。
123
72
  const queued = sub.queued ?? null
124
73
  const statusWord = queued ? (queued.kind === "slot" ? "queued" : "waiting") : null
125
74
  const modeWord = isSubRole && !queued ? (sub.async === true ? "async" : "sync") : null
@@ -160,14 +109,22 @@ export function renderSubagentPanel(state, cols, maxRows) {
160
109
  : ""
161
110
  let headText = `${bracket} ${sliceByWidth(statePart + argSummary, Math.max(0, cols - 2 - bracketWidth))}`
162
111
  const line = { color: C.tool, _foldToggle: foldKey }
163
- // §19.5 D-M7 ⏹ + D-M7b ③ 门控:⏹ 只对 async 区块(running && SUBAGENT_ROLES &&
164
- // sub.async——sync 区块无 ⏹——杜绝"可见但不可中止"误导——用户裁定 B 形态)。
165
- // done/awaitingDigest/压缩面板(role compress)/consult 无标记(点击只对池内
166
- // async 子代理有意义):dim 停止标记钉在折叠头右缘**内收一列**(code review
112
+ // §19.5 D-M7 ⏹ + D-M7b ③ 门控 + SYNC-CANCEL F3(2026-09-09):⏹ 只对 **live 可中止**
113
+ // 区块——async(running && sub.async——⟦ev⟧async 置位)∪ **sync registry live**
114
+ // (state._agent?._syncChildAborts?.has(sub.key)——阻塞 spawn 运行期注册——成功/折叠/
115
+ // 整回合停后 finally 注销——门控自动放下)——杜绝"可见但不可中止"误导。真实 subagent
116
+ // 角色(SUBAGENT_ROLES)+ advisor;escalate/consult/compress 无 registry 无 ⏹。
117
+ // **headless/测试无 _agent → sync 不钉 ⏹**(registry miss——零回归——AC6);async
118
+ // 区块不依赖 _agent(sub.async 置位即钉)。done/awaitingDigest 无标记(点击只对池内/
119
+ // registry 内 live 子代理有意义):dim 停止标记钉在折叠头右缘**内收一列**(code review
167
120
  // 🟡#1——glyph 在 cols−1、最右列留 margin——避免终端最末列点击不可靠/全角字形
168
121
  // 顶格被裁;命中区 = col ≥ _stopCol = cols−1——含 glyph 与其右 margin,左邻
169
122
  // padding 空格仍走折叠)。整行 ≤ cols:内容先按 cols−3 截断。
170
- if (!sub.done && sub.async === true && (SUBAGENT_ROLES.includes(sub.role) || sub.role === "advisor")) {
123
+ const registryLive = state._agent?._syncChildAborts?.has(sub.key) === true
124
+ // F-2(QUEUED-VISIBILITY——2026-09-09):门控扩 queued——排队块(sub.queued——
125
+ // slot/wait/depc 三态)头置取消 ⏹(用户可撤销排队决策)——queued 恒先于 async 置位
126
+ // (subagent-blocks queued 分支守卫)——async/registry-live 判定不变(running 停面)。
127
+ if (!sub.done && (sub.async === true || registryLive || !!queued) && (SUBAGENT_ROLES.includes(sub.role) || sub.role === "advisor")) {
171
128
  const cut = sliceByWidth(headText, Math.max(0, cols - 3))
172
129
  headText = cut + " ".repeat(Math.max(0, cols - 3 - stringWidth(cut)))
173
130
  line.text = `${headText} ${ansi.dim}⏹${ansi.reset} `
@@ -187,8 +144,6 @@ export function renderSubagentPanel(state, cols, maxRows) {
187
144
  out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim })
188
145
  }
189
146
  }
190
- // §27 R23:子块段(嵌套子代理——头 + tail2/展开窗口)随外层块渲染(面板与冻结共用)。
191
- out.push(...renderSubChildSections(state, sub, cols, maxRows))
192
147
  }
193
148
  return out
194
149
  }