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.
- package/CHANGELOG.md +38 -3
- package/README.md +2 -2
- package/bin/thincoder.mjs +80 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +7 -4
- package/src/advisor/messages.mjs +24 -4
- package/src/advisor/run.mjs +35 -33
- package/src/advisor.mjs +25 -6
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +102 -19
- package/src/agent/helpers.mjs +36 -0
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +18 -2
- package/src/agent/spawn-child.mjs +29 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +110 -108
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +121 -102
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/read-history.mjs +155 -31
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +7 -17
- package/src/agent-tools/subagent-actions.mjs +168 -130
- package/src/agent-tools/subagent-async.mjs +129 -174
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +45 -21
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +107 -555
- package/src/agent-tools/verify.mjs +118 -270
- package/src/agent.mjs +57 -190
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +3 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +56 -8
- package/src/context.mjs +5 -147
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +11 -11
- package/src/explore-distill.mjs +155 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +58 -48
- package/src/memory.mjs +3 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +9 -76
- package/src/prompts/advisor-round1.md +9 -68
- package/src/prompts/advisor-round2.md +7 -54
- package/src/prompts/advisor-round3.md +7 -54
- package/src/prompts/coder.md +7 -50
- package/src/prompts/consult-base.md +4 -24
- package/src/prompts/discipline.md +26 -44
- package/src/prompts/eng-coder.md +7 -32
- package/src/prompts/engineering-sub.md +3 -23
- package/src/prompts/engineering.md +53 -306
- package/src/prompts/explore.md +3 -12
- package/src/prompts/main.md +10 -32
- package/src/prompts/methodology-template.md +28 -48
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +16 -35
- package/src/provider/core.mjs +6 -67
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +11 -6
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +5 -6
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +3 -0
- package/src/tools/read.md +0 -1
- package/src/tools/shared.mjs +14 -13
- package/src/tools/system.mjs +44 -9
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +17 -228
- package/src/tui/cmd-config.mjs +48 -7
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +3 -2
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/mouse.mjs +7 -1
- package/src/tui/pickers.mjs +40 -22
- package/src/tui/render-segments.mjs +27 -10
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +95 -263
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +61 -23
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +3 -3
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +37 -173
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +12 -6
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/tui/tool-args.mjs
CHANGED
|
@@ -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.
|
|
57
|
-
if (action === "delete") return a.id ? `id ${a.id} (${a.
|
|
58
|
-
if (action === "clear") return `clear ${a.
|
|
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
|
+
}
|
package/src/tui/tool-events.mjs
CHANGED
|
@@ -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" +
|
|
15
|
-
*
|
|
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,
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 !== "
|
|
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
|
-
// §
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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 {
|
package/src/tui/wizard.mjs
CHANGED
|
@@ -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)
|