thincoder 0.12.62 → 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 (228) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +53 -27
  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 -379
  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 +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  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-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +25 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-reindex.mjs +1 -1
  31. package/src/tui/cmd-restore.mjs +2 -2
  32. package/src/tui/cmd-session.mjs +24 -1
  33. package/src/tui/cmd-skills.mjs +1 -1
  34. package/src/tui/cmd-think.mjs +22 -9
  35. package/src/tui/config-helpers.mjs +1 -1
  36. package/src/tui/display-budget.mjs +33 -11
  37. package/src/tui/index.mjs +9 -9
  38. package/src/tui/interaction.mjs +16 -7
  39. package/src/tui/key-modes.mjs +9 -4
  40. package/src/tui/ledger-surface.mjs +26 -10
  41. package/src/tui/model-catalog.mjs +4 -4
  42. package/src/tui/model-picker.mjs +8 -7
  43. package/src/tui/mouse.mjs +11 -6
  44. package/src/tui/pickers.mjs +15 -2
  45. package/src/tui/render-conversation.mjs +1 -1
  46. package/src/tui/render-frame.mjs +10 -5
  47. package/src/tui/render-loop.mjs +1 -1
  48. package/src/tui/render-segments.mjs +3 -1
  49. package/src/tui/slash-commands.mjs +1 -1
  50. package/src/tui/startup.mjs +14 -14
  51. package/src/tui/subagent-blocks.mjs +20 -3
  52. package/src/tui/subagent-freeze.mjs +73 -2
  53. package/src/tui/suspension-drive.mjs +46 -22
  54. package/src/tui/tool-events.mjs +11 -8
  55. package/src/tui/wizard.mjs +3 -3
  56. package/src/abort-provenance.mjs +0 -116
  57. package/src/advisor/citations.mjs +0 -139
  58. package/src/advisor/compaction.mjs +0 -174
  59. package/src/advisor/convergence.mjs +0 -80
  60. package/src/advisor/history.mjs +0 -77
  61. package/src/advisor/loop.mjs +0 -293
  62. package/src/advisor/messages.mjs +0 -299
  63. package/src/advisor/project-context.mjs +0 -194
  64. package/src/advisor/repos.mjs +0 -150
  65. package/src/advisor/run.mjs +0 -293
  66. package/src/advisor/truncate.mjs +0 -57
  67. package/src/advisor.mjs +0 -290
  68. package/src/agent/completion.mjs +0 -146
  69. package/src/agent/dispatch.mjs +0 -489
  70. package/src/agent/helpers.mjs +0 -384
  71. package/src/agent/post-turn.mjs +0 -70
  72. package/src/agent/record-results.mjs +0 -174
  73. package/src/agent/relay-prefix.mjs +0 -39
  74. package/src/agent/run-stages.mjs +0 -244
  75. package/src/agent/setup-reminders.mjs +0 -69
  76. package/src/agent/setup.mjs +0 -354
  77. package/src/agent/spawn-child.mjs +0 -243
  78. package/src/agent-tools/advisor-async.mjs +0 -346
  79. package/src/agent-tools/advisor-settle.mjs +0 -231
  80. package/src/agent-tools/advisor.mjs +0 -260
  81. package/src/agent-tools/async-settle.mjs +0 -204
  82. package/src/agent-tools/batch-segment.mjs +0 -195
  83. package/src/agent-tools/consult.mjs +0 -473
  84. package/src/agent-tools/design-token.mjs +0 -117
  85. package/src/agent-tools/digest-budget.mjs +0 -76
  86. package/src/agent-tools/eng.mjs +0 -67
  87. package/src/agent-tools/escalate-async.mjs +0 -295
  88. package/src/agent-tools/goal.mjs +0 -119
  89. package/src/agent-tools/plan.mjs +0 -81
  90. package/src/agent-tools/read-history.mjs +0 -309
  91. package/src/agent-tools/recent-changes.mjs +0 -24
  92. package/src/agent-tools/review-streak.mjs +0 -93
  93. package/src/agent-tools/settings.mjs +0 -265
  94. package/src/agent-tools/skill.mjs +0 -47
  95. package/src/agent-tools/subagent-actions.mjs +0 -482
  96. package/src/agent-tools/subagent-async.mjs +0 -434
  97. package/src/agent-tools/subagent-panel.mjs +0 -160
  98. package/src/agent-tools/subagent-run.mjs +0 -205
  99. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  100. package/src/agent-tools/subagent-spawn.mjs +0 -459
  101. package/src/agent-tools/subagent.mjs +0 -404
  102. package/src/agent-tools/task.mjs +0 -87
  103. package/src/agent-tools/timer.mjs +0 -46
  104. package/src/agent-tools/verify.mjs +0 -271
  105. package/src/agent-tools.mjs +0 -17
  106. package/src/agent.mjs +0 -417
  107. package/src/auto-think.mjs +0 -115
  108. package/src/config-migrate.mjs +0 -70
  109. package/src/config.mjs +0 -496
  110. package/src/context.mjs +0 -392
  111. package/src/conventions.mjs +0 -223
  112. package/src/embedding.mjs +0 -120
  113. package/src/escape.mjs +0 -152
  114. package/src/expand-home.mjs +0 -16
  115. package/src/explore-distill.mjs +0 -155
  116. package/src/generate-title.mjs +0 -88
  117. package/src/git/checkpoint.mjs +0 -448
  118. package/src/git/gitmem.mjs +0 -100
  119. package/src/hooks.mjs +0 -97
  120. package/src/ledger.mjs +0 -227
  121. package/src/log.mjs +0 -195
  122. package/src/markdown.mjs +0 -106
  123. package/src/mcp/helpers.mjs +0 -51
  124. package/src/mcp/transport-http.mjs +0 -248
  125. package/src/mcp/transport-stdio.mjs +0 -140
  126. package/src/mcp/transport-ws.mjs +0 -122
  127. package/src/mcp.mjs +0 -295
  128. package/src/memory/code-index.mjs +0 -219
  129. package/src/memory/code-sync.mjs +0 -415
  130. package/src/memory/core.mjs +0 -299
  131. package/src/memory/delete.mjs +0 -236
  132. package/src/memory/docs.mjs +0 -419
  133. package/src/memory/file-walk.mjs +0 -109
  134. package/src/memory/scan.mjs +0 -95
  135. package/src/memory/schema.mjs +0 -452
  136. package/src/memory.mjs +0 -21
  137. package/src/model-ref.mjs +0 -66
  138. package/src/model-specs.mjs +0 -179
  139. package/src/peer-domains.mjs +0 -265
  140. package/src/peer-instances.mjs +0 -231
  141. package/src/prompt-overlays.mjs +0 -82
  142. package/src/prompts/advisor-design.md +0 -41
  143. package/src/prompts/advisor-round1.md +0 -41
  144. package/src/prompts/advisor-round2.md +0 -46
  145. package/src/prompts/advisor-round3.md +0 -42
  146. package/src/prompts/common.md +0 -115
  147. package/src/prompts/consult-base.md +0 -19
  148. package/src/prompts/discipline-engineering.md +0 -258
  149. package/src/prompts/discipline-normal.md +0 -185
  150. package/src/prompts/persona-coder.md +0 -21
  151. package/src/prompts/persona-eng-coder.md +0 -37
  152. package/src/prompts/persona-eng-designer.md +0 -60
  153. package/src/prompts/persona-engineering.md +0 -55
  154. package/src/prompts/persona-explore.md +0 -15
  155. package/src/prompts/persona-normal.md +0 -27
  156. package/src/prompts/persona-plan.md +0 -26
  157. package/src/provider/anthropic.mjs +0 -225
  158. package/src/provider/core.mjs +0 -476
  159. package/src/provider/errors.mjs +0 -101
  160. package/src/provider/google.mjs +0 -257
  161. package/src/provider/index.mjs +0 -7
  162. package/src/provider/list-models.mjs +0 -93
  163. package/src/provider/normalize.mjs +0 -81
  164. package/src/provider/rate.mjs +0 -108
  165. package/src/provider/responses.mjs +0 -495
  166. package/src/provider/retry.mjs +0 -88
  167. package/src/provider/sse.mjs +0 -264
  168. package/src/proxy.mjs +0 -261
  169. package/src/rules.mjs +0 -53
  170. package/src/session-gc.mjs +0 -221
  171. package/src/session-guard.mjs +0 -59
  172. package/src/session-migrate.mjs +0 -48
  173. package/src/session-rename.mjs +0 -38
  174. package/src/session-segments.mjs +0 -100
  175. package/src/session-slots.mjs +0 -492
  176. package/src/session-store.mjs +0 -441
  177. package/src/session.mjs +0 -492
  178. package/src/skills.mjs +0 -153
  179. package/src/text-budget.mjs +0 -46
  180. package/src/token-ttl.mjs +0 -274
  181. package/src/tools/apply_patch.md +0 -15
  182. package/src/tools/bash.md +0 -37
  183. package/src/tools/bash.mjs +0 -268
  184. package/src/tools/checklist-sync.mjs +0 -181
  185. package/src/tools/checklist.md +0 -13
  186. package/src/tools/checklist.mjs +0 -299
  187. package/src/tools/delete.md +0 -13
  188. package/src/tools/edit-batch.mjs +0 -191
  189. package/src/tools/edit-diff.mjs +0 -348
  190. package/src/tools/edit.md +0 -30
  191. package/src/tools/execute.md +0 -21
  192. package/src/tools/execute.mjs +0 -228
  193. package/src/tools/fetch.md +0 -12
  194. package/src/tools/file.mjs +0 -469
  195. package/src/tools/file_ops.md +0 -17
  196. package/src/tools/get_current_time.md +0 -8
  197. package/src/tools/git-checkpoint.mjs +0 -143
  198. package/src/tools/git-ext.mjs +0 -173
  199. package/src/tools/git.md +0 -54
  200. package/src/tools/git.mjs +0 -356
  201. package/src/tools/glob-dialect.mjs +0 -130
  202. package/src/tools/glob.md +0 -11
  203. package/src/tools/grep.md +0 -19
  204. package/src/tools/hashline_edit.md +0 -14
  205. package/src/tools/index.mjs +0 -36
  206. package/src/tools/insert_after.md +0 -15
  207. package/src/tools/lint.md +0 -10
  208. package/src/tools/linter.mjs +0 -128
  209. package/src/tools/ls.md +0 -12
  210. package/src/tools/lsp.md +0 -10
  211. package/src/tools/lsp.mjs +0 -316
  212. package/src/tools/ops.mjs +0 -299
  213. package/src/tools/patch.mjs +0 -282
  214. package/src/tools/process.md +0 -10
  215. package/src/tools/question.md +0 -16
  216. package/src/tools/question.mjs +0 -26
  217. package/src/tools/read.md +0 -20
  218. package/src/tools/read_image.md +0 -8
  219. package/src/tools/repomap.mjs +0 -314
  220. package/src/tools/search.mjs +0 -236
  221. package/src/tools/shared.mjs +0 -446
  222. package/src/tools/tree.md +0 -14
  223. package/src/tools/tree.mjs +0 -66
  224. package/src/tools/wait_for.md +0 -22
  225. package/src/tools/web.mjs +0 -224
  226. package/src/tools/websearch.md +0 -16
  227. package/src/tools/write.md +0 -11
  228. package/src/traces/trace-store.mjs +0 -355
package/src/tui/mouse.mjs CHANGED
@@ -17,9 +17,9 @@
17
17
  import { computeLayout, subagentLineIndex } from "./layout.mjs"
18
18
  import { buildConvLines, convViewport } from "./render-conversation.mjs"
19
19
  import { toggleFoldBlock, scrollFoldBlock, foldScrollOffset } from "./fold-block.mjs"
20
- import { cancelAsyncSubagent, cancelSyncChild } from "../agent-tools/subagent-async.mjs"
21
- import { cancelAsyncAdvisor } from "../agent-tools/advisor-async.mjs"
22
- import { maybeRefillAsync, refreshQueuedTokens } from "../agent-tools/subagent-scheduler.mjs" // F-2:queued 取消后续(补位/位置刷新)——叶子模块
20
+ import { cancelAsyncSubagent, cancelSyncChild } from "@thincoder/core/agent-tools/subagent-async.mjs"
21
+ import { cancelAsyncAdvisor, refreshAdvisorQueuedTokens } from "@thincoder/core/agent-tools/advisor-async.mjs" // ED-4:评审队列排队块刷新
22
+ import { maybeRefillAsync, refreshQueuedTokens } from "@thincoder/core/agent-tools/subagent-scheduler.mjs" // F-2:queued 取消后续(补位/位置刷新)——叶子模块
23
23
  import { routeSubToken } from "./subagent-blocks.mjs" // F-2:queued 取消块移除(⟦ev⟧cancelled 就地路由——引擎动作路径同通道)
24
24
  import { denyModalForOwner } from "./key-modes.mjs"
25
25
  import { C } from "./ansi.mjs"
@@ -199,12 +199,14 @@ export function createMouseDispatch({ agent, state, pushLine, render, popPicker
199
199
  try {
200
200
  const id = key.slice(key.lastIndexOf("#") + 1)
201
201
  const isAdvisorBlock = key.startsWith("advisor#")
202
+ // af 批 #1(发射单源化):`emit` = 本层通道,传进核调用供其单点发射(本层不另发)。
203
+ const emit = (t) => { if (!routeSubToken(state, t, render)) pushLine(t, C.dim) }
202
204
  // §11.2 D-24b (②-6b): ⏹ on an advisor block cancels the background review
203
205
  // (directed abort → cancelled settle: no pending entry / no token).
204
206
  // SYNC-CANCEL F3: async 池/advisor miss 后查 sync registry(⏹ 门控已放开 sync——
205
207
  // cancelSyncChild 与 async cancel 同模块同形态——subagent-async.mjs)。
206
208
  let r = isAdvisorBlock
207
- ? cancelAsyncAdvisor(agent, id)
209
+ ? cancelAsyncAdvisor(agent, id, emit)
208
210
  : cancelAsyncSubagent(agent, id)
209
211
  let syncStopped = false
210
212
  if (!isAdvisorBlock && r?.status === "error") {
@@ -230,11 +232,14 @@ export function createMouseDispatch({ agent, state, pushLine, render, popPicker
230
232
  // 无 settle 事件链)——UI 直连路径无回合 ctx 事件流——补 executeCancelAction
231
233
  // 同款 TUI 维护:等待块移除(⟦ev⟧cancelled 就地路由——守卫同 routeSubToken
232
234
  // cancelled 分支——不冻结)+ 补位 + 剩余排队头位置/依赖标注刷新(⟦ev⟧queued)。
235
+ // af 批去重(#1 单源化):共享行加 `!isAdvisorBlock` 守卫——评审族发射已由核单点经
236
+ // 上方 `emit` 通道完成;子代理族自持发射面零改(该族两路互斥、各发一次)。
233
237
  if (r?.was === "queued") {
234
- const emit = (t) => { if (!routeSubToken(state, t, render)) pushLine(t, C.dim) }
235
- emit(`${key}/⟦ev⟧cancelled\x1e`)
238
+ if (!isAdvisorBlock) emit(`${key}/⟦ev⟧cancelled\x1e`)
236
239
  maybeRefillAsync(agent)
237
240
  refreshQueuedTokens(agent, emit)
241
+ // ED-4(评审池排队语义):advisor 队列余位位置刷新(subagent 队列同构面)。
242
+ if (isAdvisorBlock) refreshAdvisorQueuedTokens(agent, emit)
238
243
  }
239
244
  pushLine(`[subagent ${key} stop requested]`, C.warn)
240
245
  }
@@ -9,6 +9,7 @@ import { createModelPicker } from "./model-picker.mjs"
9
9
  * 通用绑定(showPicker/closePicker/renderPickerLines)再行装配——index.mjs 消费面不变。
10
10
  * 单一 Promise API:showPicker(title, entries, { defaultIndex }) → Promise<entry|null>。
11
11
  * picker 栈:state.pickerStack,state.picker 始终指向栈顶(layout/render/key-handler 都只读 state.picker)。
12
+ * 删除类二次确认件:confirmDelete(TUI-COMMANDS.md §5.4 单源——形态同 cmd-clear / cmd-new 先例)。
12
13
  * 选中即关闭(Enter = resolve + pop);Esc = pop 当前层并 resolve(null)。菜单循环由调用方 while 重开。 */
13
14
  export function createPickers(ctx) {
14
15
  const { agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey } = ctx
@@ -105,13 +106,25 @@ export function createPickers(ctx) {
105
106
 
106
107
  function renderPickerLines() { rebuildLines() }
107
108
 
109
+ /** 删除类入口二次确认件(单源——TUI-COMMANDS.md §5.4):形态 = 既有二次确认 picker
110
+ * (与 cmd-clear.mjs:6-9 · cmd-new.mjs:30-33 先例逐字同形,禁新造模态)。
111
+ * defaultIndex 恒指 Cancel(反误触——直接 Enter 不会删除);Esc / Cancel ⇒ false
112
+ * ⇒ 调用方零动作(取消先于落盘发生)。入口 ctx 缺此绑定 ⇒ 调用方报错(fail-closed)。 */
113
+ async function confirmDelete(question = "Delete?") {
114
+ const e = await showPicker(question, [
115
+ { type: "item", text: "Yes, delete", action: "yes" },
116
+ { type: "item", text: "Cancel", action: "no" },
117
+ ], { defaultIndex: 1 })
118
+ return e?.action === "yes"
119
+ }
120
+
108
121
  // /model 两级面 + selectModel + 渠道管理 + pickModelForSlot —— 迁 model-picker.mjs
109
122
  // (MODEL-MERGE-SESSION——pick 后本文件 ≤450)。createModelPicker 返回同名单函数,
110
123
  // 经本层原样转发(index.mjs/wizard/slash-commands 消费面零变化)。
111
124
  const modelPicker = createModelPicker({
112
125
  agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey,
113
- showPicker, closePicker, renderPickerLines,
126
+ showPicker, closePicker, renderPickerLines, confirmDelete,
114
127
  })
115
128
 
116
- return { showPicker, closePicker, popPicker, renderPickerLines, ...modelPicker }
129
+ return { showPicker, closePicker, popPicker, renderPickerLines, confirmDelete, ...modelPicker }
117
130
  }
@@ -15,7 +15,7 @@ import {
15
15
  isExpanded, foldHintLine, renderExpandedBlock, renderBlockTimeline,
16
16
  renderMathAndMarkdown, foldCapRows, renderFoldedHead, foldTailLines,
17
17
  } from "./fold-block.mjs"
18
- import { ADVISOR_THINKING_PLACEHOLDER } from "../advisor/run.mjs"
18
+ import { ADVISOR_THINKING_PLACEHOLDER } from "@thincoder/core/advisor/run.mjs"
19
19
  import { frozenSubSeg, toolSeg, frozenAdvSeg } from "./render-segments.mjs"
20
20
 
21
21
 
@@ -9,10 +9,11 @@
9
9
  import { ansi, C, ESC } from "./ansi.mjs"
10
10
  import { convCacheKey, renderConversation, countConvLines } from "./render-conversation.mjs"
11
11
  import { sliceByWidth, stringWidth } from "./render.mjs"
12
- import { providerSpec } from "../config.mjs"
12
+ import { providerSpec } from "@thincoder/core/config.mjs"
13
13
  import { computeLayout, subagentVisibleLines } from "./layout.mjs"
14
14
  import { basename } from "node:path"
15
15
  import { readFileSync } from "node:fs"
16
+ import { isYesNoModal } from "./interaction.mjs" // X8 跟进①:y/n-only 族判据单源
16
17
 
17
18
  export { convCacheKey, renderConversation, countConvLines } from "./render-conversation.mjs"
18
19
 
@@ -39,7 +40,7 @@ const SLASH_HINTS = {
39
40
 
40
41
  /** Header panel (always 1 line). */
41
42
  export function renderHeader(agent, cols) {
42
- // 2026-09-02 Q1(SESSION.md §8):provider 可为 null(无效 provider 被清空后用户 Esc 取消重选)
43
+ // 2026-09-02 Q1(SESSION.md §6.8):provider 可为 null(无效 provider 被清空后用户 Esc 取消重选)
43
44
  // —— 可选链守卫,头部显示 "no provider" 占位
44
45
  const model = agent.provider?.model ?? "no provider"
45
46
  // providerSpec(PROVIDER.md §15):模型信息显示跟随 provider 级 context 覆盖(D-C5);
@@ -323,7 +324,9 @@ function inputBoxStyle(state) {
323
324
  borderColor = C.tool; title = " Question "
324
325
  } else if (state.permission) {
325
326
  borderColor = C.warn
326
- title = state.permission.name === "continue" ? " Continue? (y/n) "
327
+ // X8 跟进①:y/n-only 族(continue / retry)按名分流——`a` 不属该族键面
328
+ title = isYesNoModal(state.permission.name)
329
+ ? (state.permission.name === "continue" ? " Continue? (y/n) " : " Retry? (y/n) ")
327
330
  : state.permission.batch ? ` Allow ${state.permission.name}? (a/o/n) `
328
331
  : ` Allow ${state.permission.name}? (y/n/a) `
329
332
  } else if (state.picker) {
@@ -349,7 +352,9 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
349
352
  : " Type answer then Enter │ Esc: cancel"
350
353
  }
351
354
  if (state.permission) {
352
- if (state.permission.name === "continue") return " y: continue │ n: stop"
355
+ if (isYesNoModal(state.permission.name)) {
356
+ return state.permission.name === "continue" ? " y: continue │ n: stop" : " y: retry │ n: stop"
357
+ }
353
358
  if (state.permission.batch) return " a: approve all │ o: one by one │ n: deny"
354
359
  return " y: approve │ n: deny │ a: approve all (AUTO)"
355
360
  }
@@ -383,7 +388,7 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
383
388
  const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
384
389
  const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
385
390
  const statusText = state.processing ? `${state.status}${toolHint}${elapsed}` : state.status
386
- // 2026-09-02 Q1(SESSION.md §8):provider 可为 null —— providerSpec(null) 保守 128K 不抛错
391
+ // 2026-09-02 Q1(SESSION.md §6.8):provider 可为 null —— providerSpec(null) 保守 128K 不抛错
387
392
  // 2026-09-02 §15:context 窗口跟随 providers[].context 覆盖(T-C6——百分比基于覆盖后的窗口)
388
393
  const modelContext = providerSpec(agent.provider).context
389
394
  const ctxPct = Math.round((state.ctxCache.tokens / modelContext) * 100)
@@ -14,7 +14,7 @@
14
14
  * so recomputing the frame each paint is cheap.
15
15
  */
16
16
  import { countConvLines, renderRows } from "./render-frame.mjs"
17
- import { estimateTokens } from "../context.mjs"
17
+ import { estimateTokens } from "@thincoder/core/context.mjs"
18
18
  import { ansi, C } from "./ansi.mjs"
19
19
  import { isTuiActive } from "./tui-lifecycle.mjs" // F-2 渲染抑制守卫(RESIZE-MOUSE-LEAK-FIX)
20
20
 
@@ -86,7 +86,9 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
86
86
  const modelPart = sub.model ? ` · ${sub.model}` : ""
87
87
  const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
88
88
  const errPart = sub.lastError ? ` — ${sub.lastError}` : ""
89
- const icon = sub.approval ? "⏸" : "✓"
89
+ // 图标三态互斥(2026-09-20 端差·显示面消差批 M5):修前 stopped 冻结头 `✓` 与 verb 相抵
90
+ // ⇒ 补齐 VSC `webview/activity-view.js:46-49` 已成的 cancelled → `⏹` 形态(VSC 标尺,CLI 缺面)。
91
+ const icon = sub.approval ? "⏸" : sub.stopped ? "⏹" : "✓"
90
92
  const verb = sub.stopped ? "stopped" : "done" // §19.5: cancel 冻结标题 "stopped"
91
93
  const header = `[${icon} ${sub.key}${modePart}${modelPart} · ${verb} ${elapsed}s${turnPart}${errPart}]`
92
94
  const out = []
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { C } from "./ansi.mjs"
12
- import { specForModel } from "../config.mjs"
12
+ import { specForModel } from "@thincoder/core/config.mjs"
13
13
  import { handleClearCommand } from "./cmd-clear.mjs"
14
14
  import { handleNewCommand } from "./cmd-new.mjs"
15
15
  import { handleExitCommand } from "./cmd-exit.mjs"
@@ -1,10 +1,10 @@
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.md §15.3.3):恢复/翻页行过额度 + state.lines 总量对账。
7
- import { capLine, accountLine, accountAll, syncLineBudget } from "./display-budget.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
8
  import { shiftFreezeAnchors } from "./subagent-blocks.mjs"
9
9
 
10
10
  /** Lazy history window (parity with VS Code HISTORY_PAGE_SIZE): first paint loads
@@ -118,7 +118,7 @@ export function historyToLines(history, startIdx, endIdx) {
118
118
  * INITIAL_HISTORY_MESSAGES, set the _history* counters the loadOlder closure
119
119
  * reads, and prepend the "… N more earlier messages" placeholder. Shared by
120
120
  * startup restore and /session switching (the display snapshot is deprecated).
121
- * TUI-OOM-ROOTCAUSE(SESSION.md §14.3.6):desc = `{ history, total, base? }`——
121
+ * TUI-OOM-ROOTCAUSE(SESSION.md §6.14):desc = `{ history, total, base? }`——
122
122
  * history = 尾窗(≤200,含 ±1 页沿头一条——跨页回标判定用;不渲染)、total = 绝对总条数
123
123
  * (「N messages」标签口径)、base = history[0] 的绝对序号(缺省由 total − len 推导)。
124
124
  */
@@ -132,7 +132,7 @@ export function restoreLines(state, desc) {
132
132
  const start = total - loaded
133
133
  state._lineIdCounter = state._lineIdCounter ?? 0
134
134
  const fresh = historyToLines(window, Math.max(0, start - base), window.length)
135
- for (const l of fresh) l.text = capLine(l.text) // 行额度(§15.3.3 恢复行过 capText)
135
+ for (const l of fresh) l.text = capLine(l.text) // 行额度(§5.3 恢复行过 capText)
136
136
  // Stable per-line ids (P1, 2026-08-30): fold keys for tool blocks derive from
137
137
  // _lineId so loadOlder's head-unshift cannot re-bind an expanded block to a
138
138
  // different tool (positional tool-{i} keys drift under unshift).
@@ -144,7 +144,7 @@ export function restoreLines(state, desc) {
144
144
  if (state._hasOlder) {
145
145
  state.lines.unshift({ text: `… ${start} more earlier messages (PgUp at top to load)`, color: C.dim })
146
146
  }
147
- accountAll(state) // 行集重建 → 总量直算重对账(§15.3.4)
147
+ accountAll(state) // 行集重建 → 总量直算重对账(§5.4)
148
148
  syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
149
149
  }
150
150
  /** 懒加载更早历史(2026-08-31 用户约定:"滚动到头自动加载"——滚轮/PgUp 到顶皆触发;
@@ -156,7 +156,7 @@ export function createLoadOlder({ agent, state, render }) {
156
156
  return () => {
157
157
  if (!state._hasOlder) return
158
158
  // 翻页锚 = 恢复时点 total(state._historyTotal)——活消息增长不再使页码漂移
159
- // (§14.1 事实 4 / §14.3.6 顺带修复)。
159
+ // (§6.14 事实 4 顺带修复)。
160
160
  const total = state._historyTotal ?? 0
161
161
  const loaded = state._historyLoaded
162
162
  const start = Math.max(0, total - loaded - HISTORY_PAGE_MESSAGES)
@@ -167,9 +167,9 @@ export function createLoadOlder({ agent, state, render }) {
167
167
  const cols = d.cols ?? ((state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80))
168
168
  const before = countConvLines(state, cols, d.rows ?? (process.stdout.rows || 24))
169
169
 
170
- if (state.lines[0]?.text?.startsWith("… ")) state.lines.shift()
170
+ if (state.lines[0]?.text?.startsWith("… ")) releaseLine(state, state.lines.shift()) // 移除必出账(§5.5 记账口径——releaseLine 同额负向)
171
171
  state._lineIdCounter = state._lineIdCounter ?? 0
172
- // 页源(§14.3.6):绑定态 → store.page 绝对区间 + ±1 页沿(页前一消息供跨页回合
172
+ // 页源(§6.14):绑定态 → store.page 绝对区间 + ±1 页沿(页前一消息供跨页回合
173
173
  // 标签判定、页后一消息供 tool_result 配对);未绑定(模式 F)→ 内存全量数组回退。
174
174
  const store = agent._recordStore
175
175
  let older
@@ -179,7 +179,7 @@ export function createLoadOlder({ agent, state, render }) {
179
179
  } else {
180
180
  older = historyToLines(agent._fullHistory ?? [], start, end)
181
181
  }
182
- for (const l of older) l.text = capLine(l.text) // 行额度(§15.3.3 翻页行过 capText)
182
+ for (const l of older) l.text = capLine(l.text) // 行额度(§5.3 翻页行过 capText)
183
183
  for (const l of older) l._lineId = ++state._lineIdCounter
184
184
  state.lines.unshift(...older)
185
185
  for (const l of older) accountLine(state, l)
@@ -190,7 +190,7 @@ export function createLoadOlder({ agent, state, render }) {
190
190
  state.lines.unshift(ph)
191
191
  accountLine(state, ph)
192
192
  }
193
- // 页内不裁头(保锚定)——§15.3.3:总量的裁头由 syncLineBudget 兜(此处只对账字符预算)
193
+ // 页内不裁头(保锚定)——§5.3:总量的裁头由 syncLineBudget 兜(此处只对账字符预算)
194
194
  syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
195
195
 
196
196
  const after = countConvLines(state, cols, (state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24))
@@ -206,7 +206,7 @@ export function showStartup(ctx) {
206
206
  const { agent, state, opts, pushLine, pushLabel, render, startWizard } = ctx
207
207
 
208
208
  // Startup screen
209
- // 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 取消重选)
210
210
  // —— 可选链守卫;`!apiKey` 触发既有 wizard(其 provider 菜单列出已存在 providers,可选中恢复,F3)
211
211
  if (!agent.provider?.apiKey) {
212
212
  pushLabel(`Welcome to ThinCoder!`, ansi.bold + C.tool)
@@ -225,7 +225,7 @@ export function showStartup(ctx) {
225
225
  // deprecated; it drifted out of sync with history on VS Code writes).
226
226
  if (opts.restored?.history?.length) {
227
227
  restoreLines(state, opts.restored)
228
- // 「N messages」标签口径 = total(§14.3.6——非窗口长度)
228
+ // 「N messages」标签口径 = total(§6.14——非窗口长度)
229
229
  const total = Number.isFinite(opts.restored.total) ? opts.restored.total : opts.restored.history.length
230
230
  pushLabel(`── Restored previous session (${total} messages); /new for a fresh session ──`, C.warn)
231
231
  }
@@ -242,7 +242,7 @@ export function showStartup(ctx) {
242
242
  * Prefers git diff incremental (fast); falls back to full scan when git is unavailable or on first run. */
243
243
  export async function backgroundIndex(ctx) {
244
244
  const { agent, state, render } = ctx
245
- const { codeSync, docSync, gitSync } = await import("../memory.mjs")
245
+ const { codeSync, docSync, gitSync } = await import("@thincoder/core/memory.mjs")
246
246
  const cwd = agent.cwd
247
247
  let codeFiles = 0, docFiles = 0
248
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,7 +82,18 @@ 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] = {
@@ -173,6 +186,10 @@ export function routeSubToken(state, t, scheduleRender) {
173
186
  const live = state.subTasks?.[path.head]
174
187
  if (live && !live.done && live.async !== true) {
175
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)
176
193
  }
177
194
  scheduleRender()
178
195
  return true
@@ -13,7 +13,8 @@
13
13
  import { C } from "./ansi.mjs"
14
14
  import { closeOpenSubChildren } from "./subagent-children.mjs"
15
15
  // TUI-OOM-ROOTCAUSE(TUI.md §15.3.3 落点表末行):state.lines 总量账——splice 插入路径过账。
16
- import { accountLine } from "./display-budget.mjs"
16
+ // zero-block 批(§6.8.3.2):摘除路径负向出账——releaseLine(增删均须过账)。
17
+ import { accountLine, releaseLine } from "./display-budget.mjs"
17
18
 
18
19
  // ─── §19.6 D-P1 面板视图(subagent panel 检查工具)───
19
20
  // CLI-ACTIVITY-DEBLOAT F-3:面板区块列表由消费面**读时现算**(action:"panel" 经
@@ -95,6 +96,15 @@ export function freezeSubTaskLines(state, sub) {
95
96
  accountLine(state, line)
96
97
  }
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))
106
+ }
107
+
98
108
  /** 头裁锚点校正(index.mjs pushLine 调用):裁 removedCount 补 1 标记行 = 净位移
99
109
  * removedCount−1(code review round1 #3);在途锚点前移,min 0 兜底。 */
100
110
  export function shiftFreezeAnchors(state, removedCount) {
@@ -104,6 +114,62 @@ export function shiftFreezeAnchors(state, removedCount) {
104
114
  }
105
115
  }
106
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
+
107
173
  /** 冻结 + 释放全部已 done 块(工具结果清扫路径)。锚点降序(后 settle 先插——
108
174
  * 绝对位置 splice 语义);无锚点(-1)排末尾推;sort 稳定。 */
109
175
  export function freezeDoneSubTasks(state) {
@@ -138,11 +204,16 @@ export function finishSubTasksByRole(state, roles, lastError = null) {
138
204
  * lastError="interrupted"(Ready 态跳过)。§17.5.5:挂起自然退出时本函数只兜底
139
205
  * **未消化残项**(已消化块由 freezeReclaimDigestedBlocks 逐条先行回收——块回收与
140
206
  * 池空解耦)。锚点降序同 freezeDoneSubTasks(挂起期 settle 锚点交错批次各按其
141
- * 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`。 */
142
212
  export function freezeAllSubTasks(state) {
143
213
  const subs = Object.values(state.subTasks ?? {})
144
214
  .sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
145
215
  for (const sub of subs) {
216
+ if (livePoolHas(state, sub.key)) continue // P0-b 存活跳过(墓碑只断言「此块已终」)
146
217
  if (!sub.done) {
147
218
  sub.done = true
148
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, releaseSettledEntry } 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,8 +302,8 @@ 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)) {
@@ -19,7 +19,7 @@
19
19
  import { C } from "./ansi.mjs"
20
20
  import { formatToolSummary } from "./tool-summaries.mjs"
21
21
  import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
22
- import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "../advisor/run.mjs"
22
+ import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "@thincoder/core/advisor/run.mjs"
23
23
  import {
24
24
  SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
25
25
  routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, freezeDoneSubTasks,
@@ -27,8 +27,10 @@ import {
27
27
  shiftFreezeAnchors,
28
28
  } from "./subagent-blocks.mjs"
29
29
  // 第 27 批 §12.3②:前缀正则换名 + import 源改文法模块(纯换名——语义零改)。
30
- import { RELAY_PREFIX_RE } from "../agent/relay-prefix.mjs"
31
- import { TURN_CAP_MARK, STOPPED_MARK } from "../agent/spawn-child.mjs"
30
+ import { RELAY_PREFIX_RE } from "@thincoder/core/agent/relay-prefix.mjs"
31
+ import { TURN_CAP_MARK, STOPPED_MARK } from "@thincoder/core/agent/spawn-child.mjs"
32
+ // 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(消费面禁各自枚举——PROVIDER.md §6.20)
33
+ import { waitStatusText } from "@thincoder/core/provider/wait-status.mjs"
32
34
  // 2026-09-05 module-split:ticks/maps/sweep/slim/settle/探测/find 族迁 tool-display.mjs——
33
35
  // buildToolCallbacks 内部引用用本地 import;sweepToolBlocks re-export(agent-turn 消费面)
34
36
  import {
@@ -402,11 +404,12 @@ export function buildToolCallbacks(deps) {
402
404
  state.tokens.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
403
405
  state.tokens.reasoningTokens += usage.completion_tokens_details?.reasoning_tokens ?? 0
404
406
  },
405
- // Throttle wait (active gate / 429 backoff): show in status bar so user knows it's not frozen
406
- onWait: ({ phase, seconds }) => {
407
- if (phase === "gate") state.status = `TPM throttle wait ~${seconds}s`
408
- else if (phase === "overloaded") state.status = `Server overloaded, retrying in ${seconds}s`
409
- else state.status = `Rate-limited 429, retry in ${seconds}s`
407
+ // Throttle wait (active gate / 429 backoff): show in status bar so user knows it's not frozen.
408
+ // 相位值域/文案取自核单源(wait-status.mjs)——未知相位与 warn 前置告警不显示(不落兜底误标)。
409
+ onWait: (ev) => {
410
+ const s = waitStatusText(ev)
411
+ if (!s) return
412
+ state.status = s
410
413
  render()
411
414
  },
412
415
  onTaskUpdate: (items) => {