thincoder 0.12.59 → 0.12.61

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 (192) hide show
  1. package/CHANGELOG.md +62 -4
  2. package/README.md +10 -8
  3. package/bin/thincoder.mjs +99 -133
  4. package/package.json +6 -4
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +45 -21
  7. package/src/acp.mjs +6 -1
  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 +59 -137
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +156 -359
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +27 -7
  17. package/src/agent/completion.mjs +17 -11
  18. package/src/agent/dispatch.mjs +145 -27
  19. package/src/agent/helpers.mjs +107 -13
  20. package/src/agent/record-results.mjs +55 -11
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +242 -0
  23. package/src/agent/setup-reminders.mjs +69 -0
  24. package/src/agent/setup.mjs +107 -127
  25. package/src/agent/spawn-child.mjs +55 -13
  26. package/src/agent-tools/advisor-async.mjs +346 -0
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +167 -116
  29. package/src/agent-tools/async-settle.mjs +191 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +139 -107
  32. package/src/agent-tools/design-token.mjs +117 -0
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +24 -29
  35. package/src/agent-tools/escalate-async.mjs +289 -0
  36. package/src/agent-tools/read-history.mjs +166 -32
  37. package/src/agent-tools/recent-changes.mjs +2 -1
  38. package/src/agent-tools/review-streak.mjs +93 -0
  39. package/src/agent-tools/settings.mjs +137 -34
  40. package/src/agent-tools/subagent-actions.mjs +180 -133
  41. package/src/agent-tools/subagent-async.mjs +184 -177
  42. package/src/agent-tools/subagent-panel.mjs +160 -0
  43. package/src/agent-tools/subagent-run.mjs +205 -0
  44. package/src/agent-tools/subagent-scheduler.mjs +100 -27
  45. package/src/agent-tools/subagent-spawn.mjs +453 -0
  46. package/src/agent-tools/subagent.mjs +256 -578
  47. package/src/agent-tools/verify.mjs +119 -292
  48. package/src/agent-tools.mjs +1 -0
  49. package/src/agent.mjs +89 -205
  50. package/src/cli/distill-command.mjs +12 -6
  51. package/src/cli/make-agent.mjs +26 -8
  52. package/src/cli/memory-command.mjs +4 -3
  53. package/src/cli/permission.mjs +2 -2
  54. package/src/cli/setup-wizard.mjs +42 -17
  55. package/src/completions.mjs +114 -0
  56. package/src/config-migrate.mjs +70 -0
  57. package/src/config.mjs +180 -63
  58. package/src/context.mjs +5 -147
  59. package/src/conventions.mjs +223 -0
  60. package/src/crash-reports.mjs +128 -0
  61. package/src/distill.mjs +11 -11
  62. package/src/expand-home.mjs +16 -0
  63. package/src/explore-distill.mjs +155 -0
  64. package/src/generate-title.mjs +1 -1
  65. package/src/hooks.mjs +7 -3
  66. package/src/memory/code-index.mjs +9 -3
  67. package/src/memory/code-sync.mjs +72 -32
  68. package/src/memory/core.mjs +6 -193
  69. package/src/memory/delete.mjs +236 -0
  70. package/src/memory/docs.mjs +68 -54
  71. package/src/memory/file-walk.mjs +109 -0
  72. package/src/memory/schema.mjs +15 -3
  73. package/src/memory.mjs +3 -1
  74. package/src/model-ref.mjs +66 -0
  75. package/src/model-specs.mjs +42 -8
  76. package/src/peer-domains.mjs +265 -0
  77. package/src/peer-instances.mjs +231 -0
  78. package/src/prompt-overlays.mjs +82 -0
  79. package/src/prompts/advisor-design.md +18 -75
  80. package/src/prompts/advisor-round1.md +14 -67
  81. package/src/prompts/advisor-round2.md +15 -51
  82. package/src/prompts/advisor-round3.md +15 -51
  83. package/src/prompts/common.md +115 -0
  84. package/src/prompts/consult-base.md +5 -23
  85. package/src/prompts/discipline-engineering.md +217 -0
  86. package/src/prompts/discipline-normal.md +179 -0
  87. package/src/prompts/persona-coder.md +21 -0
  88. package/src/prompts/persona-eng-coder.md +37 -0
  89. package/src/prompts/persona-eng-designer.md +55 -0
  90. package/src/prompts/persona-engineering.md +54 -0
  91. package/src/prompts/persona-explore.md +15 -0
  92. package/src/prompts/persona-normal.md +27 -0
  93. package/src/prompts/persona-plan.md +26 -0
  94. package/src/provider/anthropic.mjs +4 -4
  95. package/src/provider/core.mjs +18 -98
  96. package/src/provider/errors.mjs +101 -0
  97. package/src/provider/google.mjs +5 -6
  98. package/src/provider/index.mjs +2 -1
  99. package/src/provider/list-models.mjs +93 -0
  100. package/src/provider/rate.mjs +2 -1
  101. package/src/provider/responses.mjs +5 -3
  102. package/src/provider/retry.mjs +8 -45
  103. package/src/provider/sse.mjs +3 -4
  104. package/src/proxy.mjs +9 -14
  105. package/src/session-gc.mjs +214 -0
  106. package/src/session-guard.mjs +47 -0
  107. package/src/session-rename.mjs +38 -0
  108. package/src/session-slots.mjs +188 -60
  109. package/src/session.mjs +104 -124
  110. package/src/token-ttl.mjs +274 -0
  111. package/src/tools/{system.mjs → bash.mjs} +19 -221
  112. package/src/tools/checklist-sync.mjs +181 -0
  113. package/src/tools/checklist.mjs +52 -39
  114. package/src/tools/edit-batch.mjs +109 -10
  115. package/src/tools/edit-diff.mjs +110 -27
  116. package/src/tools/edit.md +17 -12
  117. package/src/tools/execute.mjs +31 -4
  118. package/src/tools/file.mjs +41 -16
  119. package/src/tools/git.md +1 -1
  120. package/src/tools/git.mjs +23 -34
  121. package/src/tools/glob-dialect.mjs +130 -0
  122. package/src/tools/glob.md +3 -3
  123. package/src/tools/grep.md +1 -1
  124. package/src/tools/index.mjs +9 -8
  125. package/src/tools/ops.mjs +188 -3
  126. package/src/tools/patch.mjs +3 -3
  127. package/src/tools/question.md +4 -0
  128. package/src/tools/question.mjs +26 -0
  129. package/src/tools/read.md +1 -2
  130. package/src/tools/read_image.md +1 -1
  131. package/src/tools/search.mjs +236 -0
  132. package/src/tools/shared.mjs +14 -13
  133. package/src/tools/wait_for.md +22 -0
  134. package/src/tui/agent-turn.mjs +36 -228
  135. package/src/tui/ansi.mjs +2 -0
  136. package/src/tui/clipboard.mjs +7 -1
  137. package/src/tui/cmd-advisor.mjs +3 -2
  138. package/src/tui/cmd-config.mjs +142 -30
  139. package/src/tui/cmd-eng.mjs +28 -40
  140. package/src/tui/cmd-exit.mjs +6 -8
  141. package/src/tui/cmd-mcp.mjs +8 -2
  142. package/src/tui/cmd-model.mjs +14 -12
  143. package/src/tui/cmd-new.mjs +3 -2
  144. package/src/tui/cmd-reindex.mjs +7 -0
  145. package/src/tui/cmd-session.mjs +19 -4
  146. package/src/tui/cmd-submodel.mjs +8 -5
  147. package/src/tui/cmd-think.mjs +10 -10
  148. package/src/tui/cmd-undo.mjs +4 -3
  149. package/src/tui/cmd-upgrade.mjs +19 -4
  150. package/src/tui/config-helpers.mjs +28 -16
  151. package/src/tui/distill-cmd.mjs +1 -1
  152. package/src/tui/index.mjs +40 -38
  153. package/src/tui/interaction.mjs +3 -3
  154. package/src/tui/key-handler.mjs +61 -17
  155. package/src/tui/key-modes.mjs +86 -8
  156. package/src/tui/layout.mjs +18 -10
  157. package/src/tui/model-catalog.mjs +89 -0
  158. package/src/tui/model-picker.mjs +498 -0
  159. package/src/tui/mouse.mjs +52 -9
  160. package/src/tui/pickers.mjs +28 -392
  161. package/src/tui/render-frame.mjs +32 -16
  162. package/src/tui/render-loop.mjs +2 -0
  163. package/src/tui/render-segments.mjs +12 -9
  164. package/src/tui/render.mjs +37 -5
  165. package/src/tui/slash-commands.mjs +2 -2
  166. package/src/tui/startup.mjs +4 -0
  167. package/src/tui/subagent-blocks.mjs +106 -295
  168. package/src/tui/subagent-children.mjs +162 -0
  169. package/src/tui/subagent-freeze.mjs +169 -0
  170. package/src/tui/subagent-panel.mjs +24 -31
  171. package/src/tui/suspension-drive.mjs +297 -0
  172. package/src/tui/tool-args.mjs +7 -5
  173. package/src/tui/tool-display.mjs +143 -0
  174. package/src/tui/tool-events.mjs +56 -185
  175. package/src/tui/tui-lifecycle.mjs +46 -4
  176. package/src/tui/update-notice.mjs +4 -0
  177. package/src/tui/wizard.mjs +61 -21
  178. package/src/tui/wrapped-spawn.mjs +38 -0
  179. package/src/prompts/coder.md +0 -56
  180. package/src/prompts/discipline.md +0 -102
  181. package/src/prompts/eng-coder.md +0 -44
  182. package/src/prompts/engineering-sub.md +0 -34
  183. package/src/prompts/engineering.md +0 -340
  184. package/src/prompts/explore.md +0 -21
  185. package/src/prompts/main.md +0 -56
  186. package/src/prompts/methodology-template.md +0 -58
  187. package/src/prompts/plan.md +0 -16
  188. package/src/prompts/system.md +0 -63
  189. package/src/tools/pdf-parse-text.mjs +0 -497
  190. package/src/tools/pdf-parse-xref.mjs +0 -499
  191. package/src/tools/pdf.mjs +0 -155
  192. package/src/tools/read_pdf.md +0 -21
@@ -0,0 +1,162 @@
1
+ /**
2
+ * subagent-children.mjs — 嵌套子代理子块载体数据层(显示契约:docs/design/TUI.md §6
3
+ * 「内层活动并入外层流(SUBAGENT-TAIL)」节——2026-09-11 批)。
4
+ *
5
+ * 子块 = 外层 state.subTasks 块内的嵌套载体(外层 .children[]——每层一子块——D-R23d):
6
+ * carrier = { key(父内段标 role#N), role, model, started, done, doneAt, stopped,
7
+ * blocks, currentTool, toolArgs, approval, lastError, dropped,
8
+ * blockEpoch, children: [], _lineCount }
9
+ * SUBAGENT-TAIL(D-ST1)后内层 relay 行**不再存子块**——append 目标上移外层块 blocks
10
+ * (单存储单账本;子块 blocks 恒空)。子块载体降为**守护元数据**,职责 = ① done 后
11
+ * 迟到 chunk 丢弃(F2 语义不回退)② 外层冻结时未收尾子块不悬空(closeOpenSubChildren
12
+ * ——写路径保留,无正读者)③ 内层工具 fresh 判别(leaf.currentTool)。
13
+ *
14
+ * N2 单环配额(D-ST4):全部内容行(含内层并入行)就地计入外层 500 **显示行**环——
15
+ * trim = 单载体最旧先行(原树级 trim「子块先丢 / done 豁免」随子块内容层退役)。
16
+ * 省略计数真值(N6——D-ST5):`…(已省略 N 行)` 的 N 只随内容移除增长;标记自身
17
+ * 不占额度、不被丢弃、不计入 N。
18
+ */
19
+ export const SUB_BLOCK_LINE_LIMIT = 500
20
+
21
+ /** 显示行计数(N6 口径):块尾 `\n` 的空元素不计(行结束符非空行)。 */
22
+ const countBlockLines = (text) => {
23
+ const lines = String(text ?? "").split("\n")
24
+ return lines[lines.length - 1] === "" ? lines.length - 1 : lines.length
25
+ }
26
+
27
+ /** 省略标记块判定(trim 产物——不占额度/不可被丢;N6)。 */
28
+ const isTrimMarker = (b) => b?.kind === "meta" && b._trimMarker === true
29
+
30
+ /** 载体级丢行(D-ST4 单环最旧先行):从头部内容块 FIFO 丢行;省略标记跳过(不占额度/
31
+ * 不被丢弃/不计入 N——N6)。返回实际丢弃行数(= N 增量——无幽灵行)。 */
32
+ function dropCarrierLines(carrier, want) {
33
+ if (want <= 0) return 0
34
+ let dropped = 0
35
+ while (want > dropped) {
36
+ const idx = carrier.blocks.findIndex((b) => !isTrimMarker(b))
37
+ if (idx === -1) break // 仅余省略标记(内容耗尽)——不再产生幽灵行
38
+ const first = carrier.blocks[idx]
39
+ const lines = countBlockLines(first.text)
40
+ if (lines <= 0) { carrier.blocks.splice(idx, 1); continue } // 空块无显示行:移除不计数
41
+ const take = Math.min(lines, want - dropped)
42
+ if (take >= lines) {
43
+ carrier.blocks.splice(idx, 1)
44
+ dropped += lines
45
+ } else {
46
+ first.text = first.text.split("\n").slice(take).join("\n")
47
+ dropped += take
48
+ }
49
+ }
50
+ carrier._lineCount = Math.max(0, (carrier._lineCount ?? 0) - dropped)
51
+ carrier.dropped = (carrier.dropped ?? 0) + dropped
52
+ if (dropped > 0) {
53
+ const marker = `…(已省略 ${carrier.dropped} 行)`
54
+ const head = carrier.blocks[0]
55
+ if (head && isTrimMarker(head)) head.text = marker // 既有标记原位更新(不重复 unshift)
56
+ else carrier.blocks.unshift({ kind: "meta", text: marker, _trimMarker: true }) // 标记不占额度(N6)
57
+ }
58
+ return dropped
59
+ }
60
+
61
+ /** 单载体 trim(原树级 trimSubTree 收窄——D-ST4):超 500 显示行 → 最旧先行丢行。 */
62
+ function trimSubCarrier(carrier) {
63
+ const over = (carrier._lineCount ?? 0) - SUB_BLOCK_LINE_LIMIT
64
+ if (over > 0) dropCarrierLines(carrier, over)
65
+ }
66
+
67
+ /** kind 合并追加(载体级)——净增行记账(显示行口径;省略标记不参与合并——N6)。 */
68
+ function pushBlock(carrier, kind, text, fresh) {
69
+ if (!text) return
70
+ const last = carrier.blocks.at(-1)
71
+ if (!fresh && last && last.kind === kind && !isTrimMarker(last)) {
72
+ const before = countBlockLines(last.text)
73
+ last.text += text
74
+ carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(last.text) - before
75
+ } else {
76
+ carrier.blocks.push({ kind, text })
77
+ carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(text)
78
+ }
79
+ carrier.blockEpoch = (carrier.blockEpoch ?? 0) + 1
80
+ }
81
+
82
+ /** 追加到块载体(外层块/压缩面板——自身即配额环):单载体 trim——无子块时与既有
83
+ * trimSubBlocks 同效(N2 既有测试口径不变)。SUBAGENT-TAIL:内层行经 routeSub* 也
84
+ * 走本函数(append 目标上移——单一配额环)。 */
85
+ export function appendSubBlock(sub, kind, text, { fresh = false } = {}) {
86
+ if (!text) return
87
+ pushBlock(sub, kind, text, fresh)
88
+ trimSubCarrier(sub)
89
+ }
90
+
91
+ /** R23 D-R23a:子块载体创建/定位——按段(role#N)在父载体 children 查找,缺失即建
92
+ * (每层一子块——任意 inner 深度同路径——D-R23d)。SUBAGENT-TAIL 后子块只承载守护
93
+ * 元数据(done/currentTool/children——内容行归外层 blocks)。@returns 子块载体 */
94
+ export function ensureSubChild(carrier, seg) {
95
+ carrier.children ??= []
96
+ let c = carrier.children.find((x) => x.key === seg)
97
+ if (!c) {
98
+ c = {
99
+ key: seg,
100
+ role: seg.slice(0, seg.lastIndexOf("#")),
101
+ model: undefined,
102
+ started: Date.now(), done: false, doneAt: null, stopped: false,
103
+ blocks: [], currentTool: null, toolArgs: null,
104
+ approval: null, lastError: null, dropped: 0,
105
+ blockEpoch: 0, children: [], _lineCount: 0,
106
+ }
107
+ carrier.children.push(c)
108
+ }
109
+ return c
110
+ }
111
+
112
+ /** 沿 inner 段逐层下降。create=false 时缺层返回 null(事件关闭路径——不建幻影空块)。 */
113
+ export function descendSubChild(root, inner, { create = true } = {}) {
114
+ let carrier = root
115
+ for (const seg of inner) {
116
+ if (create) carrier = ensureSubChild(carrier, seg)
117
+ else {
118
+ const next = (carrier.children ?? []).find((x) => x.key === seg)
119
+ if (!next) return null
120
+ carrier = next
121
+ }
122
+ }
123
+ return carrier === root ? null : carrier
124
+ }
125
+
126
+ /** R23 D-R23c2 子块完成定格(内层 ⟦ev⟧done/stopped → 子块尾定格——不落 preview、不冻结
127
+ * 外层);stopped = interrupted(外层 abort 传播/内层错误收尾——T-R23a.3 不悬空)。
128
+ * SUBAGENT-TAIL 后 done 为 F2 正读者(迟到 chunk 丢弃守卫);currentTool 为 fresh 判别源。
129
+ * 外层块头 currentTool 若指向该子块(innerPath 前缀——评审 #8 全路径现状保持)→ 清空
130
+ * (子块已定格——工具不再是 current)。 */
131
+ export function closeSubChild(root, child, stopped, innerPath) {
132
+ child.done = true
133
+ child.doneAt = Date.now()
134
+ if (stopped) child.stopped = true
135
+ child.currentTool = null
136
+ child.approval = null
137
+ child.blockEpoch = (child.blockEpoch ?? 0) + 1
138
+ if (root.currentTool && innerPath && String(root.currentTool).startsWith(`${innerPath}/`)) {
139
+ root.currentTool = null
140
+ root.toolArgs = null
141
+ }
142
+ }
143
+
144
+ /** R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层未收尾(外层 abort/中断)→
145
+ * 内层子块随外层冻结时定格 stopped——不悬空。SUBAGENT-TAIL:② 语义照旧保留(防御性
146
+ * 写路径——定格后无正读者、不设断言;见 docs/design/TUI.md §6 守护字段类目 A)。
147
+ * freezeSubTaskLines 冻结前调用。 */
148
+ export function closeOpenSubChildren(sub) {
149
+ const walk = (carrier) => {
150
+ for (const c of carrier.children ?? []) {
151
+ if (!c.done) {
152
+ c.done = true
153
+ c.doneAt = Date.now()
154
+ c.stopped = true
155
+ c.currentTool = null
156
+ c.approval = null
157
+ }
158
+ walk(c)
159
+ }
160
+ }
161
+ walk(sub)
162
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * subagent-freeze.mjs — 子agent 区块完成/冻结族(2026-09-05 module-split:
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
+ * (无镜像可刷——调用点全清)。
11
+ */
12
+
13
+ import { C } from "./ansi.mjs"
14
+ import { closeOpenSubChildren } from "./subagent-children.mjs"
15
+
16
+ // ─── §19.6 D-P1 面板视图(subagent panel 检查工具)───
17
+ // CLI-ACTIVITY-DEBLOAT F-3:面板区块列表由消费面**读时现算**(action:"panel" 经
18
+ // ctx.state = agent._tuiState(index.mjs startTUI 装配)取 TUI state——subTasks
19
+ // 活值纯推导——视图与用户所见一致(同一数据源单账本))。未挂载(headless/VSC/
20
+ // 子代理——agent._tuiState 缺省)→ 返 null = 无面板——消费面降级池视图/freeze 报
21
+ // 不可用(T-P5 语义不变)。status 三态映射(纯函数):done+awaitingDigest →
22
+ // "awaitingDigest";done → "done";queued(waiting/等位未启动)→ "queued";
23
+ // 其余 → "running"(§20 D-SD3b queued 态入镜口径保留)。
24
+
25
+ /** 现算面板块列表(state.subTasks 活值纯推导——无时点快照语义)。 */
26
+ export function computePanelBlocks(state) {
27
+ const subs = state?.subTasks
28
+ if (!subs) return null // 未挂载(headless/mock 无 TUI state)——无面板
29
+ const blocks = []
30
+ for (const sub of Object.values(subs)) {
31
+ // §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(视图与面板
32
+ // 所见一致——action:'panel' 视图 + freeze 门控读此态)。
33
+ const status = sub.done ? (sub.awaitingDigest ? "awaitingDigest" : "done") : (sub.queued ? "queued" : "running")
34
+ blocks.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
35
+ }
36
+ return blocks
37
+ }
38
+
39
+ /** §7.2.3 完成冻结——CLI-ACTIVITY-DEBLOAT F-2(2026-09-10)收窄:本函数是历史启发式
40
+ * 入口("最早 started"角色匹配——§15 async 化后的 7.2.3.1 实测误冻源:async eng-coder
41
+ * 先启动时 explore 完成会误冻其块)。评审 #4 路线定死:签名保留、函数体收窄为精确
42
+ * 匹配校验——本签名无 key 参数,无从精确归属 → 恒 no-op 返 null,不再猜任何块
43
+ * (宁可 no-op 不误冻——被否决备选"保留兜底加告警日志":冻错块是错动作,日志救不回)。
44
+ * finishSubTaskKey 为唯一完成路径;roles/lastError 仅签名兼容保留(老调用面 import
45
+ * 不断——误冻面归零,无 key 窗口的块由回合尾 freezeAllSubTasks 兜底清场)。
46
+ * @returns {null} 恒 null(无精确 key 命中面——无从归属不归属)。 */
47
+ export function finishSubTask(state, roles, lastError = null) {
48
+ void state; void roles; void lastError // 收窄后零副作用——参数仅签名兼容
49
+ return null
50
+ }
51
+
52
+ /** §7.2.3 sync spawn 完成精确冻结(2026-09-03):按 relay key 精确 settle。finishSubTask
53
+ * 的"最早 started"启发式只在面板单 running 块时成立——§15 async 化后 async eng-coder
54
+ * (先启动)与 sync explore 并存时 explore 完成会误冻先启动的 eng-coder 块(7.2.3.1)。
55
+ * dispatch 沿 ctx._subagentKey(relayPrefix 去尾 = `role#N`)把 key 传进 onToolResult——
56
+ * 有 key 即精确标 done(冻结载体进流 + 删条目由调用方 freezeDoneSubTasks 承接——与
57
+ * finishSubTask 同一契约);无匹配块(已冻结 tombstone/不存在)返回 null——调用方不得
58
+ * 落启发式兜底(精确 key 无匹配 = 无从归属——不误冻他块)。 */
59
+ export function finishSubTaskKey(state, key, lastError = null) {
60
+ const sub = state.subTasks?.[key]
61
+ if (!sub) return null
62
+ sub.done = true
63
+ sub.doneAt = Date.now()
64
+ sub.currentTool = null
65
+ sub.approval = null
66
+ if (lastError) sub.lastError = lastError
67
+ sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
68
+ return sub
69
+ }
70
+
71
+ /** 冻结完成/中断区块进 state.lines(§7.2 D4——_frozenSubTask 载体行,渲染端
72
+ * render-conversation 识别;折叠交互 key = sub-${key} 与运行面板同源跨冻结延续)。
73
+ * §27 R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层子块未收尾(外层 abort/
74
+ * 中断)→ 子块随外层冻结定格 stopped(closeOpenSubChildren——不悬空)。SUBAGENT-TAIL:
75
+ * ② 语义照旧(防御性保留——无正读者、不设断言;显示契约 docs/design/TUI.md §6)。
76
+ * 锚点插入(2026-09-03 修复轮):settled 块带 _freezeAt(settle 时刻流位置)——
77
+ * splice 落位使挂起期补发冻结块位于其 digest 总览文本之前;无锚点尾推不变;
78
+ * 多锚点批量冻结按降序(绝对位置 splice——先插小锚点会移走大锚点目标)。 */
79
+ export function freezeSubTaskLines(state, sub) {
80
+ if (!sub) return
81
+ closeOpenSubChildren(sub) // R23 D-R23c2——开子块随外层冻结定格 stopped(SUBAGENT-TAIL:② 防御性保留)
82
+ state._frozenSubKeys ??= new Set()
83
+ state._frozenSubKeys.add(sub.key)
84
+ sub.done = true
85
+ sub.doneAt = sub.doneAt ?? Date.now()
86
+ const anchor = sub._freezeAt ?? state.lines.length
87
+ state.lines.splice(Math.min(anchor, state.lines.length), 0, {
88
+ text: `subagent activity: ${sub.key}`, color: C.dim, _frozenSubTask: sub,
89
+ })
90
+ }
91
+
92
+ /** 头裁锚点校正(index.mjs pushLine 调用):裁 removedCount 补 1 标记行 = 净位移
93
+ * removedCount−1(code review round1 #3);在途锚点前移,min 0 兜底。 */
94
+ export function shiftFreezeAnchors(state, removedCount) {
95
+ const shift = removedCount - 1
96
+ for (const sub of Object.values(state.subTasks ?? {})) {
97
+ if (sub._freezeAt !== undefined) sub._freezeAt = Math.max(0, sub._freezeAt - shift)
98
+ }
99
+ }
100
+
101
+ /** 冻结 + 释放全部已 done 块(工具结果清扫路径)。锚点降序(后 settle 先插——
102
+ * 绝对位置 splice 语义);无锚点(-1)排末尾推;sort 稳定。 */
103
+ export function freezeDoneSubTasks(state) {
104
+ const subs = Object.values(state.subTasks ?? {})
105
+ .filter((s) => s.done)
106
+ .sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
107
+ for (const sub of subs) {
108
+ freezeSubTaskLines(state, sub)
109
+ delete state.subTasks[sub.key]
110
+ }
111
+ }
112
+
113
+ /** 按角色整组标记 done——consult N 并行 children 的会话级 settle(2026-08-30;
114
+ * F-2 收窄后 finishSubTask 已退役 no-op——本函数为唯一按角色完成面)。 */
115
+ export function finishSubTasksByRole(state, roles, lastError = null) {
116
+ state.subTasks ??= {}
117
+ const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
118
+ for (const sub of Object.values(state.subTasks)) {
119
+ if (!sub.done && roleSet.has(sub.role)) {
120
+ sub.done = true
121
+ sub.doneAt = Date.now()
122
+ sub.currentTool = null
123
+ sub.approval = null
124
+ if (lastError) sub.lastError = lastError
125
+ sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
126
+ }
127
+ }
128
+ }
129
+
130
+ /** 回合尾/挂起退出清扫(runAgentTurn finally / suspensionSession finally):冻结全部
131
+ * 剩余块——中断(Ctrl+C/错误)不留下钉住输入框的 ghost;未 done 者
132
+ * lastError="interrupted"(Ready 态跳过)。§17.5.5:挂起自然退出时本函数只兜底
133
+ * **未消化残项**(已消化块由 freezeReclaimDigestedBlocks 逐条先行回收——块回收与
134
+ * 池空解耦)。锚点降序同 freezeDoneSubTasks(挂起期 settle 锚点交错批次各按其
135
+ * settle 位置落位)。 */
136
+ export function freezeAllSubTasks(state) {
137
+ const subs = Object.values(state.subTasks ?? {})
138
+ .sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
139
+ for (const sub of subs) {
140
+ if (!sub.done) {
141
+ sub.done = true
142
+ sub.doneAt = Date.now()
143
+ if (!sub.lastError && state.status !== "Ready") sub.lastError = "interrupted"
144
+ }
145
+ freezeSubTaskLines(state, sub)
146
+ delete state.subTasks[sub.key]
147
+ }
148
+ }
149
+
150
+ /** §17.5.5 消化完成逐条冻结回收(2026-09-03 实测修订 + round1 #1 位置裁定):digest/会话内
151
+ * 用户回合消化完 pending 条目(run 首行已注入)后调用——把"已消化但仍驻留面板"的
152
+ * awaitingDigest 块立即冻结进流(不等池空——块回收与池空解耦;池空 freezeAllSubTasks
153
+ * 仅兜底未消化残项)。归属不变式:会话内任何 run 开始前 pinned 块的条目必在 pending
154
+ * (settle 即移交);run 消费后条目不在 pending 的 pinned 块 = 本 run 消化者——无需
155
+ * 快照即精确归属。位置(round1 #1 裁定——与 17.5.5 早版文本的矛盾已消解,见
156
+ * AGENT-LOOP.md §17.5.5):**settle 锚点 splice 落位——digest 总览文本之前**——同
157
+ * §7.2 D4 修复轮/D-S8 锚点语义(T-S6/T-S14 位置断言同口径)——锚点降序逐块冻结
158
+ * (splice 绝对位互不位移)。@returns {number} 回收块数 */
159
+ export function freezeReclaimDigestedBlocks(state, pendingList) {
160
+ const pend = pendingList ?? []
161
+ const targets = Object.values(state.subTasks ?? {})
162
+ .filter((s) => s.awaitingDigest && !pend.some((e) => `${e.role}#${e.id}` === s.key))
163
+ .sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1)) // 锚点降序(同 freezeAllSubTasks)
164
+ for (const sub of targets) {
165
+ freezeSubTaskLines(state, sub) // splice 落 settle 锚点(digest 总览文本之前)
166
+ delete state.subTasks[sub.key]
167
+ }
168
+ return targets.length
169
+ }
@@ -12,7 +12,10 @@
12
12
  *
13
13
  * §19.5 D-M7 ⏹:运行中(非 done)折叠头右缘停止标记(dim,仅折叠头)——点击 =
14
14
  * cancel(mouse.mjs 列级命中 _stopCol——不触发折叠翻转)。
15
- * §19.5 D-M8:本模块同时承载子标行 dim 样式(styleSubLabelRow——面板与冻结渲染共用)。
15
+ * SUBAGENT-TAIL(supersede §27 R23 子块段方案;显示契约 docs/design/TUI.md §6):嵌套
16
+ * 子代理内容行并入本块 blocks(数据层——append 目标上移)——面板与冻结渲染只读
17
+ * sub.blocks(无子块段/无子块头/无每层折叠键);折叠键只剩 `sub-${key}`(面板 ↔
18
+ * 冻结同键——D5 无缝衔接不变)。
16
19
  *
17
20
  * 中立模块(D1 评审 #6):layout.mjs 调 renderSubagentPanel 预计算面板高度
18
21
  * (subagentLines → subagentH),render-frame.mjs 直接 put 预计算行(不重复
@@ -30,26 +33,6 @@ import { sliceByWidth, stringWidth } from "./render.mjs"
30
33
  import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } from "./fold-block.mjs"
31
34
  import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
32
35
 
33
- /** 子标行行首标记(`explore#1 · `——sublabelLine 写入的字面形态)。 */
34
- const SUB_LABEL_RE = /^([\w-]+#\d+(?:\/[\w-]+#\d+)* · )/
35
-
36
- /** §19.5 D-M8 子标行 dim 样式(最终渲染点注入——行对象单色外 wrap 无法表达行内
37
- * 双色):行首 `explore#1 · ` 子标转 gray(90m),其后内容恢复该行 kind 色
38
- * (tool=cyan / text=白 / think=默认 fg——dim 属性本就由 C.reason 施加,不受
39
- * fg 切换影响)。折叠 tail / dim 行不需调用(整行已 dim)。返回新行对象。 */
40
- export function styleSubLabelRow(row) {
41
- const content = String(row.text).replace(/^│ /, "")
42
- const m = content.match(SUB_LABEL_RE)
43
- if (!m) return row
44
- const restore = row.color === C.tool ? ansi.fg(6)
45
- : row.color === C.text ? ansi.fg(7)
46
- : ansi.fg(9) // 默认 fg(think 行 C.reason=dim 属性——90m 后切回默认即还原 dim 白)
47
- return {
48
- ...row,
49
- text: `│ ${ansi.gray}${m[1]}${restore}${content.slice(m[1].length)}`,
50
- }
51
- }
52
-
53
36
  /**
54
37
  * 面板行构建(纯函数):顶部分隔线 `─` + 各运行中区块(折叠头 + tail 3 /
55
38
  * 展开全量)。maxRows = 终端行数(展开态 60% 封顶窗口化);省略 = 不封顶
@@ -71,18 +54,21 @@ export function renderSubagentPanel(state, cols, maxRows) {
71
54
  const icon = sub.approval ? "⏸" : sub.done ? "✓" : "▶"
72
55
  const elapsed = Math.floor(((sub.done ? (sub.doneAt ?? Date.now()) : Date.now()) - sub.started) / 1000)
73
56
  // §19.5 D-M7b ②: sync/async 显式头标(B 形态——不靠"没标推断")——async 由
74
- // ⟦ev⟧async 标记置位;sync 区块(无标记)显式标 sync。仅真实 subagent 角色
57
+ // ⟦ev⟧async 标记置位;sync 区块(无标记)显式标 sync。真实 subagent 角色
75
58
  // (escalate/consult/compress 等复用面板槽的条目无语义——非 spawn 角色豁免);
59
+ // §11.2 D-24b:role "advisor" 伪角色同面板(块/⏹/冻结复用——ruling ②-4 A)。
76
60
  // 冻结后保留(与 model 标识同生命周期——render-conversation frozenSubTaskLines
77
61
  // 同款 modePart)。**颜色后置注入**(code review 🔵#4):bracket 宽度预算用纯文
78
62
  // 本(dim ANSI 内嵌会被 sliceByWidth 截断在 restore 之前 → 行尾残留 dim)——
79
63
  // 截断后对完整存活的 mode word 单独套 dim + 恢复行色(自闭合——截断落在词内
80
64
  // 则 replace 不命中 → 无 ANSI 泄漏,词以行色显示)。
81
- const isSubRole = SUBAGENT_ROLES.includes(sub.role)
65
+ const isSubRole = SUBAGENT_ROLES.includes(sub.role) || sub.role === "advisor"
82
66
  // §20 D-SD3b waiting 块(sub.queued——排队 spawn 返回即建——未启动无 relay 流):
83
67
  // 括号状态词 = waiting(依赖/域冲突等位——detail 即原因)或 queued(槽满等位——
84
68
  // 状态区显示 position);不显示 sync/async 词(尚未启动——无 async 标记可言——
85
- // 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“接受无取消”旧裁)。
86
72
  const queued = sub.queued ?? null
87
73
  const statusWord = queued ? (queued.kind === "slot" ? "queued" : "waiting") : null
88
74
  const modeWord = isSubRole && !queued ? (sub.async === true ? "async" : "sync") : null
@@ -123,14 +109,22 @@ export function renderSubagentPanel(state, cols, maxRows) {
123
109
  : ""
124
110
  let headText = `${bracket} ${sliceByWidth(statePart + argSummary, Math.max(0, cols - 2 - bracketWidth))}`
125
111
  const line = { color: C.tool, _foldToggle: foldKey }
126
- // §19.5 D-M7 ⏹ + D-M7b ③ 门控:⏹ 只对 async 区块(running && SUBAGENT_ROLES &&
127
- // sub.async——sync 区块无 ⏹——杜绝"可见但不可中止"误导——用户裁定 B 形态)。
128
- // done/awaitingDigest/压缩面板(role compress)/consult 无标记(点击只对池内
129
- // 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
130
120
  // 🟡#1——glyph 在 cols−1、最右列留 margin——避免终端最末列点击不可靠/全角字形
131
121
  // 顶格被裁;命中区 = col ≥ _stopCol = cols−1——含 glyph 与其右 margin,左邻
132
122
  // padding 空格仍走折叠)。整行 ≤ cols:内容先按 cols−3 截断。
133
- if (!sub.done && sub.async === true && SUBAGENT_ROLES.includes(sub.role)) {
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")) {
134
128
  const cut = sliceByWidth(headText, Math.max(0, cols - 3))
135
129
  headText = cut + " ".repeat(Math.max(0, cols - 3 - stringWidth(cut)))
136
130
  line.text = `${headText} ${ansi.dim}⏹${ansi.reset} `
@@ -142,8 +136,7 @@ export function renderSubagentPanel(state, cols, maxRows) {
142
136
  out.push(line)
143
137
  if (isExpanded(state, foldKey)) {
144
138
  // 展开态:全量活动时间线(per-kind 着色,60% 屏封顶 + 块内滚动——公共组件)。
145
- // §19.5 D-M8:时间线行过 styleSubLabelRow——行首子标 dim、内容恢复 kind 色。
146
- const body = renderBlockTimeline(sub.blocks, cols).map(styleSubLabelRow)
139
+ const body = renderBlockTimeline(sub.blocks, cols)
147
140
  out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
148
141
  } else {
149
142
  // 折叠态:tail 3 非空 block 行(最近活动),dim。