thincoder 0.12.61 → 0.12.63

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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -371
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
@@ -1,8 +1,11 @@
1
- import { listSlots } from "../session.mjs"
1
+ import { listSlots } from "@thincoder/core/session.mjs"
2
2
  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-SESSION-VIEW.md §5.3/§5.4):恢复/翻页行过额度 + state.lines 总量对账。
7
+ import { capLine, accountLine, accountAll, releaseLine, 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 §6.14):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) // 行额度(§5.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) // 行集重建 → 总量直算重对账(§5.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,26 +155,43 @@ 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
+ // (§6.14 事实 4 顺带修复)。
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() : {}
152
167
  const cols = d.cols ?? ((state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80))
153
168
  const before = countConvLines(state, cols, d.rows ?? (process.stdout.rows || 24))
154
169
 
155
- if (state.lines[0]?.text?.startsWith("… ")) state.lines.shift()
170
+ if (state.lines[0]?.text?.startsWith("… ")) releaseLine(state, state.lines.shift()) // 移除必出账(§5.5 记账口径——releaseLine 同额负向)
156
171
  state._lineIdCounter = state._lineIdCounter ?? 0
157
- const older = historyToLines(full, start, end)
172
+ // 页源(§6.14):绑定态 → 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) // 行额度(§5.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
+ // 页内不裁头(保锚定)——§5.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)
@@ -176,7 +206,7 @@ export function showStartup(ctx) {
176
206
  const { agent, state, opts, pushLine, pushLabel, render, startWizard } = ctx
177
207
 
178
208
  // Startup screen
179
- // 2026-09-02 Q1(SESSION.md §8 D-S2):provider 可为 null(无效 provider 被清空后用户 Esc 取消重选)
209
+ // 2026-09-02 Q1(SESSION.md §6.8 D-S2):provider 可为 null(无效 provider 被清空后用户 Esc 取消重选)
180
210
  // —— 可选链守卫;`!apiKey` 触发既有 wizard(其 provider 菜单列出已存在 providers,可选中恢复,F3)
181
211
  if (!agent.provider?.apiKey) {
182
212
  pushLabel(`Welcome to ThinCoder!`, ansi.bold + C.tool)
@@ -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(§6.14——非窗口长度)
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
@@ -210,7 +242,7 @@ export function showStartup(ctx) {
210
242
  * Prefers git diff incremental (fast); falls back to full scan when git is unavailable or on first run. */
211
243
  export async function backgroundIndex(ctx) {
212
244
  const { agent, state, render } = ctx
213
- const { codeSync, docSync, gitSync } = await import("../memory.mjs")
245
+ const { codeSync, docSync, gitSync } = await import("@thincoder/core/memory.mjs")
214
246
  const cwd = agent.cwd
215
247
  let codeFiles = 0, docFiles = 0
216
248
 
@@ -17,7 +17,9 @@
17
17
  import { C } from "./ansi.mjs"
18
18
  // 第 27 批 §12.3①/②:relay 前缀文法单一权威 = src/agent/relay-prefix.mjs——本文件
19
19
  // 不再自持前缀正则/解析副本(防第二套平行正则再漂移)。
20
- import { parseRelayPath } from "../agent/relay-prefix.mjs"
20
+ import { parseRelayPath } from "@thincoder/core/agent/relay-prefix.mjs"
21
+ // zero-block 批(TUI.md §6.8.3.2 P1):墓碑存活闸复活分支留痕——日志面(零新增 UI 形态)。
22
+ import { logEvent } from "@thincoder/core/log.mjs"
21
23
  import { describeToolArgs } from "./tool-args.mjs"
22
24
  import {
23
25
  appendSubBlock, descendSubChild,
@@ -28,7 +30,7 @@ export { SUB_BLOCK_LINE_LIMIT, appendSubBlock } from "./subagent-children.mjs"
28
30
  // routeSub*/compression panel 内部用本地 import;re-export 保外部 import 面(index.mjs 等)
29
31
  // CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):面板手工镜像退役——re-export 面换现算导出
30
32
  // computePanelBlocks(读时现算),本文件全部刷镜调用点删除(单账本——变更点不再手动同步)。
31
- import { freezeSubTaskLines, finishSubTaskKey, finishSubTask, freezeDoneSubTasks, finishSubTasksByRole, freezeAllSubTasks, freezeReclaimDigestedBlocks, shiftFreezeAnchors } from "./subagent-freeze.mjs"
33
+ import { freezeSubTaskLines, finishSubTaskKey, finishSubTask, freezeDoneSubTasks, finishSubTasksByRole, freezeAllSubTasks, freezeReclaimDigestedBlocks, shiftFreezeAnchors, livePoolHas, removeFrozenSubTaskLine, tombstoneSubKey } from "./subagent-freeze.mjs"
32
34
  export { computePanelBlocks, finishSubTask, finishSubTaskKey, freezeSubTaskLines, shiftFreezeAnchors, freezeDoneSubTasks, finishSubTasksByRole, freezeAllSubTasks, freezeReclaimDigestedBlocks } from "./subagent-freeze.mjs"
33
35
 
34
36
  // 前缀文法/解析已迁 src/agent/relay-prefix.mjs(第 27 批 §12.3②)——本文件不自持副本。
@@ -80,13 +82,25 @@ export function throttleSubRender(scheduleRender) {
80
82
  export function ensureSubTaskKey(state, key, role) {
81
83
  // Tombstone guard:abort 子代理冻结后晚到 token 不得复活区块(2026-08-30 残项)
82
84
  state._frozenSubKeys ??= new Set()
83
- if (state._frozenSubKeys.has(key)) return null
85
+ if (state._frozenSubKeys.has(key)) {
86
+ // 墓碑存活闸 P0-a(§6.8.3.2——zero-block 批):**墓碑只能断言「此块已终」,不得对
87
+ // 「仍在池中存活」的条目生效**——存活判据(逐字)见 livePoolHas(条目在池 ∧
88
+ // done !== true ∧ cancelled !== true)。存活 ⇒ ① 摘墓碑;② 摘旧冻结载体行
89
+ // (免一 key 两载体——折叠键共用);③ 照常建块(model / async 由后续 token 重填)
90
+ // + 留痕(**每次复活记一条(无去重状态)**)。不存活(池外 / done / cancelled
91
+ // ——含 done-in-pool)⇒ 维持丢弃(迟到 chunk 正常高频面——不留痕,D-ZB5)。
92
+ if (!livePoolHas(state, key)) return null
93
+ state._frozenSubKeys.delete(key)
94
+ removeFrozenSubTaskLine(state, key)
95
+ logEvent("ev:subagent-block-revived", { id: key })
96
+ }
84
97
  state.subTasks ??= {}
85
98
  if (!state.subTasks[key]) {
86
99
  state.subTasks[key] = {
87
100
  key, role, model: undefined, started: Date.now(), done: false, doneAt: null,
88
101
  blocks: [], currentTool: null, toolArgs: null, turn: 0, maxTurns: 0, approval: null,
89
102
  lastError: null, dropped: 0,
103
+ _lineCount: 0, _charCount: 0, // 双维记账(TUI.md §15.3.4——行数维 + 字符维,subagent-children.mjs)
90
104
  stopped: false, // §19.5: ⟦ev⟧stopped 冻结标记(标题 "stopped")
91
105
  children: [], // SUBAGENT-TAIL: 嵌套子代理守护载体(内容并入本块——subagent-children.mjs)
92
106
  }
@@ -172,6 +186,10 @@ export function routeSubToken(state, t, scheduleRender) {
172
186
  const live = state.subTasks?.[path.head]
173
187
  if (live && !live.done && live.async !== true) {
174
188
  delete state.subTasks[path.head]
189
+ // c2(af 批——§6.8.2「已移除块不复建」):**与移除同一守卫**(命中移除才写)落键级墓碑
190
+ // ⇒ 后续 ⟦ev⟧stopped 经 ensureSubTaskKey 直接丢弃(零幻影冻结块——探针实证的
191
+ // state.lines 0 → 1 路径封死);no-block 面不写(无可防之幻影——语义零扩)。
192
+ tombstoneSubKey(state, path.head)
175
193
  }
176
194
  scheduleRender()
177
195
  return true
@@ -15,9 +15,26 @@
15
15
  * trim = 单载体最旧先行(原树级 trim「子块先丢 / done 豁免」随子块内容层退役)。
16
16
  * 省略计数真值(N6——D-ST5):`…(已省略 N 行)` 的 N 只随内容移除增长;标记自身
17
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)。
18
23
  */
24
+ import { SUB_BLOCK_CHAR_LIMIT, MIDDLE_TRUNC_MARKER, capText } from "./display-budget.mjs"
25
+
19
26
  export const SUB_BLOCK_LINE_LIMIT = 500
20
27
 
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
+ }
37
+
21
38
  /** 显示行计数(N6 口径):块尾 `\n` 的空元素不计(行结束符非空行)。 */
22
39
  const countBlockLines = (text) => {
23
40
  const lines = String(text ?? "").split("\n")
@@ -27,11 +44,42 @@ const countBlockLines = (text) => {
27
44
  /** 省略标记块判定(trim 产物——不占额度/不可被丢;N6)。 */
28
45
  const isTrimMarker = (b) => b?.kind === "meta" && b._trimMarker === true
29
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
+
30
77
  /** 载体级丢行(D-ST4 单环最旧先行):从头部内容块 FIFO 丢行;省略标记跳过(不占额度/
31
- * 不被丢弃/不计入 N——N6)。返回实际丢弃行数(= N 增量——无幽灵行)。 */
78
+ * 不被丢弃/不计入 N——N6)。返回实际丢弃行数(= N 增量——无幽灵行)。字符账同步扣减。 */
32
79
  function dropCarrierLines(carrier, want) {
33
80
  if (want <= 0) return 0
34
81
  let dropped = 0
82
+ let freedChars = 0
35
83
  while (want > dropped) {
36
84
  const idx = carrier.blocks.findIndex((b) => !isTrimMarker(b))
37
85
  if (idx === -1) break // 仅余省略标记(内容耗尽)——不再产生幽灵行
@@ -42,39 +90,63 @@ function dropCarrierLines(carrier, want) {
42
90
  if (take >= lines) {
43
91
  carrier.blocks.splice(idx, 1)
44
92
  dropped += lines
93
+ freedChars += String(first.text).length
45
94
  } else {
46
- 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
47
98
  dropped += take
48
99
  }
49
100
  }
50
101
  carrier._lineCount = Math.max(0, (carrier._lineCount ?? 0) - dropped)
51
102
  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
- }
103
+ carrier._charCount = Math.max(0, (carrier._charCount ?? 0) - freedChars)
104
+ if (dropped > 0) updateTrimMarker(carrier)
58
105
  return dropped
59
106
  }
60
107
 
61
- /** 单载体 trim(原树级 trimSubTree 收窄——D-ST4):超 500 显示行 → 最旧先行丢行。 */
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)
121
+ }
122
+ return freed
123
+ }
124
+
125
+ /** 单载体 trim(原树级 trimSubTree 收窄——D-ST4):超 500 显示行 → 最旧先行丢行;
126
+ * 超 SUB_BLOCK_CHAR_LIMIT 字符 → 同款最旧先行(双维先到先裁——TUI.md §15.3.4)。 */
62
127
  function trimSubCarrier(carrier) {
63
128
  const over = (carrier._lineCount ?? 0) - SUB_BLOCK_LINE_LIMIT
64
129
  if (over > 0) dropCarrierLines(carrier, over)
130
+ const overChars = (carrier._charCount ?? 0) - SUB_BLOCK_CHAR_LIMIT
131
+ if (overChars > 0) dropCarrierChars(carrier, overChars)
65
132
  }
66
133
 
67
- /** kind 合并追加(载体级)——净增行记账(显示行口径;省略标记不参与合并——N6)。 */
134
+ /** kind 合并追加(载体级)——净增行记账(显示行口径;省略标记不参与合并——N6);
135
+ * 字符账同步累加(第二维——§15.3.4)。 */
68
136
  function pushBlock(carrier, kind, text, fresh) {
69
137
  if (!text) return
138
+ const capped = capText(text, SUB_BLOCK_CAP_OPTS) // 单块自身额度(无换行巨 chunk 堵口)
70
139
  const last = carrier.blocks.at(-1)
71
140
  if (!fresh && last && last.kind === kind && !isTrimMarker(last)) {
72
141
  const before = countBlockLines(last.text)
73
- last.text += text
142
+ const beforeChars = last.text.length
143
+ last.text = capText(last.text + capped, SUB_BLOCK_CAP_OPTS)
74
144
  carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(last.text) - before
145
+ carrier._charCount = (carrier._charCount ?? 0) + (last.text.length - beforeChars)
75
146
  } else {
76
- carrier.blocks.push({ kind, text })
77
- 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
78
150
  }
79
151
  carrier.blockEpoch = (carrier.blockEpoch ?? 0) + 1
80
152
  }
@@ -102,7 +174,7 @@ export function ensureSubChild(carrier, seg) {
102
174
  started: Date.now(), done: false, doneAt: null, stopped: false,
103
175
  blocks: [], currentTool: null, toolArgs: null,
104
176
  approval: null, lastError: null, dropped: 0,
105
- blockEpoch: 0, children: [], _lineCount: 0,
177
+ blockEpoch: 0, children: [], _lineCount: 0, _charCount: 0,
106
178
  }
107
179
  carrier.children.push(c)
108
180
  }
@@ -12,6 +12,9 @@
12
12
 
13
13
  import { C } from "./ansi.mjs"
14
14
  import { closeOpenSubChildren } from "./subagent-children.mjs"
15
+ // TUI-OOM-ROOTCAUSE(TUI.md §15.3.3 落点表末行):state.lines 总量账——splice 插入路径过账。
16
+ // zero-block 批(§6.8.3.2):摘除路径负向出账——releaseLine(增删均须过账)。
17
+ import { accountLine, releaseLine } from "./display-budget.mjs"
15
18
 
16
19
  // ─── §19.6 D-P1 面板视图(subagent panel 检查工具)───
17
20
  // CLI-ACTIVITY-DEBLOAT F-3:面板区块列表由消费面**读时现算**(action:"panel" 经
@@ -84,9 +87,22 @@ export function freezeSubTaskLines(state, sub) {
84
87
  sub.done = true
85
88
  sub.doneAt = sub.doneAt ?? Date.now()
86
89
  const anchor = sub._freezeAt ?? state.lines.length
87
- state.lines.splice(Math.min(anchor, state.lines.length), 0, {
90
+ const line = {
88
91
  text: `subagent activity: ${sub.key}`, color: C.dim, _frozenSubTask: sub,
89
- })
92
+ }
93
+ state.lines.splice(Math.min(anchor, state.lines.length), 0, line)
94
+ // TUI-OOM-ROOTCAUSE(TUI.md §15.3.3 落点表末行「冻结子代理 splice 插入——经
95
+ // syncLineBudget 同款对账」):冻结行进 state.lines 总量账(splice 插入路径)。
96
+ accountLine(state, line)
97
+ }
98
+
99
+ /** 键级墓碑(af 批 c2——§6.8.3.3 墓碑写入单点同址):key 计入 `_frozenSubKeys` ⇒ 后续 token
100
+ * 经 `ensureSubTaskKey` 墓碑守卫直接丢弃(`⟦ev⟧cancelled` 移除块后的幻影冻结块路径封死);
101
+ * 只写键集、不写载体行;存活条目复活 ⇒ 闸门摘墓碑 + 重建块(不永久失明)。 */
102
+ export function tombstoneSubKey(state, key) {
103
+ if (!state) return
104
+ state._frozenSubKeys ??= new Set()
105
+ state._frozenSubKeys.add(String(key))
90
106
  }
91
107
 
92
108
  /** 头裁锚点校正(index.mjs pushLine 调用):裁 removedCount 补 1 标记行 = 净位移
@@ -98,6 +114,62 @@ export function shiftFreezeAnchors(state, removedCount) {
98
114
  }
99
115
  }
100
116
 
117
+ // ─── 墓碑存活闸(zero-block 批——docs/cli/design/TUI.md §6.8.3)─────────────────
118
+
119
+ /** 存活查询(P0-a/P0-b 共用单点——与墓碑写点 `freezeSubTaskLines` 同址:墓碑条件 /
120
+ * 墓碑写入 / 载体行增删单一权威)。key 形 = `role#id`(`_frozenSubKeys` / `subTasks`
121
+ * 同命名空间);**存活判据(逐字 = §6.8.3.3)**:**条目在池 ∧ `entry.done !== true` ∧
122
+ * `entry.cancelled !== true`(running/queued)**(判据先例 = `thincoder-core/agent-tools/async-discard.mjs:55`
123
+ * ——零新谓词;仅「键在池内」会把 done-in-pool 误判为存活)。
124
+ * 映射(写死 = §6.8.3.3):最后 `#` 切分 → role + id;命中 = `pool.has(String(id))` ∧
125
+ * `entry.role === role`;非池键(`compress#N` 等)恒 false(维持既有丢弃语义)。
126
+ * 降级(§6.8.3.2 末):`state._agent` 缺省(headless / 子代理内 / 夹具)⇒ 无存活信息
127
+ * ⇒ false——与批前**逐字等价**(丢弃),零回归。
128
+ * @returns {boolean} 该 key 对应条目是否池内存活 */
129
+ export function livePoolHas(state, key) {
130
+ const agent = state?._agent
131
+ if (!agent) return false
132
+ const k = String(key ?? "")
133
+ const i = k.lastIndexOf("#")
134
+ if (i <= 0 || i === k.length - 1) return false // 非池键形态(无 `#` / 空 role / 空 id)
135
+ const role = k.slice(0, i)
136
+ const id = k.slice(i + 1)
137
+ for (const pool of [agent._asyncSubagents, agent._asyncAdvisors]) {
138
+ if (!(pool instanceof Map)) continue
139
+ const entry = pool.get(id) // 池键 = String(id)(subagent-run.mjs:187 / advisor-async.mjs:410)
140
+ if (!entry || entry.role !== role) continue
141
+ if (entry.done === true || entry.cancelled === true) continue // 已终态(含 done-in-pool)
142
+ return true
143
+ }
144
+ return false
145
+ }
146
+
147
+ /** 摘旧冻结载体行(P0-a ②——复活时摘该 key 全部 `_frozenSubTask` 载体行):墓碑源已把
148
+ * 载体行 splice 进 `state.lines`(`freezeSubTaskLines`);不摘则一 key 两载体、折叠键
149
+ * `sub-${key}` 两处共用(render-segments.mjs / subagent-panel.mjs)⇒ 旧块永久留流。
150
+ * ① 逐行 `releaseLine` **负向出账**(增删均须过账——display-budget.mjs);② 摘除位
151
+ * **之后**(严格大于该位)的 `_freezeAt` 在途锚点 −1(`shiftFreezeAnchors` 同款语义
152
+ * ——锚点 = 流位置;摘除位之前的锚点不动;**恰等摘除位者亦不动**——逐字照 §6.8.3.2
153
+ * 「摘除位之后」;若需改判〔该位锚点原指被摘行槽位〕须先改设计)。
154
+ * @returns {number} 摘除的载体行数 */
155
+ export function removeFrozenSubTaskLine(state, key) {
156
+ const lines = state?.lines
157
+ if (!Array.isArray(lines)) return 0
158
+ const k = String(key ?? "")
159
+ let removed = 0
160
+ for (let i = lines.length - 1; i >= 0; i--) {
161
+ const l = lines[i]
162
+ if (l?._frozenSubTask?.key !== k) continue
163
+ releaseLine(state, l)
164
+ lines.splice(i, 1)
165
+ removed++
166
+ for (const sub of Object.values(state.subTasks ?? {})) {
167
+ if (sub._freezeAt !== undefined && sub._freezeAt > i) sub._freezeAt = Math.max(0, sub._freezeAt - 1)
168
+ }
169
+ }
170
+ return removed
171
+ }
172
+
101
173
  /** 冻结 + 释放全部已 done 块(工具结果清扫路径)。锚点降序(后 settle 先插——
102
174
  * 绝对位置 splice 语义);无锚点(-1)排末尾推;sort 稳定。 */
103
175
  export function freezeDoneSubTasks(state) {
@@ -132,11 +204,16 @@ export function finishSubTasksByRole(state, roles, lastError = null) {
132
204
  * lastError="interrupted"(Ready 态跳过)。§17.5.5:挂起自然退出时本函数只兜底
133
205
  * **未消化残项**(已消化块由 freezeReclaimDigestedBlocks 逐条先行回收——块回收与
134
206
  * 池空解耦)。锚点降序同 freezeDoneSubTasks(挂起期 settle 锚点交错批次各按其
135
- * settle 位置落位)。 */
207
+ * settle 位置落位)。
208
+ * zero-block 批 P0-b(§6.8.3.2):**墓碑存活闸——池内存活(在池 ∧ `done !== true` ∧
209
+ * `cancelled !== true`)条目跳过**(不置 done / 不写墓碑 / 不出 `subTasks`——保留 live
210
+ * 驻留,待其自身 done/stopped/settled 通道收尾);**已终态(含 done-in-pool)与池外
211
+ * 条目照旧冻结**(中断 ghost 语义不变)。存活判据单点 = `livePoolHas`。 */
136
212
  export function freezeAllSubTasks(state) {
137
213
  const subs = Object.values(state.subTasks ?? {})
138
214
  .sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
139
215
  for (const sub of subs) {
216
+ if (livePoolHas(state, sub.key)) continue // P0-b 存活跳过(墓碑只断言「此块已终」)
140
217
  if (!sub.done) {
141
218
  sub.done = true
142
219
  sub.doneAt = Date.now()
@@ -9,6 +9,9 @@
9
9
  * digest)输入禁用(INPUT-LOCK-ASYNC C'——提交吞——2026-09-09)、settle 事件驱动
10
10
  * auto-turn 消化(手动档 organize-only / AUTO 档全语义)、池空 + 无待处理输入 → 补发
11
11
  * done 冻结自然退出。状态机行表见 AGENT-LOOP.md §9.2。
12
+ * F-UC7(2026-09-19 批,AGENT-LOOP-SUBAGENT.md §6.27.12):第二开轮源 = 未 drain 的上行
13
+ * ask(`upstreamWaiting` 谓词——ask 入队即唤醒本驱动);唤醒轮同走 auto-turn(旗标
14
+ * `upstreamTurn` 供核域文本选择,提示行走第三档)。
12
15
  */
13
16
 
14
17
  // 函数级静态环(2026-09-05):drive 的 digestTurn/用户回合经 runAgentTurn 递归进入
@@ -16,15 +19,20 @@
16
19
  import { runAgentTurn } from "./agent-turn.mjs"
17
20
  import { freezeAllSubTasks, freezeReclaimDigestedBlocks } from "./subagent-blocks.mjs"
18
21
  import { sweepToolBlocks } from "./tool-events.mjs"
19
- import { logEvent } from "../log.mjs"
22
+ import { logEvent } from "@thincoder/core/log.mjs"
20
23
  import { C } from "./ansi.mjs"
21
24
  // ASYNC-RESULT-CONTAINER.md D1/D2:池 accessor(双池 absorb)+ pending 单容器停靠
22
- import { getAsyncPool, parkAsyncPending } from "../agent-tools/async-settle.mjs"
25
+ import { getAsyncPool, parkAsyncPending, releaseSettledEntry } from "@thincoder/core/agent-tools/async-settle.mjs"
26
+ // F-UC7(AGENT-LOOP-SUBAGENT.md §6.27.12——2026-09-19 批):上行 ask 开轮谓词单点(核导出)
27
+ import { upstreamWaiting } from "@thincoder/core/agent-tools/parent-channel.mjs"
28
+ // X9(2026-09-20 端差·显示面消差批):消化收尾文案单源 = 核 i18n 容器(禁第三份字面)
29
+ import { t } from "@thincoder/core/i18n.mjs"
30
+ // 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-SUBAGENT.md §6.20):中止分支「只清已死」收尾单点
31
+ import { discardAbortedPool, discardAbortedAdvisors } from "@thincoder/core/agent-tools/async-discard.mjs"
23
32
 
24
33
  // INPUT-LOCK-ASYNC(C'——2026-09-09,本档 INPUT-LOCK-ASYNC.md):R15 排队
25
34
  // 用户指令合并(§11.3 D-24c——攒批计划/合并文案/上限常量)整批废弃
26
35
  // ——busy 提交吞 + pendingInput 单槽化(至多一条待交接——单消息逐发不攒批)。
27
- // 废弃记录见 AGENT-LOOP.md §11.3。
28
36
 
29
37
  /** 后台池计数(LOGGING susp/digest 事件字段——pendingN/poolN)。
30
38
  * poolN = _asyncSubagents + _asyncAdvisors(§11.2 D-24b advisor 池同面板计数——queued
@@ -153,27 +161,40 @@ function backgroundStatusText(agent) {
153
161
  /** 消化轮:系统驱动的 auto-turn(D-S6)。手动档不传权限/问答 handler(D-S7 装配
154
162
  * 契约——denied 不弹面板、不悬挂);AUTO 档沿用普通回调(autoApprove 短路自动
155
163
  * 执行)。_suspended 保持 true:消化中 settle 延迟冻结 + 移交 pending。R17:pending
156
- * 计数/消化触发 = 任一 pending 族(T-R17j——consult/escalate 空闲 settle 也触发消化轮)。 */
157
- async function digestTurn(ctx) {
164
+ * 计数/消化触发 = 任一 pending 族(T-R17j——consult/escalate 空闲 settle 也触发消化轮)。
165
+ * F-UC7(§6.27.12.5 D):`upstream` = 未 drain 的 ask 在场(唤醒轮)——提示行走第三
166
+ * 档、旗标随 auto 轮贯通到核 `runAgent`(域文本选择面)、`digest:*` 载荷条件携带
167
+ * `upstream: true`(宿主日志区分唤醒轮与 digest 轮——两端同规)。 */
168
+ async function digestTurn(ctx, upstream = false) {
158
169
  const { agent, pushLine } = ctx
159
170
  const manual = !agent.autoApprove
160
- pushLine(manual
161
- ? "[auto-turn: digesting finished subagent reports…]"
162
- : "[auto-turn: continuing background work…]", C.dim)
171
+ // 提示行三分(§6.27.12.5 D):manual 档 ask 轮 = 第三档(「消化已完成的报告」字面在
172
+ // ask 轮相抵——本轮主事 = 答复子代理在飞提问);既有两档字面零改。
173
+ const label = manual
174
+ ? (upstream ? "[auto-turn: answering a subagent's in-flight message…]" : "[auto-turn: digesting finished subagent reports…]")
175
+ : "[auto-turn: continuing background work…]"
176
+ pushLine(label, C.dim)
163
177
  const digestCtx = manual
164
178
  ? { ...ctx, askPermission: null, askBatchPermission: null, askQuestion: null }
165
179
  : ctx
166
180
  // LOGGING:digest:* 事件(D-S9 消化轮边界——LOGGING.md F-L4 挂起态覆盖)
167
181
  const d0 = Date.now()
168
182
  const pend0 = pendingFamilyCount(agent)
169
- logEvent("digest:start", { pendingN: pend0 })
170
- await runAgentTurn(digestCtx, "", { autoTurn: true, skipSession: true })
171
- logEvent("digest:end", { pendingN: pendingFamilyCount(agent), ms: Date.now() - d0 })
183
+ logEvent("digest:start", { pendingN: pend0, ...(upstream ? { upstream: true } : {}) })
184
+ const outcome = await runAgentTurn(digestCtx, "", { autoTurn: true, upstreamTurn: upstream, skipSession: true })
185
+ logEvent("digest:end", { pendingN: pendingFamilyCount(agent), ms: Date.now() - d0, ...(upstream ? { upstream: true } : {}) })
186
+ // X9(2026-09-20 端差·显示面消差批):轮尾**可见**收尾行(修前只有 `digest:end` 日志事件——
187
+ // 用户不可见;对位 VSC `webview/chat.js:394-401`)。计数口径 = **起跑数** `pend0`
188
+ // (与 VSC `suspension.mjs:302/:314` 同源);文案单源 = 核 i18n `digest.done` /
189
+ // `digest.aborted`;终态 ≠ ok(中止/失败)⇒ aborted 形态(VSC ok 旗标同口径)。
190
+ const seconds = ((Date.now() - d0) / 1000).toFixed(1)
191
+ pushLine(t(outcome === "ok" ? "digest.done" : "digest.aborted", { n: pend0, seconds }), C.dim)
172
192
  }
173
193
 
174
194
  /**
175
195
  * §17 挂起会话驱动(D-S9 行表;由 runAgentTurn 回合尾进入,池空自然退出):
176
196
  * - suspension:池项 settle → 入 pending → 开 auto-turn(合并消化近邻 settle);
197
+ * 上行 ask 入队 → 唤醒 + 谓词 → 开唤醒轮(F-UC7——谓词先于池空退出判,见第 2 步);
177
198
  * 挂起空闲用户 Enter → pendingInput 单槽(busy 含 digest 提交吞——INPUT-LOCK-ASYNC)
178
199
  * ——用户输入优先于 digest;
179
200
  * - auto-turn:消化中 settle 不并发开新轮(单 runAgent 循环),轮末按 pending/池态
@@ -224,10 +245,13 @@ export async function suspensionSession(ctx) {
224
245
  state.status = backgroundStatusText(agent)
225
246
  continue
226
247
  }
227
- // 2. pending 任一族非空 → 合并消化轮(注入由 runAgent 首行统一完成——D-S3 单注入点;
228
- // R17 判据推广——consult/escalate 族同样触发——T-R17j)
229
- if (pendingFamiliesNonEmpty(agent)) {
230
- await digestTurn(ctx)
248
+ // 2. pending 任一族非空**或存在未 drain 的 ask** → 合并消化轮 / 唤醒轮(注入由 runAgent
249
+ // 首行 + 循环头统一完成——D-S3 单注入点;R17 判据推广——consult/escalate 族同样触发
250
+ // ——T-R17j。F-UC7(§6.27.12.5 D):谓词**必须先于第 3 步池空退出判**——「池空 +
251
+ // 队列留 ask」(子代理已 settle 且报告已消化)仍须开一轮把它 drain 出来)
252
+ const upstream = upstreamWaiting(agent)
253
+ if (pendingFamiliesNonEmpty(agent) || upstream) {
254
+ await digestTurn(ctx, upstream)
231
255
  // §17.5.5 实测修订(2026-09-03):digest 消化完成(pending 条目已注入)→ 逐条补发
232
256
  // done 冻结回收——不等池空——块从面板移除进流(settle 锚点 splice 落位——digest
233
257
  // 总览文本之前——round1 #1 裁定);池空 freeze-out 仅兜底未消化残项(挂起会话
@@ -256,15 +280,15 @@ export async function suspensionSession(ctx) {
256
280
  state._suspWake = null
257
281
  state.suspAbortArmed = false // round2 偏差 #4:会话退出即解除挂起中止武装(防跨会话粘滞)
258
282
  if (aborted) {
259
- // §15 abort 语义:清池不注入(用户显式停——不注入陈旧错误)
260
- agent._asyncSubagents?.clear()
261
- agent._asyncAdvisors?.clear()
262
- agent._asyncQueue = []
283
+ // §15 abort 语义:只清已死条目(AGENT-LOOP-SUBAGENT.md §6.20——墓碑/出池/队列剔除/
284
+ // 整批一次提醒;存活与已 settle 者留池消化——不注入陈旧错误)。
285
+ discardAbortedPool(agent)
286
+ discardAbortedAdvisors(agent)
263
287
  // ASYNC-RESULT-CONTAINER.md D2:pending 单容器——中止清容器不注入陈旧结果(四族
264
288
  // 统一一处清;consult 会话标记 stopped——settle 不入 digest 流——T-R17c)+
265
289
  // children abort。
266
290
  agent._pendingAsyncResults = []
267
- const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
291
+ const { cleanupConsultSessions } = await import("@thincoder/core/agent-tools/consult.mjs")
268
292
  cleanupConsultSessions(agent)
269
293
  // §17 round2 偏差 #2-CLI(code review round2 #2-CLI)+ INPUT-LOCK 单槽化:中止时
270
294
  // 不静默丢弃挂起期输入——Enter 已清空输入框并入 pendingInput(用户视为已发送)——
@@ -278,13 +302,15 @@ export async function suspensionSession(ctx) {
278
302
  // D-S3 ③ 兜底:退出前残余(极端竞态)直注入再退——结果零丢失(AC-S2;
279
303
  // ASYNC-RESULT-CONTAINER.md D2:pending 单容器一处清——注入器按 role 分发
280
304
  // (consult → injectConsultResult;其余 → injectAsyncResult——四族同容器)。
281
- const { injectAsyncResult } = await import("../agent-tools/subagent.mjs")
282
- const { injectConsultResult } = await import("../agent-tools/consult.mjs")
305
+ const { injectAsyncResult } = await import("@thincoder/core/agent-tools/subagent.mjs")
306
+ const { injectConsultResult } = await import("@thincoder/core/agent-tools/consult.mjs")
283
307
  const residual = agent._pendingAsyncResults
284
308
  if (residual?.length) {
285
309
  for (const e of residual.splice(0)) {
286
310
  if (e.role === "consult") await injectConsultResult(agent, e)
287
311
  else await injectAsyncResult(agent, e)
312
+ // TUI-OOM-ROOTCAUSE(§23.3.1 消费点③——挂起残差注入):注入完成 → 释放条目持有
313
+ releaseSettledEntry(e)
288
314
  }
289
315
  }
290
316
  }
@@ -12,6 +12,7 @@
12
12
  * 全量必须落行)。
13
13
  */
14
14
  import { sliceByWidth } from "./render.mjs"
15
+ import { capLines, ARGS_JSON_MAX_CHARS } from "./display-budget.mjs"
15
16
 
16
17
  /** 单行参数摘要:按工具挑关键字段(vscode 卡片头对齐),未知工具回退 JSON。 */
17
18
  export function describeToolArgs(name, args) {
@@ -74,8 +75,10 @@ export function describeToolArgs(name, args) {
74
75
  }
75
76
 
76
77
  /** 恢复路径用:全量参数 pretty JSON 的 dim 行(非空才输出)。
77
- * 与工具结果的恢复惯例一致——完整落行,超长由连续 dim 折叠收纳。 */
78
+ * 与工具结果的恢复惯例一致——完整落行;**总量额度**(TUI-OOM-ROOTCAUSE §15.3.1
79
+ * ARGS_JSON_MAX_CHARS——`write`/`apply_patch` 整文件内容进显示层的堵口):超出尾截断
80
+ * + 标记,首行保真。 */
78
81
  export function toolArgsLines(args) {
79
82
  if (!args || typeof args !== "object" || Object.keys(args).length === 0) return []
80
- return JSON.stringify(args, null, 2).split("\n")
83
+ return capLines(JSON.stringify(args, null, 2).split("\n"), ARGS_JSON_MAX_CHARS)
81
84
  }