thincoder 0.12.59 → 0.12.60

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 (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. package/src/tools/read_pdf.md +0 -21
@@ -53,9 +53,9 @@ export function describeToolArgs(name, args) {
53
53
  const action = String(a.action ?? "")
54
54
  if (action === "put") return String(a.title ?? "")
55
55
  if (action === "search") return String(a.query ?? "")
56
- if (action === "list") return [a.scope && `scope ${a.scope}`, a.type && `type ${a.type}`, a.keyword && `kw ${a.keyword}`].filter(Boolean).join(" ")
57
- if (action === "delete") return a.id ? `id ${a.id} (${a.scope ?? ""})` : `batch ${a.scope ?? ""} ${a.type ? `type ${a.type} ` : ""}${a.keyword ? `kw ${a.keyword}` : ""}`.trim()
58
- if (action === "clear") return `clear ${a.scope ?? ""}`
56
+ if (action === "list") return [a.layer && `layer ${a.layer}`, a.type && `type ${a.type}`, a.keyword && `kw ${a.keyword}`].filter(Boolean).join(" ")
57
+ if (action === "delete") return a.id ? `id ${a.id}${a.layer ? ` (layer ${a.layer})` : ""}` : `batch ${a.layer ?? ""} ${a.type ? `type ${a.type} ` : ""}${a.keyword ? `kw ${a.keyword}` : ""}`.trim()
58
+ if (action === "clear") return `clear ${a.layer ?? ""}`
59
59
  return action || JSON.stringify(a)
60
60
  }
61
61
  case "lsp": return [a.subcommand, a.uri].filter(Boolean).map(String).join(" ")
@@ -0,0 +1,142 @@
1
+ /**
2
+ * tool-display.mjs — 工具块显示/计时/清扫 helper 族(2026-09-05 module-split:
3
+ * tool-events.mjs 537 > 500 硬限——28-162 区(ticks/subActions maps、sweep/slim/
4
+ * settle/async 探测/find)verbatim 迁入,语义零变;tool-events.mjs import 回
5
+ * (buildToolCallbacks 调用点零改)+ re-export sweepToolBlocks(agent-turn 消费面)。
6
+ * 注意:_subActions/_subActionQ/_toolTicks 为模块级**可变对象**导出——ESM 只读绑定
7
+ * 不禁内容变更(.set/.delete/.push),调用方行为与同模块时代一致。
8
+ */
9
+
10
+ // Tool execution start timestamps (performance.now ms). Keyed by tool_call id
11
+ // when available (parallel same-name tools each get their own tick — the
12
+ // P0-3 fix, 2026-08-30), falling back to a per-name FIFO queue for callers
13
+ // without ids (subagent relay). FIFO shift assumes near-call-order completion;
14
+ // a parallel same-name batch finishing out of order swaps durations between
15
+ // siblings — same magnitude, both keep an elapsed (display-level, acceptable).
16
+
17
+ // Named caps (consult P2, 2026-08-30): inline 200/8/120/3/5 were magic numbers.
18
+ export const TOOL_OUTPUT_LINE_CAP = 200 // per-call streaming output ring buffer
19
+ export const SUBAGENT_PREVIEW_LINES = 8 // report preview rows in the conversation
20
+ export const PREVIEW_LINE_CHARS = 120 // per-line preview slice
21
+ export const REMINDER_CAP = 3 // max pending reminders shown on turn end
22
+ export const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
23
+
24
+ export const _toolTicks = new Map()
25
+ // §19 action registry: tool_call id → subagent action (non-spawn only — spawn is
26
+ // the default when no record exists). onToolCall sees the args, onToolResult only
27
+ // the name; without the record every subagent result would route as a spawn.
28
+ export const _subActions = new Map()
29
+ export const _subActionQ = [] // FIFO for subagent calls without a tool id (same fallback as tick queue)
30
+
31
+ export function tickStart(name, toolId) {
32
+ const key = toolId ?? name
33
+ if (toolId) { _toolTicks.set(key, performance.now()); return }
34
+ const q = _toolTicks.get(key) ?? []
35
+ q.push(performance.now())
36
+ _toolTicks.set(key, q)
37
+ }
38
+
39
+ /** Settle one pending tick: by tool_call id, or per-name FIFO. Returns start or null. */
40
+ export function tickTake(name, toolId) {
41
+ const key = toolId ?? name
42
+ const v = _toolTicks.get(key)
43
+ if (v === undefined) return null
44
+ if (toolId) { _toolTicks.delete(key); return v }
45
+ if (v.length === 0) { _toolTicks.delete(key); return null }
46
+ const started = v.shift()
47
+ if (v.length === 0) _toolTicks.delete(key)
48
+ return started
49
+ }
50
+
51
+ /** P0-2 sweep (2026-08-30 consult): an interrupted turn (Ctrl+C / error) leaves
52
+ * running tool-block carriers without an onToolResult — their header would say
53
+ * "running" forever. runAgentTurn's finally calls this: mark them done with an
54
+ * "(interrupted)" status and clear the tick table so no stale start time leaks
55
+ * into the next turn. Mirrors freezeAllSubTasks for the tool-block family. */
56
+ export function sweepToolBlocks(state) {
57
+ for (const l of state.lines ?? []) {
58
+ const b = l._toolBlock
59
+ if (b && !b.done) {
60
+ b.done = true
61
+ b.summary = "(interrupted)"
62
+ b.interrupted = true
63
+ }
64
+ }
65
+ _toolTicks.clear()
66
+ _subActions.clear()
67
+ _subActionQ.length = 0
68
+ }
69
+
70
+ /** Shared display guard for tool results — LIVE and RESTORE use the same
71
+ * function (P1, 2026-08-30 consult: restore lacked the live path's guards).
72
+ * 1) Multimodal results (read_image) embed FULL base64 images in the JSON —
73
+ * the human needs only the text part (model gets images via the multimodal
74
+ * channel); 2) results beyond maxRows are truncated in the block (full
75
+ * text always lives in history for the model). Returns row array. */
76
+ export function slimToolResultForDisplay(result, maxRows = 400) {
77
+ let displayResult = result
78
+ try {
79
+ const parsed = JSON.parse(result)
80
+ if (parsed?.images?.length) displayResult = parsed.text ?? result
81
+ } catch { /* not JSON — show as-is */ }
82
+ const rows = String(displayResult).split("\n").filter((l) => l.trim())
83
+ return rows.length > maxRows
84
+ ? [...rows.slice(0, maxRows), `… (result truncated at ${maxRows} rows — full text in history)`]
85
+ : rows
86
+ }
87
+ /** Mark the dispatch-level tool carrier done when its result is consumed by a
88
+ * dedicated branch (subagent/escalate/advisor blocks) — without this the turn
89
+ * sweep mislabels successful calls as "(interrupted)" (consult P1, 2026-08-30). */
90
+ export function settleToolBlock(state, name, toolId, summary) {
91
+ const block = findToolBlock(state, name, toolId)
92
+ if (block) {
93
+ block.done = true
94
+ block.summary = summary
95
+ const started = tickTake(name, toolId)
96
+ block.elapsed = started !== null ? Math.round(performance.now() - started) : null
97
+ }
98
+ }
99
+
100
+ /** Async spawn detection (§15 D-A1): the subagent tool's async:true result is a
101
+ * status JSON ({id, role, status: running|queued}), NOT a report — the child
102
+ * keeps running, so its activity block must not be frozen at spawn time (it
103
+ * freezes via the ⟦ev⟧done event at settle — §15 D-A3). */
104
+ export function isAsyncSpawnResult(result) {
105
+ try {
106
+ const o = JSON.parse(result)
107
+ return Boolean(o && typeof o === "object" && typeof o.id !== "undefined" && (o.status === "running" || o.status === "queued"))
108
+ } catch {
109
+ return false
110
+ }
111
+ }
112
+
113
+ /** §7.2.3 spawn 门拒错误探测(T-F5):subagent spawn 的机械拒绝以 {status:"error"}
114
+ * JSON 返回(例:manual auto-turn digest spawn 门——digest 语义 organize-only)——
115
+ * 错误路径不得触发完成冻结(round1 #1——错误路径不冻结任何 running 块)。该形态只
116
+ * 出现在无 subKey 的拒绝路径(成功路径恒带 dispatch 传的 ctx._subagentKey)。 */
117
+ export function isSpawnErrorResult(result) {
118
+ try {
119
+ const o = JSON.parse(result)
120
+ return Boolean(o && typeof o === "object" && o.status === "error")
121
+ } catch {
122
+ return false
123
+ }
124
+ }
125
+
126
+ /** Find the live tool-block carrier for a tool event: exact id match when the
127
+ * callback carries one (P0-3 — parallel same-name tools route to their own
128
+ * block); falls back to the last unfinished block of that name. */
129
+ export function findToolBlock(state, name, toolId) {
130
+ const lines = state.lines ?? []
131
+ if (toolId !== undefined && toolId !== null) {
132
+ for (let i = lines.length - 1; i >= 0; i--) {
133
+ if (lines[i]._toolBlock?.id === toolId) return lines[i]._toolBlock
134
+ }
135
+ return null
136
+ }
137
+ for (let i = lines.length - 1; i >= 0; i--) {
138
+ const b = lines[i]._toolBlock
139
+ if (b && b.name === name && !b.done) return b
140
+ }
141
+ return null
142
+ }
@@ -10,9 +10,10 @@
10
10
  * - 其余 : usage 累计、等待提示、task 面板、回合末增量落盘
11
11
  *
12
12
  * flushStream 同时返回给调用方(回合循环 / onTurnEnd 共用)。纯回调装配,无终端副作用
13
- * (除经 deps 注入的 pushLine/render)。§19: subagent_check/escalate 工具退役后,
14
- * subagent 家族全部调用以工具名 "subagent" + action 到达——完成路由按 onToolCall 时
15
- * 记录的 action 分流(spawn 区块 / escalate 区块 / check·status 普通工具块)。
13
+ * (除经 deps 注入的 pushLine/render)。§19: subagent_check/escalate 工具退役后
14
+ * (§19.8——check 动作已删),subagent 家族全部调用以工具名 "subagent" +
15
+ * action 到达——完成路由按 onToolCall 时记录的 action 分流(spawn 区块 / escalate
16
+ * 区块 / status/observe/send 普通工具块——observe/send 不建子代理区块)。
16
17
  */
17
18
  import { C } from "./ansi.mjs"
18
19
  import { formatToolSummary } from "./tool-summaries.mjs"
@@ -20,147 +21,19 @@ import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
20
21
  import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "../advisor/run.mjs"
21
22
  import {
22
23
  SUB_PREFIX_RE, SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
23
- routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, finishSubTaskByModel, freezeDoneSubTasks,
24
+ routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, freezeDoneSubTasks,
24
25
  ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
25
26
  } from "./subagent-blocks.mjs"
26
27
  import { TURN_CAP_MARK } from "../agent/spawn-child.mjs"
27
-
28
- /** Tool execution start timestamps (performance.now ms). Keyed by tool_call id
29
- * when available (parallel same-name tools each get their own tick — the
30
- * P0-3 fix, 2026-08-30), falling back to a per-name FIFO queue for callers
31
- * without ids (subagent relay). FIFO shift assumes near-call-order completion;
32
- * a parallel same-name batch finishing out of order swaps durations between
33
- * siblings — same magnitude, both keep an elapsed (display-level, acceptable). */
34
-
35
- // Named caps (consult P2, 2026-08-30): inline 200/8/120/3/5 were magic numbers.
36
- const TOOL_OUTPUT_LINE_CAP = 200 // per-call streaming output ring buffer
37
- const SUBAGENT_PREVIEW_LINES = 8 // report preview rows in the conversation
38
- const PREVIEW_LINE_CHARS = 120 // per-line preview slice
39
- const REMINDER_CAP = 3 // max pending reminders shown on turn end
40
- const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
41
-
42
- const _toolTicks = new Map()
43
- // §19 action registry: tool_call id → subagent action (non-spawn only — spawn is
44
- // the default when no record exists). onToolCall sees the args, onToolResult only
45
- // the name; without the record every subagent result would route as a spawn.
46
- const _subActions = new Map()
47
- const _subActionQ = [] // FIFO for subagent calls without a tool id (same fallback as tick queue)
48
-
49
- function tickStart(name, toolId) {
50
- const key = toolId ?? name
51
- if (toolId) { _toolTicks.set(key, performance.now()); return }
52
- const q = _toolTicks.get(key) ?? []
53
- q.push(performance.now())
54
- _toolTicks.set(key, q)
55
- }
56
-
57
- /** Settle one pending tick: by tool_call id, or per-name FIFO. Returns start or null. */
58
- function tickTake(name, toolId) {
59
- const key = toolId ?? name
60
- const v = _toolTicks.get(key)
61
- if (v === undefined) return null
62
- if (toolId) { _toolTicks.delete(key); return v }
63
- if (v.length === 0) { _toolTicks.delete(key); return null }
64
- const started = v.shift()
65
- if (v.length === 0) _toolTicks.delete(key)
66
- return started
67
- }
68
-
69
- /** P0-2 sweep (2026-08-30 consult): an interrupted turn (Ctrl+C / error) leaves
70
- * running tool-block carriers without an onToolResult — their header would say
71
- * "running" forever. runAgentTurn's finally calls this: mark them done with an
72
- * "(interrupted)" status and clear the tick table so no stale start time leaks
73
- * into the next turn. Mirrors freezeAllSubTasks for the tool-block family. */
74
- export function sweepToolBlocks(state) {
75
- for (const l of state.lines ?? []) {
76
- const b = l._toolBlock
77
- if (b && !b.done) {
78
- b.done = true
79
- b.summary = "(interrupted)"
80
- b.interrupted = true
81
- }
82
- }
83
- _toolTicks.clear()
84
- _subActions.clear()
85
- _subActionQ.length = 0
86
- }
87
-
88
- /** Shared display guard for tool results — LIVE and RESTORE use the same
89
- * function (P1, 2026-08-30 consult: restore lacked the live path's guards).
90
- * 1) Multimodal results (read_image) embed FULL base64 images in the JSON —
91
- * the human needs only the text part (model gets images via the multimodal
92
- * channel); 2) results beyond maxRows are truncated in the block (full
93
- * text always lives in history for the model). Returns row array. */
94
- export function slimToolResultForDisplay(result, maxRows = 400) {
95
- let displayResult = result
96
- try {
97
- const parsed = JSON.parse(result)
98
- if (parsed?.images?.length) displayResult = parsed.text ?? result
99
- } catch { /* not JSON — show as-is */ }
100
- const rows = String(displayResult).split("\n").filter((l) => l.trim())
101
- return rows.length > maxRows
102
- ? [...rows.slice(0, maxRows), `… (result truncated at ${maxRows} rows — full text in history)`]
103
- : rows
104
- }
105
- /** Mark the dispatch-level tool carrier done when its result is consumed by a
106
- * dedicated branch (subagent/escalate/advisor blocks) — without this the turn
107
- * sweep mislabels successful calls as "(interrupted)" (consult P1, 2026-08-30). */
108
- function settleToolBlock(state, name, toolId, summary) {
109
- const block = findToolBlock(state, name, toolId)
110
- if (block) {
111
- block.done = true
112
- block.summary = summary
113
- const started = tickTake(name, toolId)
114
- block.elapsed = started !== null ? Math.round(performance.now() - started) : null
115
- }
116
- }
117
-
118
- /** Async spawn detection (§15 D-A1): the subagent tool's async:true result is a
119
- * status JSON ({id, role, status: running|queued}), NOT a report — the child
120
- * keeps running, so its activity block must not be frozen at spawn time (it
121
- * freezes via the ⟦ev⟧done event at settle — §15 D-A3). */
122
- function isAsyncSpawnResult(result) {
123
- try {
124
- const o = JSON.parse(result)
125
- return Boolean(o && typeof o === "object" && typeof o.id !== "undefined" && (o.status === "running" || o.status === "queued"))
126
- } catch {
127
- return false
128
- }
129
- }
130
-
131
- /** §7.2.3 spawn 门拒错误探测(T-F5):subagent spawn 的机械拒绝以 {status:"error"}
132
- * JSON 返回(例:manual auto-turn digest spawn 门——digest 语义 organize-only)——
133
- * 错误路径不得触发完成冻结(round1 #1——错误路径不冻结任何 running 块)。该形态只
134
- * 出现在无 subKey 的拒绝路径(成功路径恒带 dispatch 传的 ctx._subagentKey)。 */
135
- function isSpawnErrorResult(result) {
136
- try {
137
- const o = JSON.parse(result)
138
- return Boolean(o && typeof o === "object" && o.status === "error")
139
- } catch {
140
- return false
141
- }
142
- }
143
-
144
-
145
-
146
- /** Find the live tool-block carrier for a tool event: exact id match when the
147
- * callback carries one (P0-3 — parallel same-name tools route to their own
148
- * block); falls back to the last unfinished block of that name. */
149
- function findToolBlock(state, name, toolId) {
150
- const lines = state.lines ?? []
151
- if (toolId !== undefined && toolId !== null) {
152
- for (let i = lines.length - 1; i >= 0; i--) {
153
- if (lines[i]._toolBlock?.id === toolId) return lines[i]._toolBlock
154
- }
155
- return null
156
- }
157
- for (let i = lines.length - 1; i >= 0; i--) {
158
- const b = lines[i]._toolBlock
159
- if (b && b.name === name && !b.done) return b
160
- }
161
- return null
162
- }
163
-
28
+ // 2026-09-05 module-split:ticks/maps/sweep/slim/settle/探测/find 族迁 tool-display.mjs——
29
+ // buildToolCallbacks 内部引用用本地 import;sweepToolBlocks re-export(agent-turn 消费面)
30
+ import {
31
+ TOOL_OUTPUT_LINE_CAP, SUBAGENT_PREVIEW_LINES, PREVIEW_LINE_CHARS, REMINDER_CAP, REMINDER_PERSIST_TURNS,
32
+ _toolTicks, _subActions, _subActionQ,
33
+ tickStart, tickTake, settleToolBlock, isAsyncSpawnResult, isSpawnErrorResult,
34
+ findToolBlock, slimToolResultForDisplay, sweepToolBlocks,
35
+ } from "./tool-display.mjs"
36
+ export { sweepToolBlocks, slimToolResultForDisplay } from "./tool-display.mjs"
164
37
 
165
38
  /** Build the agent callbacks + the shared flushStream for one turn.
166
39
  * askPermission, askQuestion, saveSessionImpl } */
@@ -278,7 +151,7 @@ export function buildToolCallbacks(deps) {
278
151
  : _subActionQ.shift() ?? null
279
152
  if (toolId !== undefined && toolId !== null) _subActions.delete(toolId)
280
153
  }
281
- const isSubagent = name === "subagent" && subAction !== "check" && subAction !== "status" && subAction !== "escalate" && subAction !== "cancel" && subAction !== "panel"
154
+ const isSubagent = name === "subagent" && subAction !== "status" && subAction !== "escalate" && subAction !== "cancel" && subAction !== "panel" && subAction !== "observe" && subAction !== "send"
282
155
  const isEscalate = name === "subagent" && subAction === "escalate"
283
156
  // Subagent complete: mark the earliest running child as done — the block
284
157
  // persists (✓ frozen elapsed header, expandable) as the ONLY carrier of the
@@ -321,38 +194,29 @@ export function buildToolCallbacks(deps) {
321
194
  // 飞刀 post-op report landed under the subagent tool name — freeze the
322
195
  // escalate#N activity block (no preview; legacy surface).
323
196
  settleToolBlock(state, name, toolId, "completed")
324
- // §7.2.3(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
325
- // 失败/老回调无 subKey escalate 角色启发式兜底(escalate 串行 + 角色限定——
326
- // 既有行为)。
327
- const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : null
328
- if (subKey !== undefined && subKey !== null && subKey !== "") {
329
- finishSubTaskKey(state, String(subKey), lastError)
330
- } else {
331
- finishSubTask(state, ["escalate"], lastError)
332
- }
333
- freezeDoneSubTasks(state)
334
- } else if (name === "consult_check" || name === "consult_stop") {
335
- // Consult session-level settle (2026-08-30 consult review): a session spawns
336
- // N parallel children, so completion must settle ALL of them (single-shot
337
- // finishSubTask froze only the earliest — N-1 stayed "running" then got
338
- // mislabeled "interrupted").
339
- // - individual reply (done:false): settle precisely by r.model.
340
- // - done:true / stopped: settle every remaining consult block.
341
- try {
342
- const r = JSON.parse(result)
343
- if (r?.done || r?.stopped !== undefined) {
344
- finishSubTasksByRole(state, ["consult"], null)
345
- freezeDoneSubTasks(state)
346
- } else if (r?.model) {
347
- finishSubTaskByModel(state, "consult", r.model)
348
- freezeDoneSubTasks(state)
349
- }
350
- } catch {
351
- if (name === "consult_stop") {
352
- finishSubTasksByRole(state, ["consult"], null)
353
- freezeDoneSubTasks(state)
197
+ // §25 D-R17b (R17): async escalate ack ({id, role:"escalate", status:
198
+ // running|queued}) the child KEEPS running skip the freeze like the
199
+ // async spawn path (the block freezes on the ⟦ev⟧done/stopped settle
200
+ // event at flight end). Sync results (async:false) freeze below.
201
+ if (!isAsyncSpawnResult(result)) {
202
+ // §7.2.3(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
203
+ // 失败/老回调无 subKey → escalate 角色启发式兜底(escalate 串行 + 角色限定——
204
+ // 既有行为)。
205
+ const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : null
206
+ if (subKey !== undefined && subKey !== null && subKey !== "") {
207
+ finishSubTaskKey(state, String(subKey), lastError)
208
+ } else {
209
+ finishSubTask(state, ["escalate"], lastError)
354
210
  }
355
- } /* non-JSON result — leave blocks as-is */
211
+ freezeDoneSubTasks(state)
212
+ }
213
+ } else if (name === "consult_stop") {
214
+ // R17(§25 D-R17a——check 已删):consult_stop = 取消指定会诊会话。被 abort 的
215
+ // children 各自 settle(settleChild)时发 ⟦ev⟧done 冻结自己的卡——此处**不做按
216
+ // 角色整组清扫**(会诊会话可并发——按角色会误冻其他仍在运行的会话的卡,冻结即
217
+ // 截断其活动流——tool-events advisor 复评 🟡1 修正);取消的即时可见性由 abort
218
+ // settle 事件承担(abort 解绕通常在同回合内完成——卡片在其 child settle 即冻结)。
219
+ // 仅结果本身落本工具调用自己的载体块(下方通用分支)。
356
220
  }
357
221
  if (!isSubagent && !isEscalate && name !== "advisor") {
358
222
  // Result lands INSIDE the block (restore parity — the restored carrier
@@ -3,6 +3,7 @@
3
3
  * 超 500 行硬限拆分——启动序列、退出清理序列与退出闭包从 index.mjs 移入本模块)。
4
4
  */
5
5
 
6
+ import { appendFileSync } from "node:fs"
6
7
  import { ansi } from "./ansi.mjs"
7
8
 
8
9
  /** TUI 启动序列:alt buffer + 隐藏光标 + 鼠标/粘贴/键盘增强 + 禁环绕。
@@ -19,6 +20,33 @@ export function writeCleanupSequence(write = (s) => process.stdout.write(s)) {
19
20
  write(ansi.clearScreen + ansi.mouseOff + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn)
20
21
  }
21
22
 
23
+ // R25(ARCHITECTURE.md §R25 F-R25a——复审 #1):TUI 活动态标志——index.mjs 在终端接管
24
+ // 处(writeStartupSequence 之后)经 setTuiActive(true) 置位,createExitCleanup 清理后清
25
+ // false;bin/thincoder.mjs 崩溃钩子经 restoreTerminalAfterCrash 读同一标志——chat 模式
26
+ // stdout 管道不得收 ANSI(误判即污染输出)。默认 false = 未启动(生产零变化——测试缝同源:
27
+ // T-R25a.2 注入 mock 走同一 setter)。
28
+ let tuiActive = false
29
+
30
+ /** 置/清 TUI 活动态(TUI 启动接管处与退出清理处调用;测试 env 门注入同走此 setter)。 */
31
+ export function setTuiActive(active) {
32
+ tuiActive = active === true
33
+ }
34
+
35
+ /** R25 崩溃恢复:仅 TUI 活动态执行 writeCleanupSequence(复用本模块清理序列——符号锚)。
36
+ * 测试缝(T-R25a.2——env 门注入):THINCODER_TEST_CLEANUP_OUT 指向文件时序列写入该文件
37
+ * (观察恢复被调 + stdout 管道零 ANSI)——生产不设该 env → 恒 stdout(零变化)。
38
+ * @returns {boolean} 是否执行了恢复(false = TUI 未启动——调用方无需处理) */
39
+ export function restoreTerminalAfterCrash() {
40
+ if (!tuiActive) return false
41
+ const seamOut = process.env.THINCODER_TEST_CLEANUP_OUT
42
+ if (seamOut) {
43
+ writeCleanupSequence((s) => appendFileSync(seamOut, s, "utf8"))
44
+ } else {
45
+ writeCleanupSequence()
46
+ }
47
+ return true
48
+ }
49
+
22
50
  /** 退出清理闭包:保存会话(同步)+ 关闭 MCP 子进程 + 恢复终端。幂等(cleanedUp 守卫)。 */
23
51
  export function createExitCleanup({ agent, saveSession, closeAllMcp }) {
24
52
  let cleanedUp = false
@@ -41,5 +69,6 @@ export function createExitCleanup({ agent, saveSession, closeAllMcp }) {
41
69
  }
42
70
  process.stdin.setRawMode(false)
43
71
  writeCleanupSequence()
72
+ setTuiActive(false) // R25:清理完成即清活动态(崩溃钩子不再收到误判)
44
73
  }
45
74
  }
@@ -37,9 +37,13 @@ export function createUpdateNotice(ctx) {
37
37
  const { exec } = await import("node:child_process")
38
38
  const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
39
39
  let stdout = ""
40
+ let done = false
40
41
  cp.stdout?.on("data", (d) => { stdout += d })
41
42
  cp.stderr?.on("data", (d) => { stdout += d })
43
+ cp.on("error", (e) => { if (done) return; done = true; pushLine(`✗ Upgrade failed (${e.message}). Run \`thincoder upgrade\` manually.`, C.error); render() })
42
44
  cp.on("close", (code) => {
45
+ if (done) return
46
+ done = true
43
47
  if (code === 0) {
44
48
  pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
45
49
  } else {
@@ -161,6 +161,16 @@ export function createWizard(ctx) {
161
161
  for (const k of ["thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
162
162
  if (f[k]) providerRec[k] = f[k]
163
163
  }
164
+ // D-F5a(wizard finishWizard——清单外同型写回补正)先盘后存:磁盘 fresh raw 单操作
165
+ // (upsert 目标项 + active 指针)——冲突放弃不留下内存 ghost(F5 约定)
166
+ await persistRaw((raw) => {
167
+ raw.providers ??= []
168
+ const existing = raw.providers.find((p) => p?.name === f.name)
169
+ if (existing) Object.assign(existing, providerRec)
170
+ else raw.providers.push(providerRec)
171
+ raw.activeProvider = f.name
172
+ raw.activeModel = undefined // reset to default model
173
+ })
164
174
  const existing = agent.providers.find((p) => p.name === f.name)
165
175
  if (existing) Object.assign(existing, providerRec)
166
176
  else agent.providers.push(providerRec)
@@ -171,20 +181,16 @@ export function createWizard(ctx) {
171
181
  const { resolveCompactThreshold } = await import("../config.mjs")
172
182
  agent.config.agent.compactThreshold = resolveCompactThreshold(null, f.model).value
173
183
  }
174
- await persistRaw((raw) => {
175
- raw.providers = agent.providers
176
- raw.activeProvider = f.name
177
- raw.activeModel = undefined // reset to default model
178
- })
179
184
  agent.config.activeProvider = f.name
180
185
  agent.config.activeModel = null
181
186
  pushLabel(`❯ Setup`, ansi.bold + C.tool)
182
187
  pushLine(`Setup complete: ${f.name} / ${f.model} (saved to config)`, C.tool)
183
188
  // embedding key: if provided, enable vector search; if not, show how to enable later
184
189
  if (f.embedkey) {
190
+ // D-F5b 语义先盘后存(embedding 单键补丁——冲突放弃不留 ghost)
191
+ await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: f.embedkey } })
185
192
  agent.config.embedding ??= {}
186
193
  agent.config.embedding.apiKey = f.embedkey
187
- await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: f.embedkey } })
188
194
  if (agent.memory && !agent.memory.embedder) {
189
195
  const { createEmbedder } = await import("../embedding.mjs")
190
196
  agent.memory.embedder = createEmbedder(agent.config.embedding)