thincoder 0.12.60 → 0.12.62
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 +49 -2
- package/README.md +8 -6
- package/bin/thincoder.mjs +38 -124
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +19 -6
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +24 -7
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +43 -11
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/async-settle.mjs +13 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +28 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +22 -13
- package/src/agent-tools/read-history.mjs +31 -6
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +18 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +10 -7
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +69 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +42 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/context.mjs +12 -1
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +35 -8
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +9 -4
- package/src/heap-watch.mjs +88 -0
- package/src/hooks.mjs +7 -3
- package/src/ledger.mjs +227 -0
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +77 -36
- package/src/memory/core.mjs +8 -9
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +17 -11
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/scan.mjs +95 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +19 -9
- package/src/prompts/advisor-round1.md +8 -2
- package/src/prompts/advisor-round2.md +14 -3
- package/src/prompts/advisor-round3.md +14 -3
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +258 -0
- package/src/prompts/discipline-normal.md +185 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +60 -0
- package/src/prompts/persona-engineering.md +55 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +13 -32
- package/src/provider/errors.mjs +26 -1
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +9 -2
- package/src/session-guard.mjs +12 -0
- package/src/session-segments.mjs +100 -0
- package/src/session-slots.mjs +10 -2
- package/src/session-store.mjs +441 -0
- package/src/session.mjs +137 -99
- package/src/text-budget.mjs +46 -0
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/traces/trace-store.mjs +195 -64
- package/src/tui/agent-turn.mjs +32 -13
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +5 -1
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +8 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/display-budget.mjs +184 -0
- package/src/tui/index.mjs +69 -40
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/ledger-surface.mjs +69 -0
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +38 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +45 -13
- package/src/tui/subagent-blocks.mjs +70 -90
- package/src/tui/subagent-children.mjs +125 -67
- package/src/tui/subagent-freeze.mjs +48 -45
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +31 -83
- package/src/tui/tool-args.mjs +9 -4
- package/src/tui/tool-display.mjs +20 -5
- package/src/tui/tool-events.mjs +76 -30
- package/src/tui/tui-lifecycle.mjs +27 -7
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +54 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* re-import(runAgentTurn ↔ suspensionSession 函数级静态环——模块求值期无顶层调用,
|
|
5
5
|
* 环安全——session-slots ↔ session.mjs 同款先例)。
|
|
6
6
|
*
|
|
7
|
-
* §17(2026-09-02,AGENT-LOOP.md §
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* §17(2026-09-02,AGENT-LOOP.md §9 D-S1..S9):回合尾后台池非空 → 不阻塞等待,
|
|
8
|
+
* 进入挂起态——挂起空闲输入开放(Enter = 新回合填单槽 + 唤醒)、busy(processing 含
|
|
9
|
+
* digest)输入禁用(INPUT-LOCK-ASYNC C'——提交吞——2026-09-09)、settle 事件驱动
|
|
10
|
+
* auto-turn 消化(手动档 organize-only / AUTO 档全语义)、池空 + 无待处理输入 → 补发
|
|
11
|
+
* done 冻结自然退出。状态机行表见 AGENT-LOOP.md §9.2。
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
// 函数级静态环(2026-09-05):drive 的 digestTurn/用户回合经 runAgentTurn 递归进入
|
|
@@ -18,59 +19,15 @@ import { sweepToolBlocks } from "./tool-events.mjs"
|
|
|
18
19
|
import { logEvent } from "../log.mjs"
|
|
19
20
|
import { C } from "./ansi.mjs"
|
|
20
21
|
// ASYNC-RESULT-CONTAINER.md D1/D2:池 accessor(双池 absorb)+ pending 单容器停靠
|
|
21
|
-
import { getAsyncPool, parkAsyncPending } from "../agent-tools/async-settle.mjs"
|
|
22
|
+
import { getAsyncPool, parkAsyncPending, releaseSettledEntry } from "../agent-tools/async-settle.mjs"
|
|
22
23
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
export const MAX_MERGE_CHARS = 2000
|
|
28
|
-
|
|
29
|
-
/** 合并注入形态(编号逐条——模型一次处理全部——设计措辞锚)。 */
|
|
30
|
-
export function formatMergedMessages(items) {
|
|
31
|
-
return `你排队了 ${items.length} 条消息:\n${items.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n——一次处理`
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* R15 攒批计划(纯函数——消费点共享同一事实源):按队列顺序产出动作序列。
|
|
36
|
-
* - { kind:"slash", text, count:1 }——/cmd 逐条即时执行(保序——不进合并缓冲);
|
|
37
|
-
* - { kind:"turn", text, count, merged }——文本批次:连续非 / 条目攒批(单批 ≤8 条;
|
|
38
|
-
* 合并注入(格式化后)≤2000 字符;count ≥2 → merged 编号注入;count === 1 → 原文
|
|
39
|
-
* 直发(含单条 >2000 字符——不进批);超限截批先行——余下条目留待下批(不丢)。
|
|
40
|
-
* 消费者每次取动作 [0] 并按 count 从源队列移除——下轮重新计划余项(边界幂等)。
|
|
41
|
-
*/
|
|
42
|
-
export function planQueuedInput(items) {
|
|
43
|
-
const actions = []
|
|
44
|
-
const n = items.length
|
|
45
|
-
let i = 0
|
|
46
|
-
while (i < n) {
|
|
47
|
-
const cur = String(items[i])
|
|
48
|
-
if (cur.startsWith("/")) { actions.push({ kind: "slash", text: cur, count: 1 }); i++; continue }
|
|
49
|
-
let j = i
|
|
50
|
-
for (;;) {
|
|
51
|
-
if (j - i >= MAX_MERGE_ITEMS || j >= n) break
|
|
52
|
-
const it = String(items[j])
|
|
53
|
-
if (it.startsWith("/")) break
|
|
54
|
-
if (it.length > MAX_MERGE_CHARS) break // 单条超长——不进批(留作直发)
|
|
55
|
-
const cand = items.slice(i, j + 1)
|
|
56
|
-
if (cand.length > 1 && formatMergedMessages(cand).length > MAX_MERGE_CHARS) break // 批总长超限——截批
|
|
57
|
-
j++
|
|
58
|
-
}
|
|
59
|
-
// 超长条目堵头(内环一步未进)→ 该条直发(不进批——也不与前后合并)
|
|
60
|
-
if (j === i) { actions.push({ kind: "turn", text: String(items[i]), count: 1, merged: false }); i++; continue }
|
|
61
|
-
const taken = items.slice(i, j)
|
|
62
|
-
if (taken.length === 1) {
|
|
63
|
-
actions.push({ kind: "turn", text: String(taken[0]), count: 1, merged: false })
|
|
64
|
-
} else {
|
|
65
|
-
actions.push({ kind: "turn", text: formatMergedMessages(taken), count: taken.length, merged: true })
|
|
66
|
-
}
|
|
67
|
-
i = j
|
|
68
|
-
}
|
|
69
|
-
return actions
|
|
70
|
-
}
|
|
24
|
+
// INPUT-LOCK-ASYNC(C'——2026-09-09,本档 INPUT-LOCK-ASYNC.md):R15 排队
|
|
25
|
+
// 用户指令合并(§11.3 D-24c——攒批计划/合并文案/上限常量)整批废弃
|
|
26
|
+
// ——busy 提交吞 + pendingInput 单槽化(至多一条待交接——单消息逐发不攒批)。
|
|
27
|
+
// 废弃记录见 AGENT-LOOP.md §11.3。
|
|
71
28
|
|
|
72
29
|
/** 后台池计数(LOGGING susp/digest 事件字段——pendingN/poolN)。
|
|
73
|
-
* poolN = _asyncSubagents + _asyncAdvisors(§
|
|
30
|
+
* poolN = _asyncSubagents + _asyncAdvisors(§11.2 D-24b advisor 池同面板计数——queued
|
|
74
31
|
* 条目同样在 map 内——2026-09-03 code review #2:不再 +queue.length 双计)。
|
|
75
32
|
* R17(§25 D-R17a/b):pendingN = pending 单容器 `_pendingAsyncResults` 条数(
|
|
76
33
|
* ASYNC-RESULT-CONTAINER.md D2——四族统一停靠——digest 驱动判据同单容器)。 */
|
|
@@ -117,7 +74,7 @@ export function allPendingEntries(agent) {
|
|
|
117
74
|
/** 后台池存活判据(D-S2/F5 口径):running/queued 子代理或后台评审,或已 settle 未注入结果
|
|
118
75
|
* (pending 单容器非空 = D-S3 "未注入"),或 running consult 会话(会诊跨回合——
|
|
119
76
|
* §25 D-R17a 挂起活度钩子——consult 启动回合尾即入挂起态)。回合尾与每次轮末都用它
|
|
120
|
-
* 评估退出。§
|
|
77
|
+
* 评估退出。§11.2 D-24b:_asyncAdvisors(后台评审池)与子代理池同判——评审飞行中挂起
|
|
121
78
|
* 会话必须存活。 */
|
|
122
79
|
export function poolLive(agent) {
|
|
123
80
|
const map = agent._asyncSubagents
|
|
@@ -173,7 +130,7 @@ function waitForSettleOrWake(agent, state) {
|
|
|
173
130
|
}
|
|
174
131
|
|
|
175
132
|
/** 后台模式状态行文本(D-S8;17.5.4 #6 顺手对齐):"后台 N 子代理运行中 · M 完成待消化"
|
|
176
|
-
* ——"运行中" = running + queued(含后台评审——§
|
|
133
|
+
* ——"运行中" = running + queued(含后台评审——§11.2 D-24b 同面板计数)+ running consult
|
|
177
134
|
* children(R17——会诊跨回合);"完成待消化" = pending 任一族移交项 + §17.5 回合尾留池
|
|
178
135
|
* 的 settled 未消费项(挂起会话 sweep 前的可见窗口)。 */
|
|
179
136
|
function backgroundStatusText(agent) {
|
|
@@ -217,7 +174,8 @@ async function digestTurn(ctx) {
|
|
|
217
174
|
/**
|
|
218
175
|
* §17 挂起会话驱动(D-S9 行表;由 runAgentTurn 回合尾进入,池空自然退出):
|
|
219
176
|
* - suspension:池项 settle → 入 pending → 开 auto-turn(合并消化近邻 settle);
|
|
220
|
-
*
|
|
177
|
+
* 挂起空闲用户 Enter → pendingInput 单槽(busy 含 digest 提交吞——INPUT-LOCK-ASYNC)
|
|
178
|
+
* ——用户输入优先于 digest;
|
|
221
179
|
* - auto-turn:消化中 settle 不并发开新轮(单 runAgent 循环),轮末按 pending/池态
|
|
222
180
|
* 续开合并消化轮或回挂起;pendingInput 非空 → 以该消息开新回合(不触发新 digest);
|
|
223
181
|
* - §17.5.5:每次消化/会话内用户回合消费 pending 后 → freezeReclaimDigestedBlocks
|
|
@@ -250,25 +208,14 @@ export async function suspensionSession(ctx) {
|
|
|
250
208
|
try {
|
|
251
209
|
while (!state._suspAborted && !agent._sessionAbort.signal.aborted) {
|
|
252
210
|
sweepSettledToPending(agent)
|
|
253
|
-
// 1. 用户输入优先(D-S5):pendingInput
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
// /cmd
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const srcLen = fromPending ? state.pendingInput.length : state.queue.length
|
|
260
|
-
if (srcLen > 0) {
|
|
261
|
-
const src = fromPending ? state.pendingInput : state.queue.map((q) => q.text)
|
|
262
|
-
const head = planQueuedInput(src)[0]
|
|
263
|
-
if (fromPending) state.pendingInput.splice(0, head.count)
|
|
264
|
-
else state.queue.splice(0, head.count)
|
|
265
|
-
if (head.kind === "slash") {
|
|
266
|
-
await ctx.handleSlash?.(head.text)
|
|
267
|
-
render()
|
|
268
|
-
continue
|
|
269
|
-
}
|
|
211
|
+
// 1. 用户输入优先(D-S5):pendingInput 单槽(INPUT-LOCK-ASYNC C'——busy(processing
|
|
212
|
+
// 含 digest)提交吞——Enter 只可能落在挂起空闲/释放窗口——至多一条待交接——driver
|
|
213
|
+
// 消费清槽即开新回合)。R15 攒批/queue 双源已随排队机制废弃收敛(2026-09-09——
|
|
214
|
+
// 单消息交接——无合并无 /cmd 分流——key-handler 只收非斜杠文本——单消息逐发)。
|
|
215
|
+
if ((state.pendingInput?.length ?? 0) > 0) {
|
|
216
|
+
const head = String(state.pendingInput.shift())
|
|
270
217
|
agent._suspended = false // 用户回合 = 普通回合语义(① 直注入 + settle 即冻结)
|
|
271
|
-
await runAgentTurn(ctx, head
|
|
218
|
+
await runAgentTurn(ctx, head, { skipSession: true })
|
|
272
219
|
agent._suspended = true
|
|
273
220
|
// §17.5.5:该回合消化完 pending(run 首行注入)→ 逐条冻结回收驻留块
|
|
274
221
|
// (不等池空——settle 锚点 splice——digest 总览文本之前;与 digest 回收同规则)
|
|
@@ -319,14 +266,13 @@ export async function suspensionSession(ctx) {
|
|
|
319
266
|
agent._pendingAsyncResults = []
|
|
320
267
|
const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
|
|
321
268
|
cleanupConsultSessions(agent)
|
|
322
|
-
// §17 round2 偏差 #2-CLI(code review round2 #2-CLI
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
pushLine(`[background work stopped — ${queuedN} queued message${queuedN > 1 ? "s" : ""} will run as a normal turn]`, C.warn)
|
|
269
|
+
// §17 round2 偏差 #2-CLI(code review round2 #2-CLI)+ INPUT-LOCK 单槽化:中止时
|
|
270
|
+
// 不静默丢弃挂起期输入——Enter 已清空输入框并入 pendingInput(用户视为已发送)——
|
|
271
|
+
// 单槽语义下残余至多一条——转回 state.queue({text} 单条目,下个普通回合的队列
|
|
272
|
+
// 循环续发——零丢失)+ 提示行明示去向(不静默丢)。
|
|
273
|
+
if ((state.pendingInput?.length ?? 0) > 0) {
|
|
274
|
+
state.queue.push({ text: String(state.pendingInput.shift()) })
|
|
275
|
+
pushLine(`[background work stopped — the message you entered will run as a normal turn]`, C.warn)
|
|
330
276
|
}
|
|
331
277
|
} else {
|
|
332
278
|
// D-S3 ③ 兜底:退出前残余(极端竞态)直注入再退——结果零丢失(AC-S2;
|
|
@@ -339,6 +285,8 @@ export async function suspensionSession(ctx) {
|
|
|
339
285
|
for (const e of residual.splice(0)) {
|
|
340
286
|
if (e.role === "consult") await injectConsultResult(agent, e)
|
|
341
287
|
else await injectAsyncResult(agent, e)
|
|
288
|
+
// TUI-OOM-ROOTCAUSE(§23.3.1 消费点③——挂起残差注入):注入完成 → 释放条目持有
|
|
289
|
+
releaseSettledEntry(e)
|
|
342
290
|
}
|
|
343
291
|
}
|
|
344
292
|
}
|
package/src/tui/tool-args.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* 全量必须落行)。
|
|
13
13
|
*/
|
|
14
14
|
import { sliceByWidth } from "./render.mjs"
|
|
15
|
+
import { capLines, ARGS_JSON_MAX_CHARS } from "./display-budget.mjs"
|
|
15
16
|
|
|
16
17
|
/** 单行参数摘要:按工具挑关键字段(vscode 卡片头对齐),未知工具回退 JSON。 */
|
|
17
18
|
export function describeToolArgs(name, args) {
|
|
@@ -41,9 +42,11 @@ export function describeToolArgs(name, args) {
|
|
|
41
42
|
return a.filter ? `${p} (filter: ${String(a.filter)})` : p
|
|
42
43
|
}
|
|
43
44
|
case "websearch": return String(a.query ?? "")
|
|
44
|
-
case "subagent": case "coder": case "explore": case "plan": case "eng-coder": {
|
|
45
|
+
case "subagent": case "coder": case "explore": case "plan": case "eng-coder": case "eng-designer": {
|
|
45
46
|
const task = String(a.task ?? "").replace(/\s+/g, " ").trim()
|
|
46
|
-
|
|
47
|
+
if (task) return task.slice(0, 60) + (task.length > 60 ? "…" : "")
|
|
48
|
+
// action-only 调用(status/observe/send/cancel/escalate/consume-design…)无 task——action 兜底(2.4 批)
|
|
49
|
+
return a.action ? `(${String(a.action)})` : ""
|
|
47
50
|
}
|
|
48
51
|
case "advisor": return String(a.type ?? "review")
|
|
49
52
|
case "read_image": return String(a.path ?? "")
|
|
@@ -72,8 +75,10 @@ export function describeToolArgs(name, args) {
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
/** 恢复路径用:全量参数 pretty JSON 的 dim 行(非空才输出)。
|
|
75
|
-
*
|
|
78
|
+
* 与工具结果的恢复惯例一致——完整落行;**总量额度**(TUI-OOM-ROOTCAUSE §15.3.1
|
|
79
|
+
* ARGS_JSON_MAX_CHARS——`write`/`apply_patch` 整文件内容进显示层的堵口):超出尾截断
|
|
80
|
+
* + 标记,首行保真。 */
|
|
76
81
|
export function toolArgsLines(args) {
|
|
77
82
|
if (!args || typeof args !== "object" || Object.keys(args).length === 0) return []
|
|
78
|
-
return JSON.stringify(args, null, 2).split("\n")
|
|
83
|
+
return capLines(JSON.stringify(args, null, 2).split("\n"), ARGS_JSON_MAX_CHARS)
|
|
79
84
|
}
|
package/src/tui/tool-display.mjs
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* 不禁内容变更(.set/.delete/.push),调用方行为与同模块时代一致。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
// TUI-OOM-ROOTCAUSE(TUI.md §15.3.1):显示层额度常量/工具行定位(display-budget 叶子)。
|
|
11
|
+
import { capLines, TOOL_RESULT_MAX_CHARS } from "./display-budget.mjs"
|
|
12
|
+
|
|
10
13
|
// Tool execution start timestamps (performance.now ms). Keyed by tool_call id
|
|
11
14
|
// when available (parallel same-name tools each get their own tick — the
|
|
12
15
|
// P0-3 fix, 2026-08-30), falling back to a per-name FIFO queue for callers
|
|
@@ -14,10 +17,11 @@
|
|
|
14
17
|
// a parallel same-name batch finishing out of order swaps durations between
|
|
15
18
|
// siblings — same magnitude, both keep an elapsed (display-level, acceptable).
|
|
16
19
|
|
|
17
|
-
// Named caps (consult P2, 2026-08-30): inline 200/
|
|
20
|
+
// Named caps (consult P2, 2026-08-30): inline 200/3/5 were magic numbers.
|
|
21
|
+
// CLI-ACTIVITY-DEBLOAT F-1 (2026-09-10): the report preview constants (former named caps)
|
|
22
|
+
// deleted with the conversation-stream preview (tool-events onToolResult) — the
|
|
23
|
+
// frozen block is the ONLY carrier of a child's report.
|
|
18
24
|
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
25
|
export const REMINDER_CAP = 3 // max pending reminders shown on turn end
|
|
22
26
|
export const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
|
|
23
27
|
|
|
@@ -72,7 +76,9 @@ export function sweepToolBlocks(state) {
|
|
|
72
76
|
* 1) Multimodal results (read_image) embed FULL base64 images in the JSON —
|
|
73
77
|
* the human needs only the text part (model gets images via the multimodal
|
|
74
78
|
* channel); 2) results beyond maxRows are truncated in the block (full
|
|
75
|
-
* text always lives in history for the model). Returns row array.
|
|
79
|
+
* text always lives in history for the model). Returns row array.
|
|
80
|
+
* TUI-OOM-ROOTCAUSE(§15.3.1 TOOL_RESULT_MAX_CHARS):总量额度与 400 行双维
|
|
81
|
+
* (单行任意大——无 `\n` 巨 chunk/JSON 尾行),尾截断 + 标记。 */
|
|
76
82
|
export function slimToolResultForDisplay(result, maxRows = 400) {
|
|
77
83
|
let displayResult = result
|
|
78
84
|
try {
|
|
@@ -80,9 +86,18 @@ export function slimToolResultForDisplay(result, maxRows = 400) {
|
|
|
80
86
|
if (parsed?.images?.length) displayResult = parsed.text ?? result
|
|
81
87
|
} catch { /* not JSON — show as-is */ }
|
|
82
88
|
const rows = String(displayResult).split("\n").filter((l) => l.trim())
|
|
83
|
-
|
|
89
|
+
const capped = rows.length > maxRows
|
|
84
90
|
? [...rows.slice(0, maxRows), `… (result truncated at ${maxRows} rows — full text in history)`]
|
|
85
91
|
: rows
|
|
92
|
+
return capLines(capped, TOOL_RESULT_MAX_CHARS)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 按块定位其载体行(字符账 re-account 用——块对象唯一)。 */
|
|
96
|
+
export function findToolLine(state, name, toolId) {
|
|
97
|
+
const block = findToolBlock(state, name, toolId)
|
|
98
|
+
if (!block) return null
|
|
99
|
+
for (const l of state.lines ?? []) if (l._toolBlock === block) return l
|
|
100
|
+
return null
|
|
86
101
|
}
|
|
87
102
|
/** Mark the dispatch-level tool carrier done when its result is consumed by a
|
|
88
103
|
* dedicated branch (subagent/escalate/advisor blocks) — without this the turn
|
package/src/tui/tool-events.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* - onToken/onReasoning : 子agent 前缀分流(routeSub*)→ 主流 streaming/reasoning
|
|
6
6
|
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时 + §19 action 记录
|
|
7
|
-
* - onToolResult : 子agent 完成冻结(
|
|
7
|
+
* - onToolResult : 子agent 完成冻结(finishSubTaskKey 精确冻 + freezeDoneSubTasks——
|
|
8
|
+
* F-2 后 finishSubTask 为 no-op 兼容保留)、
|
|
8
9
|
* 工具块结果入块、advisor 评审冻结框
|
|
9
10
|
* - onToolOutput : advisor 有序块缓冲 / 工具块输出流
|
|
10
11
|
* - 其余 : usage 累计、等待提示、task 面板、回合末增量落盘
|
|
@@ -20,25 +21,56 @@ import { formatToolSummary } from "./tool-summaries.mjs"
|
|
|
20
21
|
import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
|
|
21
22
|
import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "../advisor/run.mjs"
|
|
22
23
|
import {
|
|
23
|
-
|
|
24
|
+
SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
|
|
24
25
|
routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, freezeDoneSubTasks,
|
|
25
26
|
ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
|
|
27
|
+
shiftFreezeAnchors,
|
|
26
28
|
} from "./subagent-blocks.mjs"
|
|
27
|
-
|
|
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"
|
|
28
32
|
// 2026-09-05 module-split:ticks/maps/sweep/slim/settle/探测/find 族迁 tool-display.mjs——
|
|
29
33
|
// buildToolCallbacks 内部引用用本地 import;sweepToolBlocks re-export(agent-turn 消费面)
|
|
30
34
|
import {
|
|
31
|
-
TOOL_OUTPUT_LINE_CAP,
|
|
35
|
+
TOOL_OUTPUT_LINE_CAP, REMINDER_CAP, REMINDER_PERSIST_TURNS,
|
|
32
36
|
_toolTicks, _subActions, _subActionQ,
|
|
33
37
|
tickStart, tickTake, settleToolBlock, isAsyncSpawnResult, isSpawnErrorResult,
|
|
34
|
-
findToolBlock, slimToolResultForDisplay, sweepToolBlocks,
|
|
38
|
+
findToolBlock, findToolLine, slimToolResultForDisplay, sweepToolBlocks,
|
|
35
39
|
} from "./tool-display.mjs"
|
|
36
40
|
export { sweepToolBlocks, slimToolResultForDisplay } from "./tool-display.mjs"
|
|
41
|
+
// TUI-OOM-ROOTCAUSE(TUI.md §15.3.1/§15.3.3):显示层额度(行/载体/输出环/评审/流式)。
|
|
42
|
+
import {
|
|
43
|
+
capText, capLine, capLines, appendCapped, capAdvisorText, accountLine, syncLineBudget,
|
|
44
|
+
LINE_TRUNC_MARKER, MIDDLE_TRUNC_MARKER, ADVISOR_CAP_OPTS, STREAM_CAP_OPTS,
|
|
45
|
+
TOOL_OUTPUT_ENTRY_MAX_CHARS, TOOL_OUTPUT_TOTAL_MAX_CHARS, ADVISOR_TEXT_MAX_CHARS,
|
|
46
|
+
} from "./display-budget.mjs"
|
|
47
|
+
|
|
48
|
+
/** 输出环字符维总量(§15.3.1 TOOL_OUTPUT_TOTAL_MAX_CHARS——与 200 条目环同款丢最旧)。 */
|
|
49
|
+
function trimOutputChars(block) {
|
|
50
|
+
let total = 0
|
|
51
|
+
for (const s of block.output) total += s.length
|
|
52
|
+
while (total > TOOL_OUTPUT_TOTAL_MAX_CHARS && block.output.length > 0) {
|
|
53
|
+
total -= block.output.shift().length
|
|
54
|
+
}
|
|
55
|
+
}
|
|
37
56
|
|
|
38
57
|
/** Build the agent callbacks + the shared flushStream for one turn.
|
|
39
58
|
* askPermission, askQuestion, saveSessionImpl } */
|
|
40
59
|
export function buildToolCallbacks(deps) {
|
|
41
60
|
const { agent, state, pushLine, render, scheduleRender, ensureAssistantLabel, askPermission, askBatchPermission, askQuestion, saveSessionImpl } = deps
|
|
61
|
+
// 行集总量对账便捷式(§15.3.3——冻结锚点平移交 subagent 族函数)
|
|
62
|
+
const budgetSync = () => syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
|
|
63
|
+
/** Advisor 有序块累积 + 额度(§15.3.1 ADVISOR_TEXT_MAX_CHARS——头 32K/尾 96K/中段标记)。 */
|
|
64
|
+
const pushAdvisorChunk = (raw, kind) => {
|
|
65
|
+
const blocks = state._advisorBlocks ??= []
|
|
66
|
+
const last = blocks.at(-1)
|
|
67
|
+
if (last && last.kind === kind) last.text = appendCapped(last.text, raw, ADVISOR_CAP_OPTS)
|
|
68
|
+
else blocks.push({ kind, text: appendCapped("", raw, ADVISOR_CAP_OPTS) })
|
|
69
|
+
// 多块总量:丢最旧块直至 ≤ 额度(裁决尾部优先)
|
|
70
|
+
let total = 0
|
|
71
|
+
for (const b of blocks) total += b.text.length
|
|
72
|
+
while (total > ADVISOR_TEXT_MAX_CHARS && blocks.length > 1) total -= blocks.shift().text.length
|
|
73
|
+
}
|
|
42
74
|
// NOTE: advisor buffers (_advisorThink/advisorStreaming) are cleared here too.
|
|
43
75
|
// Timing safety: onToolResult flushes _advisorThink into history and empties
|
|
44
76
|
// the buffers BEFORE onTurnEnd can call flushStream (tool result is
|
|
@@ -66,7 +98,7 @@ export function buildToolCallbacks(deps) {
|
|
|
66
98
|
// block content, never the main stream (D1). Routing details in subagent-blocks.
|
|
67
99
|
if (routeSubToken(state, t, scheduleRender)) return
|
|
68
100
|
ensureAssistantLabel()
|
|
69
|
-
state.streaming
|
|
101
|
+
state.streaming = appendCapped(state.streaming, t, STREAM_CAP_OPTS) // §15.3.1 STREAM_MAX_CHARS
|
|
70
102
|
scheduleRender()
|
|
71
103
|
},
|
|
72
104
|
onReasoning: (t) => {
|
|
@@ -74,7 +106,7 @@ export function buildToolCallbacks(deps) {
|
|
|
74
106
|
// buffer as kind=think (F2: same treatment as the main reasoning stream).
|
|
75
107
|
if (routeSubReasoning(state, t, scheduleRender)) return
|
|
76
108
|
ensureAssistantLabel()
|
|
77
|
-
state.reasoning
|
|
109
|
+
state.reasoning = appendCapped(state.reasoning, t, STREAM_CAP_OPTS) // §15.3.1 STREAM_MAX_CHARS
|
|
78
110
|
scheduleRender()
|
|
79
111
|
},
|
|
80
112
|
onToolCall: (name, args, toolId) => {
|
|
@@ -136,6 +168,8 @@ export function buildToolCallbacks(deps) {
|
|
|
136
168
|
done: false,
|
|
137
169
|
},
|
|
138
170
|
})
|
|
171
|
+
accountLine(state, state.lines[state.lines.length - 1]) // 载体入总量账(§15.3.4)
|
|
172
|
+
budgetSync()
|
|
139
173
|
tickStart(name, toolId)
|
|
140
174
|
},
|
|
141
175
|
// §7.2.3(方案 e):dispatch runOne 把工具 ctx 上的 _subagentKey(sync spawn/
|
|
@@ -173,9 +207,12 @@ export function buildToolCallbacks(deps) {
|
|
|
173
207
|
// 启发式(async eng-coder 先启动时 explore 完成会误冻其块——7.2.3.1/T-F2);
|
|
174
208
|
// ② spawn 门拒错误({status:"error"} JSON——auto-turn digest spawn 拒绝):
|
|
175
209
|
// 不冻结任何块(round1 #1——错误路径不冻结 running 块——T-F5);
|
|
176
|
-
// ③ subKey undefined 非错误(老回调/测试直调——成功路径未知工具)→
|
|
177
|
-
//
|
|
178
|
-
|
|
210
|
+
// ③ subKey undefined 非错误(老回调/测试直调——成功路径未知工具)→ 不冻结
|
|
211
|
+
// 任何块(CLI-ACTIVITY-DEBLOAT F-2 收窄:finishSubTask 恒 no-op——无 key
|
|
212
|
+
// 无从精确归属,宁可 no-op 不误冻;块由回合尾 freezeAllSubTasks 兜底清场)。
|
|
213
|
+
// SYNC-CANCEL(R6):⏹ 折叠报告带 STOPPED_MARK——块冻结标 stopped 而非 done
|
|
214
|
+
// (lastError 注记 + 事件定格——兜底竞态窗口的 dispatch 精确冻路径)
|
|
215
|
+
const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : result.includes(STOPPED_MARK) ? "stopped by user — work may be partial" : null
|
|
179
216
|
const hasSubKey = subKey !== undefined && subKey !== null && subKey !== ""
|
|
180
217
|
if (hasSubKey) {
|
|
181
218
|
finishSubTaskKey(state, String(subKey), lastError)
|
|
@@ -184,11 +221,10 @@ export function buildToolCallbacks(deps) {
|
|
|
184
221
|
finishSubTask(state, SUBAGENT_ROLES, lastError)
|
|
185
222
|
freezeDoneSubTasks(state)
|
|
186
223
|
}
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (lines.length > SUBAGENT_PREVIEW_LINES) pushLine(` ... (${lines.length - SUBAGENT_PREVIEW_LINES} more lines)`, C.dim)
|
|
224
|
+
// CLI-ACTIVITY-DEBLOAT F-1 (2026-09-10): the conversation-stream report
|
|
225
|
+
// preview (max 8 dim lines) is deleted — the frozen block is the ONLY
|
|
226
|
+
// carrier of the child's report (full text also lives in history for the
|
|
227
|
+
// model; escalate#N keeps its no-preview surface unchanged).
|
|
192
228
|
}
|
|
193
229
|
} else if (isEscalate) {
|
|
194
230
|
// 飞刀 post-op report landed under the subagent tool name — freeze the
|
|
@@ -200,9 +236,11 @@ export function buildToolCallbacks(deps) {
|
|
|
200
236
|
// event at flight end). Sync results (async:false) freeze below.
|
|
201
237
|
if (!isAsyncSpawnResult(result)) {
|
|
202
238
|
// §7.2.3(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
|
|
203
|
-
// 失败/老回调无 subKey →
|
|
204
|
-
//
|
|
205
|
-
|
|
239
|
+
// 失败/老回调无 subKey → 不冻结任何块(F-2 收窄——finishSubTask 恒 no-op,
|
|
240
|
+
// 块由回合尾 freezeAllSubTasks 兜底清场)。
|
|
241
|
+
// SYNC-CANCEL(R6):同上——escalate 路径同款扩展(sync escalate 无 registry——
|
|
242
|
+
// 恒不折叠——扩展仅口径一致——零行为变化)
|
|
243
|
+
const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : result.includes(STOPPED_MARK) ? "stopped by user — work may be partial" : null
|
|
206
244
|
if (subKey !== undefined && subKey !== null && subKey !== "") {
|
|
207
245
|
finishSubTaskKey(state, String(subKey), lastError)
|
|
208
246
|
} else {
|
|
@@ -229,11 +267,13 @@ export function buildToolCallbacks(deps) {
|
|
|
229
267
|
// screen of garbage (user report 2026-08-30). The model gets the image
|
|
230
268
|
// via the multimodal channel (agent.mjs), the human needs only the
|
|
231
269
|
// text part: strip image parts from the displayed result.
|
|
232
|
-
block.result = slimToolResultForDisplay(result)
|
|
270
|
+
block.result = slimToolResultForDisplay(result) // §15.3.1 RESULT 额度(行 400 + 字符 64K 双维)
|
|
233
271
|
block.summary = formatToolSummary(name, result)
|
|
234
272
|
block.done = true
|
|
235
273
|
const started = tickTake(name, toolId)
|
|
236
274
|
block.elapsed = started !== null ? Math.round(performance.now() - started) : null
|
|
275
|
+
accountLine(state, findToolLine(state, name, toolId)) // 载体原地变更 → 重新入账
|
|
276
|
+
budgetSync()
|
|
237
277
|
}
|
|
238
278
|
}
|
|
239
279
|
if (name === "advisor") {
|
|
@@ -248,17 +288,22 @@ export function buildToolCallbacks(deps) {
|
|
|
248
288
|
// _advisorBlocks keep rendering the running view until cleared at turn end.
|
|
249
289
|
const blocks = state._advisorBlocks ?? []
|
|
250
290
|
if (blocks.length > 0) {
|
|
251
|
-
|
|
291
|
+
// 冻结文本额度(§15.3.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——非静默截断,
|
|
292
|
+
// 代码评审 #3:capLines(...)[0] 会丢标记且截尾)
|
|
293
|
+
const text = capAdvisorText(blocks
|
|
252
294
|
.map((b) => b.text.replaceAll(ADVISOR_THINKING_PLACEHOLDER, ""))
|
|
253
295
|
.join("")
|
|
254
296
|
.replace(/\n{3,}/g, "\n\n")
|
|
255
|
-
.trim()
|
|
297
|
+
.trim())
|
|
256
298
|
if (text) {
|
|
257
|
-
|
|
299
|
+
const line = {
|
|
258
300
|
text: "advisor review",
|
|
259
301
|
color: C.dim,
|
|
260
302
|
_frozenAdvisor: text,
|
|
261
|
-
}
|
|
303
|
+
}
|
|
304
|
+
state.lines.push(line)
|
|
305
|
+
accountLine(state, line)
|
|
306
|
+
budgetSync()
|
|
262
307
|
}
|
|
263
308
|
}
|
|
264
309
|
}
|
|
@@ -271,7 +316,7 @@ export function buildToolCallbacks(deps) {
|
|
|
271
316
|
// child-stdout/SSE fragments at arbitrary byte boundaries; trimEnd eats
|
|
272
317
|
// real trailing newlines and routeSubToolOutput's verbatim concat would
|
|
273
318
|
// glue lines (2026-09-03 修复轮; main path keeps the trimmed form below).
|
|
274
|
-
const isSubRelay =
|
|
319
|
+
const isSubRelay = RELAY_PREFIX_RE.test(name)
|
|
275
320
|
const rawText = typeof chunk === "string" ? chunk : String(chunk?.text ?? "")
|
|
276
321
|
const part = {
|
|
277
322
|
kind: typeof chunk === "string" ? "text" : (chunk?.kind ?? "text"),
|
|
@@ -291,25 +336,26 @@ export function buildToolCallbacks(deps) {
|
|
|
291
336
|
const isString = typeof chunk === "string"
|
|
292
337
|
const raw = isString ? chunk : String(chunk?.text ?? "")
|
|
293
338
|
const kind = isString ? "text" : (chunk?.kind ?? "text")
|
|
294
|
-
|
|
295
|
-
const last = blocks.at(-1)
|
|
296
|
-
if (last && last.kind === kind) last.text += raw
|
|
297
|
-
else blocks.push({ kind, text: raw })
|
|
339
|
+
pushAdvisorChunk(raw, kind) // §15.3.1 累积额度(多块丢最旧)
|
|
298
340
|
scheduleRender()
|
|
299
341
|
return
|
|
300
342
|
}
|
|
301
343
|
// Append into the CURRENT tool block's output buffer (the block is the
|
|
302
344
|
// display; no _live scroll lines anymore). N2-style cap keeps memory
|
|
303
|
-
// bounded: keep the LAST 200 output lines per call
|
|
345
|
+
// bounded: keep the LAST 200 output lines per call; 字符维双维
|
|
346
|
+
// (§15.3.1:单条目 ≤ TOOL_OUTPUT_ENTRY_MAX_CHARS、总量 ≤ TOOL_OUTPUT_TOTAL_MAX_CHARS 丢最旧)。
|
|
304
347
|
const block = findToolBlock(state, name, toolId)
|
|
305
348
|
if (block) {
|
|
306
349
|
for (const line of part.text.split("\n")) {
|
|
307
350
|
const trimmed = line.trimEnd()
|
|
308
|
-
if (trimmed) block.output.push(trimmed)
|
|
351
|
+
if (trimmed) block.output.push(capText(trimmed, { max: TOOL_OUTPUT_ENTRY_MAX_CHARS, keepHead: 6_000, keepTail: 2_000, marker: MIDDLE_TRUNC_MARKER }))
|
|
309
352
|
}
|
|
310
353
|
if (block.output.length > TOOL_OUTPUT_LINE_CAP) {
|
|
311
354
|
block.output.splice(0, block.output.length - TOOL_OUTPUT_LINE_CAP)
|
|
312
355
|
}
|
|
356
|
+
trimOutputChars(block)
|
|
357
|
+
accountLine(state, findToolLine(state, name, toolId))
|
|
358
|
+
budgetSync()
|
|
313
359
|
}
|
|
314
360
|
scheduleRender()
|
|
315
361
|
},
|
|
@@ -15,9 +15,16 @@ export function writeStartupSequence(write = (s) => process.stdout.write(s)) {
|
|
|
15
15
|
write(ansi.altBuffer + ansi.hideCursor + ansi.mouseOn + ansi.bracketedPasteOn + ansi.keyboardPush + ansi.modifyOtherKeysOn + ansi.wrapOff)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/** TUI
|
|
18
|
+
/** TUI 清理序列 = 异常退出恢复序列(单一来源——§9.2 表 2 候选 1):清屏 + 关闭鼠标/
|
|
19
|
+
* 粘贴/键盘增强 + 退出 alt buffer + 显示光标 + 恢复环绕。
|
|
20
|
+
* TUI-OOM-ROOTCAUSE(CRASH-REPORTS.md §9.3):提取为导出常量 `RECOVERY_SEQUENCE`——
|
|
21
|
+
* 包装父在子异常退出(V8 fatal——不走 JS 钩子)时补发同一序列(单一来源,禁第三份
|
|
22
|
+
* 副本);writeCleanupSequence 引用同常量(行为逐字不变——既有字节锁测试保持,AC-RT3)。 */
|
|
23
|
+
export const RECOVERY_SEQUENCE =
|
|
24
|
+
ansi.clearScreen + ansi.mouseOff + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn
|
|
25
|
+
|
|
19
26
|
export function writeCleanupSequence(write = (s) => process.stdout.write(s)) {
|
|
20
|
-
write(
|
|
27
|
+
write(RECOVERY_SEQUENCE)
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
// R25(ARCHITECTURE.md §R25 F-R25a——复审 #1):TUI 活动态标志——index.mjs 在终端接管
|
|
@@ -32,6 +39,11 @@ export function setTuiActive(active) {
|
|
|
32
39
|
tuiActive = active === true
|
|
33
40
|
}
|
|
34
41
|
|
|
42
|
+
/** 读 TUI 活动态(F-2 渲染抑制守卫——render-loop doRender 前查——cleanup 清 false 后不再重绘已恢复的主屏)。 */
|
|
43
|
+
export function isTuiActive() {
|
|
44
|
+
return tuiActive
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
/** R25 崩溃恢复:仅 TUI 活动态执行 writeCleanupSequence(复用本模块清理序列——符号锚)。
|
|
36
48
|
* 测试缝(T-R25a.2——env 门注入):THINCODER_TEST_CLEANUP_OUT 指向文件时序列写入该文件
|
|
37
49
|
* (观察恢复被调 + stdout 管道零 ANSI)——生产不设该 env → 恒 stdout(零变化)。
|
|
@@ -47,8 +59,10 @@ export function restoreTerminalAfterCrash() {
|
|
|
47
59
|
return true
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
62
|
+
/** 退出清理闭包:保存会话 + 关闭 MCP + 恢复终端。幂等(cleanedUp 守卫)。stdin/write 注入缝(测试锁序——缺省生产零变化)。
|
|
63
|
+
* F-1 唯一权威序(RESIZE-MOUSE-LEAK-FIX):① mouseOff DECRST → ② settle ~20ms(DECRST 往返——raw 仍开回显仍关)→
|
|
64
|
+
* ③ raw off → ④ stdin 排空 → ⑤ 恢复屏幕 → ⑥ 清 TUI 活动态。 */
|
|
65
|
+
export function createExitCleanup({ agent, saveSession, closeAllMcp, stdin = process.stdin, write = (s) => process.stdout.write(s) }) {
|
|
52
66
|
let cleanedUp = false
|
|
53
67
|
return () => {
|
|
54
68
|
if (cleanedUp) return
|
|
@@ -67,8 +81,14 @@ export function createExitCleanup({ agent, saveSession, closeAllMcp }) {
|
|
|
67
81
|
} catch {
|
|
68
82
|
// Can't close? fine, process is exiting anyway
|
|
69
83
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
// ① mouseOff 单独写(DECRST——不整包 writeCleanupSequence):先于 raw off 停鼠标——消除「回显开而鼠标未停」暴露窗口
|
|
85
|
+
write(ansi.mouseOff)
|
|
86
|
+
// ② settle 20ms:Atomics.wait 同步等(不拆注册点——cleanup 亦挂 process.on("exit") 兜底异常退出——exit 事件仅同步合法)
|
|
87
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20)
|
|
88
|
+
// ③ raw off(DECRST 已被终端处理——无新上报可回显)→ ④ 排空:摘 data 监听 + pause——不依赖限时读——在途已读字节丢弃不归 shell
|
|
89
|
+
stdin.setRawMode(false); stdin.removeAllListeners("data"); stdin.pause()
|
|
90
|
+
// ⑤ writeCleanupSequence 余部(mouseOff 已单写——clearScreen/bracketedPasteOff/…/wrapOn 恢复屏幕)
|
|
91
|
+
write(ansi.clearScreen + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn)
|
|
92
|
+
setTuiActive(false) // ⑥ R25:清理完即清活动态(崩溃钩子不再误判——渲染抑制锚)
|
|
73
93
|
}
|
|
74
94
|
}
|