thincoder 0.12.61 → 0.12.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -371
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +17 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -479
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -453
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -413
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -381
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -217
- package/src/prompts/discipline-normal.md +0 -179
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -55
- package/src/prompts/persona-engineering.md +0 -54
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -224
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
|
|
@@ -73,7 +76,9 @@ export function sweepToolBlocks(state) {
|
|
|
73
76
|
* 1) Multimodal results (read_image) embed FULL base64 images in the JSON —
|
|
74
77
|
* the human needs only the text part (model gets images via the multimodal
|
|
75
78
|
* channel); 2) results beyond maxRows are truncated in the block (full
|
|
76
|
-
* 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 尾行),尾截断 + 标记。 */
|
|
77
82
|
export function slimToolResultForDisplay(result, maxRows = 400) {
|
|
78
83
|
let displayResult = result
|
|
79
84
|
try {
|
|
@@ -81,9 +86,18 @@ export function slimToolResultForDisplay(result, maxRows = 400) {
|
|
|
81
86
|
if (parsed?.images?.length) displayResult = parsed.text ?? result
|
|
82
87
|
} catch { /* not JSON — show as-is */ }
|
|
83
88
|
const rows = String(displayResult).split("\n").filter((l) => l.trim())
|
|
84
|
-
|
|
89
|
+
const capped = rows.length > maxRows
|
|
85
90
|
? [...rows.slice(0, maxRows), `… (result truncated at ${maxRows} rows — full text in history)`]
|
|
86
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
|
|
87
101
|
}
|
|
88
102
|
/** Mark the dispatch-level tool carrier done when its result is consumed by a
|
|
89
103
|
* dedicated branch (subagent/escalate/advisor blocks) — without this the turn
|
package/src/tui/tool-events.mjs
CHANGED
|
@@ -19,29 +19,60 @@
|
|
|
19
19
|
import { C } from "./ansi.mjs"
|
|
20
20
|
import { formatToolSummary } from "./tool-summaries.mjs"
|
|
21
21
|
import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
|
|
22
|
-
import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "
|
|
22
|
+
import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "@thincoder/core/advisor/run.mjs"
|
|
23
23
|
import {
|
|
24
24
|
SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
|
|
25
25
|
routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, freezeDoneSubTasks,
|
|
26
26
|
ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
|
|
27
|
+
shiftFreezeAnchors,
|
|
27
28
|
} from "./subagent-blocks.mjs"
|
|
28
29
|
// 第 27 批 §12.3②:前缀正则换名 + import 源改文法模块(纯换名——语义零改)。
|
|
29
|
-
import { RELAY_PREFIX_RE } from "
|
|
30
|
-
import { TURN_CAP_MARK, STOPPED_MARK } from "
|
|
30
|
+
import { RELAY_PREFIX_RE } from "@thincoder/core/agent/relay-prefix.mjs"
|
|
31
|
+
import { TURN_CAP_MARK, STOPPED_MARK } from "@thincoder/core/agent/spawn-child.mjs"
|
|
32
|
+
// 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(消费面禁各自枚举——PROVIDER.md §6.20)
|
|
33
|
+
import { waitStatusText } from "@thincoder/core/provider/wait-status.mjs"
|
|
31
34
|
// 2026-09-05 module-split:ticks/maps/sweep/slim/settle/探测/find 族迁 tool-display.mjs——
|
|
32
35
|
// buildToolCallbacks 内部引用用本地 import;sweepToolBlocks re-export(agent-turn 消费面)
|
|
33
36
|
import {
|
|
34
37
|
TOOL_OUTPUT_LINE_CAP, REMINDER_CAP, REMINDER_PERSIST_TURNS,
|
|
35
38
|
_toolTicks, _subActions, _subActionQ,
|
|
36
39
|
tickStart, tickTake, settleToolBlock, isAsyncSpawnResult, isSpawnErrorResult,
|
|
37
|
-
findToolBlock, slimToolResultForDisplay, sweepToolBlocks,
|
|
40
|
+
findToolBlock, findToolLine, slimToolResultForDisplay, sweepToolBlocks,
|
|
38
41
|
} from "./tool-display.mjs"
|
|
39
42
|
export { sweepToolBlocks, slimToolResultForDisplay } from "./tool-display.mjs"
|
|
43
|
+
// TUI-OOM-ROOTCAUSE(TUI.md §15.3.1/§15.3.3):显示层额度(行/载体/输出环/评审/流式)。
|
|
44
|
+
import {
|
|
45
|
+
capText, capLine, capLines, appendCapped, capAdvisorText, accountLine, syncLineBudget,
|
|
46
|
+
LINE_TRUNC_MARKER, MIDDLE_TRUNC_MARKER, ADVISOR_CAP_OPTS, STREAM_CAP_OPTS,
|
|
47
|
+
TOOL_OUTPUT_ENTRY_MAX_CHARS, TOOL_OUTPUT_TOTAL_MAX_CHARS, ADVISOR_TEXT_MAX_CHARS,
|
|
48
|
+
} from "./display-budget.mjs"
|
|
49
|
+
|
|
50
|
+
/** 输出环字符维总量(§15.3.1 TOOL_OUTPUT_TOTAL_MAX_CHARS——与 200 条目环同款丢最旧)。 */
|
|
51
|
+
function trimOutputChars(block) {
|
|
52
|
+
let total = 0
|
|
53
|
+
for (const s of block.output) total += s.length
|
|
54
|
+
while (total > TOOL_OUTPUT_TOTAL_MAX_CHARS && block.output.length > 0) {
|
|
55
|
+
total -= block.output.shift().length
|
|
56
|
+
}
|
|
57
|
+
}
|
|
40
58
|
|
|
41
59
|
/** Build the agent callbacks + the shared flushStream for one turn.
|
|
42
60
|
* askPermission, askQuestion, saveSessionImpl } */
|
|
43
61
|
export function buildToolCallbacks(deps) {
|
|
44
62
|
const { agent, state, pushLine, render, scheduleRender, ensureAssistantLabel, askPermission, askBatchPermission, askQuestion, saveSessionImpl } = deps
|
|
63
|
+
// 行集总量对账便捷式(§15.3.3——冻结锚点平移交 subagent 族函数)
|
|
64
|
+
const budgetSync = () => syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
|
|
65
|
+
/** Advisor 有序块累积 + 额度(§15.3.1 ADVISOR_TEXT_MAX_CHARS——头 32K/尾 96K/中段标记)。 */
|
|
66
|
+
const pushAdvisorChunk = (raw, kind) => {
|
|
67
|
+
const blocks = state._advisorBlocks ??= []
|
|
68
|
+
const last = blocks.at(-1)
|
|
69
|
+
if (last && last.kind === kind) last.text = appendCapped(last.text, raw, ADVISOR_CAP_OPTS)
|
|
70
|
+
else blocks.push({ kind, text: appendCapped("", raw, ADVISOR_CAP_OPTS) })
|
|
71
|
+
// 多块总量:丢最旧块直至 ≤ 额度(裁决尾部优先)
|
|
72
|
+
let total = 0
|
|
73
|
+
for (const b of blocks) total += b.text.length
|
|
74
|
+
while (total > ADVISOR_TEXT_MAX_CHARS && blocks.length > 1) total -= blocks.shift().text.length
|
|
75
|
+
}
|
|
45
76
|
// NOTE: advisor buffers (_advisorThink/advisorStreaming) are cleared here too.
|
|
46
77
|
// Timing safety: onToolResult flushes _advisorThink into history and empties
|
|
47
78
|
// the buffers BEFORE onTurnEnd can call flushStream (tool result is
|
|
@@ -69,7 +100,7 @@ export function buildToolCallbacks(deps) {
|
|
|
69
100
|
// block content, never the main stream (D1). Routing details in subagent-blocks.
|
|
70
101
|
if (routeSubToken(state, t, scheduleRender)) return
|
|
71
102
|
ensureAssistantLabel()
|
|
72
|
-
state.streaming
|
|
103
|
+
state.streaming = appendCapped(state.streaming, t, STREAM_CAP_OPTS) // §15.3.1 STREAM_MAX_CHARS
|
|
73
104
|
scheduleRender()
|
|
74
105
|
},
|
|
75
106
|
onReasoning: (t) => {
|
|
@@ -77,7 +108,7 @@ export function buildToolCallbacks(deps) {
|
|
|
77
108
|
// buffer as kind=think (F2: same treatment as the main reasoning stream).
|
|
78
109
|
if (routeSubReasoning(state, t, scheduleRender)) return
|
|
79
110
|
ensureAssistantLabel()
|
|
80
|
-
state.reasoning
|
|
111
|
+
state.reasoning = appendCapped(state.reasoning, t, STREAM_CAP_OPTS) // §15.3.1 STREAM_MAX_CHARS
|
|
81
112
|
scheduleRender()
|
|
82
113
|
},
|
|
83
114
|
onToolCall: (name, args, toolId) => {
|
|
@@ -139,6 +170,8 @@ export function buildToolCallbacks(deps) {
|
|
|
139
170
|
done: false,
|
|
140
171
|
},
|
|
141
172
|
})
|
|
173
|
+
accountLine(state, state.lines[state.lines.length - 1]) // 载体入总量账(§15.3.4)
|
|
174
|
+
budgetSync()
|
|
142
175
|
tickStart(name, toolId)
|
|
143
176
|
},
|
|
144
177
|
// §7.2.3(方案 e):dispatch runOne 把工具 ctx 上的 _subagentKey(sync spawn/
|
|
@@ -236,11 +269,13 @@ export function buildToolCallbacks(deps) {
|
|
|
236
269
|
// screen of garbage (user report 2026-08-30). The model gets the image
|
|
237
270
|
// via the multimodal channel (agent.mjs), the human needs only the
|
|
238
271
|
// text part: strip image parts from the displayed result.
|
|
239
|
-
block.result = slimToolResultForDisplay(result)
|
|
272
|
+
block.result = slimToolResultForDisplay(result) // §15.3.1 RESULT 额度(行 400 + 字符 64K 双维)
|
|
240
273
|
block.summary = formatToolSummary(name, result)
|
|
241
274
|
block.done = true
|
|
242
275
|
const started = tickTake(name, toolId)
|
|
243
276
|
block.elapsed = started !== null ? Math.round(performance.now() - started) : null
|
|
277
|
+
accountLine(state, findToolLine(state, name, toolId)) // 载体原地变更 → 重新入账
|
|
278
|
+
budgetSync()
|
|
244
279
|
}
|
|
245
280
|
}
|
|
246
281
|
if (name === "advisor") {
|
|
@@ -255,17 +290,22 @@ export function buildToolCallbacks(deps) {
|
|
|
255
290
|
// _advisorBlocks keep rendering the running view until cleared at turn end.
|
|
256
291
|
const blocks = state._advisorBlocks ?? []
|
|
257
292
|
if (blocks.length > 0) {
|
|
258
|
-
|
|
293
|
+
// 冻结文本额度(§15.3.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——非静默截断,
|
|
294
|
+
// 代码评审 #3:capLines(...)[0] 会丢标记且截尾)
|
|
295
|
+
const text = capAdvisorText(blocks
|
|
259
296
|
.map((b) => b.text.replaceAll(ADVISOR_THINKING_PLACEHOLDER, ""))
|
|
260
297
|
.join("")
|
|
261
298
|
.replace(/\n{3,}/g, "\n\n")
|
|
262
|
-
.trim()
|
|
299
|
+
.trim())
|
|
263
300
|
if (text) {
|
|
264
|
-
|
|
301
|
+
const line = {
|
|
265
302
|
text: "advisor review",
|
|
266
303
|
color: C.dim,
|
|
267
304
|
_frozenAdvisor: text,
|
|
268
|
-
}
|
|
305
|
+
}
|
|
306
|
+
state.lines.push(line)
|
|
307
|
+
accountLine(state, line)
|
|
308
|
+
budgetSync()
|
|
269
309
|
}
|
|
270
310
|
}
|
|
271
311
|
}
|
|
@@ -298,25 +338,26 @@ export function buildToolCallbacks(deps) {
|
|
|
298
338
|
const isString = typeof chunk === "string"
|
|
299
339
|
const raw = isString ? chunk : String(chunk?.text ?? "")
|
|
300
340
|
const kind = isString ? "text" : (chunk?.kind ?? "text")
|
|
301
|
-
|
|
302
|
-
const last = blocks.at(-1)
|
|
303
|
-
if (last && last.kind === kind) last.text += raw
|
|
304
|
-
else blocks.push({ kind, text: raw })
|
|
341
|
+
pushAdvisorChunk(raw, kind) // §15.3.1 累积额度(多块丢最旧)
|
|
305
342
|
scheduleRender()
|
|
306
343
|
return
|
|
307
344
|
}
|
|
308
345
|
// Append into the CURRENT tool block's output buffer (the block is the
|
|
309
346
|
// display; no _live scroll lines anymore). N2-style cap keeps memory
|
|
310
|
-
// bounded: keep the LAST 200 output lines per call
|
|
347
|
+
// bounded: keep the LAST 200 output lines per call; 字符维双维
|
|
348
|
+
// (§15.3.1:单条目 ≤ TOOL_OUTPUT_ENTRY_MAX_CHARS、总量 ≤ TOOL_OUTPUT_TOTAL_MAX_CHARS 丢最旧)。
|
|
311
349
|
const block = findToolBlock(state, name, toolId)
|
|
312
350
|
if (block) {
|
|
313
351
|
for (const line of part.text.split("\n")) {
|
|
314
352
|
const trimmed = line.trimEnd()
|
|
315
|
-
if (trimmed) block.output.push(trimmed)
|
|
353
|
+
if (trimmed) block.output.push(capText(trimmed, { max: TOOL_OUTPUT_ENTRY_MAX_CHARS, keepHead: 6_000, keepTail: 2_000, marker: MIDDLE_TRUNC_MARKER }))
|
|
316
354
|
}
|
|
317
355
|
if (block.output.length > TOOL_OUTPUT_LINE_CAP) {
|
|
318
356
|
block.output.splice(0, block.output.length - TOOL_OUTPUT_LINE_CAP)
|
|
319
357
|
}
|
|
358
|
+
trimOutputChars(block)
|
|
359
|
+
accountLine(state, findToolLine(state, name, toolId))
|
|
360
|
+
budgetSync()
|
|
320
361
|
}
|
|
321
362
|
scheduleRender()
|
|
322
363
|
},
|
|
@@ -363,11 +404,12 @@ export function buildToolCallbacks(deps) {
|
|
|
363
404
|
state.tokens.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
|
|
364
405
|
state.tokens.reasoningTokens += usage.completion_tokens_details?.reasoning_tokens ?? 0
|
|
365
406
|
},
|
|
366
|
-
// Throttle wait (active gate / 429 backoff): show in status bar so user knows it's not frozen
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
407
|
+
// Throttle wait (active gate / 429 backoff): show in status bar so user knows it's not frozen.
|
|
408
|
+
// 相位值域/文案取自核单源(wait-status.mjs)——未知相位与 warn 前置告警不显示(不落兜底误标)。
|
|
409
|
+
onWait: (ev) => {
|
|
410
|
+
const s = waitStatusText(ev)
|
|
411
|
+
if (!s) return
|
|
412
|
+
state.status = s
|
|
371
413
|
render()
|
|
372
414
|
},
|
|
373
415
|
onTaskUpdate: (items) => {
|
|
@@ -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 在终端接管
|
package/src/tui/wizard.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* ctx: { agent, state, pushLine, pushLabel, render, persistRaw, openModelPicker }
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { PROVIDER_PRESETS as PRESETS } from "
|
|
9
|
+
import { PROVIDER_PRESETS as PRESETS } from "@thincoder/core/config.mjs"
|
|
10
10
|
import { ansi, C } from "./ansi.mjs"
|
|
11
11
|
import { computeLayout } from "./layout.mjs"
|
|
12
12
|
import { probeChannelModels } from "./model-catalog.mjs"
|
|
@@ -202,7 +202,7 @@ export function createWizard(ctx) {
|
|
|
202
202
|
agent.provider = { ...agent.providers.find((p) => p.name === f.name) }
|
|
203
203
|
agent.provider.model = agent.activeModel
|
|
204
204
|
if (agent.config?.agent?.compactThresholdAuto) {
|
|
205
|
-
const { resolveCompactThreshold } = await import("
|
|
205
|
+
const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
|
|
206
206
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
207
207
|
}
|
|
208
208
|
// agent.config 是 loadConfig merged——无 active* 键可写——defaultModel 随内存 merged 更新
|
|
@@ -226,7 +226,7 @@ export function createWizard(ctx) {
|
|
|
226
226
|
agent.config.embedding ??= {}
|
|
227
227
|
agent.config.embedding.apiKey = f.embedkey
|
|
228
228
|
if (agent.memory && !agent.memory.embedder) {
|
|
229
|
-
const { createEmbedder } = await import("
|
|
229
|
+
const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
|
|
230
230
|
agent.memory.embedder = createEmbedder(agent.config.embedding)
|
|
231
231
|
}
|
|
232
232
|
pushLine(`Vector search enabled (${agent.config.embedding.model ?? "BAAI/bge-m3"})`, C.tool)
|
|
@@ -6,12 +6,13 @@ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"
|
|
|
6
6
|
import { spawn } from "node:child_process"
|
|
7
7
|
import { fileURLToPath } from "node:url"
|
|
8
8
|
import { crashReportsDir } from "../crash-reports.mjs"
|
|
9
|
+
import { RECOVERY_SEQUENCE } from "./tui-lifecycle.mjs"
|
|
9
10
|
|
|
10
11
|
// F-3 信号语义(raw mode 既有 key-handler 双按语义——Ctrl+C = stdin 字节不产生信号 → 子正常退 → 父收
|
|
11
12
|
// exit 同码退):父忽略 SIGINT/SIGTERM——tee 不被打断。2026-09-09 实测:Windows process.kill(SIGINT)
|
|
12
13
|
// = 硬杀 ≠ 控制台 Ctrl+C 事件(handler 不触发)——信号面测试走 mock(真控制台端到端留发布前手动 QA)。
|
|
13
14
|
export const ignoreSignal = () => {} // no-op 单一引用——父信号 + stderr-error 监听共用——测试可精确复原
|
|
14
|
-
export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPath(new URL("../../bin/thincoder.mjs", import.meta.url)), spawnImpl = spawn, exitImpl = (code) => process.exit(code) } = {}) {
|
|
15
|
+
export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPath(new URL("../../bin/thincoder.mjs", import.meta.url)), spawnImpl = spawn, exitImpl = (code) => process.exit(code), writeImpl = (s) => process.stdout.write(s) } = {}) {
|
|
15
16
|
// ① mkdir 前置(评审 #2——prepareCrashReporting 只在子内跑——首启目录缺失会静默不包装)+ 开日志
|
|
16
17
|
//(文件头元信息行:时间/pid/argv——F-2——0600 append)——失败 → false:不包装直接跑现逻辑(尽力面)
|
|
17
18
|
let logPath = null
|
|
@@ -25,14 +26,29 @@ export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPat
|
|
|
25
26
|
const note = (s) => { try { appendFileSync(logPath, s) } catch { /* 落盘尽力面——不阻断 tee */ } }
|
|
26
27
|
let exitCode = null, exitSignal = null, settled = false
|
|
27
28
|
const finish = (code) => { if (!settled) { settled = true; exitImpl(code) } }
|
|
28
|
-
|
|
29
|
+
// TUI-OOM-ROOTCAUSE(CRASH-REPORTS.md §9.3):子异常退出(V8 fatal——不走 JS 钩子)→
|
|
30
|
+
// 包装父补发恢复序列(唯一存活方;序列单一来源 = tui-lifecycle.RECOVERY_SEQUENCE)。
|
|
31
|
+
// 守卫落点 = exit/close 处置内(**不在共享 finish**——spawn error 路径经 error→finish(1),
|
|
32
|
+
// 字面挂 finish 会在子未启动时补发 clearScreen 序列,违 F5③);按 exitCode/exitSignal
|
|
33
|
+
// 判定(code !== 0 || signal != null);异常退出零动作;正常退出(code 0)零干预;
|
|
34
|
+
// 每进程恰一次(recovered 守卫,exit/close 双路 + 30s 兜底路径同守);写失败不阻断收尾。
|
|
35
|
+
let recovered = false
|
|
36
|
+
const recoverTerminal = (code, signal) => {
|
|
37
|
+
if (recovered) return
|
|
38
|
+
if (!(code !== 0 || signal != null)) return
|
|
39
|
+
recovered = true
|
|
40
|
+
try { writeImpl(RECOVERY_SEQUENCE) } catch { /* 尽力面:终端已关/管道已断——不阻断收尾 */ }
|
|
41
|
+
}
|
|
42
|
+
// F3③(CRASH-REPORTS):近堆上限快照落点定向——--diagnostic-dir 为 Node 选项,须在脚本路径前;
|
|
43
|
+
// 无条件注入(gate 单点在 crash-reports.mjs——包装器不判 env);未触发快照时零副作用。
|
|
44
|
+
const child = spawnImpl(process.execPath, [`--diagnostic-dir=${dir}`, script, ...process.argv.slice(2)], {
|
|
29
45
|
stdio: ["inherit", "inherit", "pipe"], // 子 stderr pipe → tee;stdin/stdout 继承(TTY 原样)
|
|
30
46
|
env: { ...process.env, THINCODER_TUI_WRAPPED: "1" }, // env 门——子内判定不包装——纯现逻辑(红线)
|
|
31
47
|
windowsHide: false,
|
|
32
48
|
})
|
|
33
49
|
child.stderr.on("data", (chunk) => { try { process.stderr.write(chunk) } catch { /* 终端已关——日志仍落 */ } note(chunk) }) // ② tee 双写:终端实时 + 日志
|
|
34
|
-
child.on("exit", (code, signal) => { exitCode = code; exitSignal = signal; setTimeout(() => finish(exitCode ?? (exitSignal ? 1 : 0)), 30_000).unref() }) // ③ exit
|
|
35
|
-
child.on("close", () => finish(exitCode ?? (exitSignal ? 1 : 0))) // ④ close = stderr 尾数据全收(AC-2)→ 同码退
|
|
36
|
-
child.on("error", (err) => { note(`# spawn error: ${err.message}\n`); finish(1) }) // ⑤ spawn 失败不发 exit 只发 error
|
|
50
|
+
child.on("exit", (code, signal) => { exitCode = code; exitSignal = signal; recoverTerminal(code, signal); setTimeout(() => { recoverTerminal(exitCode, exitSignal); finish(exitCode ?? (exitSignal ? 1 : 0)) }, 30_000).unref() }) // ③ exit 记码;异常退出→补发恢复;兜底 30s 强退
|
|
51
|
+
child.on("close", () => { recoverTerminal(exitCode, exitSignal); finish(exitCode ?? (exitSignal ? 1 : 0)) }) // ④ close = stderr 尾数据全收(AC-2)→ 同码退
|
|
52
|
+
child.on("error", (err) => { note(`# spawn error: ${err.message}\n`); finish(1) }) // ⑤ spawn 失败不发 exit 只发 error(零序列动作——F5③)
|
|
37
53
|
return true
|
|
38
54
|
}
|
package/src/abort-provenance.mjs
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* abort-provenance.mjs — abort 来源标注词汇表(单一权威源——AGENT-LOOP.md §20.3 第 1/2 条)
|
|
3
|
-
*
|
|
4
|
-
* 子代理 / 评审 / 会诊死亡的可诊断性:产生点标注结构化来源(`err.abortInfo`)→
|
|
5
|
-
* controller 链 hop 逐跳保 reason(§20.3 第 4 条 #10)→ 结算 / 报告面用 `deathLine`
|
|
6
|
-
* 合成死亡行(原 message 前缀逐字保留 + 来源后缀)。
|
|
7
|
-
*
|
|
8
|
-
* 纯函数、零 import——任意层可引、无环(provider / proxy / agent / settle 族共用)。
|
|
9
|
-
* 词汇表:
|
|
10
|
-
* - trigger(5 值):`user`(reason.interrupt)· `timeout`(TimeoutError / 定时器面)·
|
|
11
|
-
* `cancel`(定向中止)· `stop`(整批 / 会话 / 回合级停)· `unknown`(残留 / 未标注——告警态)。
|
|
12
|
-
* - layer(3 值):`provider` / `agent` / `settle`;未标注错误回落 `unrecorded`(合成器 token)。
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** trigger 枚举权威(测试锁计数 5——AGENT-LOOP.md §20.3 第 1 条表)。 */
|
|
16
|
-
export const TRIGGERS = ["user", "timeout", "cancel", "stop", "unknown"]
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* trigger 判定(判据序 = §20.3 第 1 条表;signal.reason 四形态:
|
|
20
|
-
* ① `{interrupt:true(,message)}`(既有 user 面)② `TimeoutError`(原生——timeout 面)
|
|
21
|
-
* ③ `{abortTrigger:"cancel"|"stop"|"timeout"(,abortDetail)}`(新增——程序性中止)
|
|
22
|
-
* ④ 缺失 / 无已知键(→ unknown——诊断告警态,不得静默)。
|
|
23
|
-
*/
|
|
24
|
-
export function triggerOf(signal) {
|
|
25
|
-
const reason = signal?.reason
|
|
26
|
-
if (reason == null) return "unknown"
|
|
27
|
-
if (reason.interrupt === true) return "user"
|
|
28
|
-
if (reason.name === "TimeoutError" || reason.abortTrigger === "timeout") return "timeout"
|
|
29
|
-
if (reason.abortTrigger === "cancel") return "cancel"
|
|
30
|
-
if (reason.abortTrigger === "stop") return "stop"
|
|
31
|
-
return "unknown"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 产生点错误(abort 面):AbortError + `reason` 透传 + `abortInfo` 标注。
|
|
36
|
-
* message 取既有通用文案(既有前缀断言零回归——死亡行前缀逐字保留)。
|
|
37
|
-
*/
|
|
38
|
-
export function abortError(signal, layer, detail) {
|
|
39
|
-
const e = new DOMException("The operation was aborted", "AbortError")
|
|
40
|
-
e.reason = signal?.reason
|
|
41
|
-
e.abortInfo = { trigger: triggerOf(signal), layer, detail }
|
|
42
|
-
return e
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 产生点错误(定时器面):自有 message + `abortInfo{trigger:"timeout"}`。
|
|
47
|
-
* name 保持 "Error"(既有分类谓词——`AbortError` 分支——零变化)。
|
|
48
|
-
*/
|
|
49
|
-
export function timeoutError(message, layer, detail) {
|
|
50
|
-
const e = new Error(message)
|
|
51
|
-
e.abortInfo = { trigger: "timeout", layer, detail }
|
|
52
|
-
return e
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 外部错误补标(undici / fetch 拒否等):缺 `abortInfo` 才补(不覆盖产生点标注);
|
|
57
|
-
* 不改 `name` / `message`(消费面零回归)。
|
|
58
|
-
* 第 4 参 detail 为站点名(站点表逐站点传——自由短串)。
|
|
59
|
-
*/
|
|
60
|
-
export function annotateAbort(err, signal, layer, detail) {
|
|
61
|
-
if (err?.abortInfo) return err
|
|
62
|
-
try {
|
|
63
|
-
err.abortInfo = { trigger: triggerOf(signal), layer, detail: detail ?? "unrecorded" }
|
|
64
|
-
} catch { /* frozen error——保持未标注(合成器 unknown 告警兜底) */ }
|
|
65
|
-
return err
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 死亡行长度上限(AGENT-LOOP.md §20.3 第 3 条——超长优先截 detail)。 */
|
|
69
|
-
const MAX_LEN = 300
|
|
70
|
-
/** unknown 形态 detail token(§20.3 第 3 条逐字)。 */
|
|
71
|
-
const NO_REASON = "no reason on signal"
|
|
72
|
-
/** 未标注回落 token(layer / detail 两面兜底)。 */
|
|
73
|
-
const UNRECORDED = "unrecorded"
|
|
74
|
-
|
|
75
|
-
/** 信号域 detail 回落(§20.3 第 1 条形态③——`abortDetail` 载站点名;错误未标注时仍可辨发起动作)。 */
|
|
76
|
-
const detailOf = (signal) => (typeof signal?.reason?.abortDetail === "string" ? signal.reason.abortDetail : null)
|
|
77
|
-
|
|
78
|
-
/** 求值链(§20.3 第 1 条:err.abortInfo → triggerOf(signal) → err.name 兜底)。 */
|
|
79
|
-
function resolveAbort(err, signal) {
|
|
80
|
-
const info = err?.abortInfo
|
|
81
|
-
if (info?.trigger) return { trigger: info.trigger, layer: info.layer ?? UNRECORDED, detail: info.detail || null }
|
|
82
|
-
const fromSignal = triggerOf(signal)
|
|
83
|
-
if (fromSignal !== "unknown") return { trigger: fromSignal, layer: UNRECORDED, detail: detailOf(signal) }
|
|
84
|
-
if (err?.name === "TimeoutError") return { trigger: "timeout", layer: UNRECORDED, detail: detailOf(signal) }
|
|
85
|
-
// AbortError / 无名——告警态(unknown 显式,空后缀即违规)
|
|
86
|
-
return { trigger: "unknown", layer: UNRECORDED, detail: NO_REASON }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** detail 裁剪:优先截 detail(budget = 上限内剩余字符)。 */
|
|
90
|
-
function clipDetail(detail, budget) {
|
|
91
|
-
if (budget <= 0) return ""
|
|
92
|
-
return detail.length <= budget ? detail : detail.slice(0, Math.max(1, budget - 1)) + "…"
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 死亡行合成器(§20.3 第 3 条——报告面单点):
|
|
97
|
-
* `<原 message>[ ← cause: <cause.message>][ · abort(<trigger>@<layer>:<detail>)]`
|
|
98
|
-
* - 原 message 前缀逐字保留(既有前缀 / 包含断言零回归);
|
|
99
|
-
* - 后缀出现条件 = `err.abortInfo` 存在 ∨ `signal?.aborted` ∨ `err.name ∈ {AbortError, TimeoutError}`;
|
|
100
|
-
* - cause 段(P1)独立于后缀("fetch failed" 类网络死亡真因同判);
|
|
101
|
-
* - 总长 ≤300 字符(超长先截 detail,仍超再截尾)。
|
|
102
|
-
*/
|
|
103
|
-
export function deathLine(err, signal) {
|
|
104
|
-
let line = err?.message ?? String(err ?? "")
|
|
105
|
-
if (err?.cause?.message) line += ` ← cause: ${err.cause.message}`
|
|
106
|
-
const abortish = Boolean(err?.abortInfo) || Boolean(signal?.aborted) ||
|
|
107
|
-
err?.name === "AbortError" || err?.name === "TimeoutError"
|
|
108
|
-
if (abortish) {
|
|
109
|
-
const { trigger, layer, detail } = resolveAbort(err, signal)
|
|
110
|
-
const head = `${line} · abort(${trigger}@${layer}:`
|
|
111
|
-
// unknown 形态逐字用告警 token(§20.3 第 3 条——站点 detail 不进告警行);非串 detail 归一化
|
|
112
|
-
const text = trigger === "unknown" ? NO_REASON : (detail == null ? UNRECORDED : String(detail))
|
|
113
|
-
line = `${head}${clipDetail(text, MAX_LEN - head.length - 1)})`
|
|
114
|
-
}
|
|
115
|
-
return line.length > MAX_LEN ? line.slice(0, MAX_LEN - 1) + "…" : line
|
|
116
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* citations.mjs — host-verified citation checking (decision d698434).
|
|
3
|
-
* Extracted from advisor/run.mjs (file-size split). The evidence rule becomes
|
|
4
|
-
* a host fact: every `file:line: content` reference in a review is mechanically
|
|
5
|
-
* checked against the CURRENT disk state; mismatches mark the finding
|
|
6
|
-
* unverified and cannot support a push-back.
|
|
7
|
-
*
|
|
8
|
-
* 第 11 批(C / F14 / §14.5):解析候选 = cwd + **评审对象声明范围派生根**(声明仓根 /
|
|
9
|
-
* 声明文件目录 / 声明目录——纯路径派生,零扫描、零 git);命中判据三条件全中(围栏内 ∧
|
|
10
|
-
* 可读 ∧ 目标行含引文内容)⇒ 零新增假命中。失败原因三分(file unreadable /
|
|
11
|
-
* content mismatch @ path / path traversal)——父侧不再人肉复核。
|
|
12
|
-
*/
|
|
13
|
-
import { readFileSync, realpathSync } from "node:fs"
|
|
14
|
-
import { resolve, relative, dirname, extname, sep, isAbsolute, join } from "node:path"
|
|
15
|
-
|
|
16
|
-
// `file:line: content` citations — the file group is narrowed to source/config
|
|
17
|
-
// extensions so URLs (`example.com:8080: …`) don't become false-positive
|
|
18
|
-
// citations that fail as "file unreadable" in the verification report.
|
|
19
|
-
// Single-letter extensions (c/h) are kept — false positives (e.g. "a.c:1: x")
|
|
20
|
-
// are rare and only add a failed-citation line to the report; the report is
|
|
21
|
-
// advisory for the parent agent, never a crash path.
|
|
22
|
-
const CITATION_RE = /([\w./\\-]+\.(?:mjs|cjs|js|ts|jsx|tsx|mts|cts|py|rs|go|c|h|cpp|hpp|java|rb|php|sh|bash|json|md|markdown|mdx|yaml|yml|toml|css|html)):(\d+):\s*([^`\n]{4,})/g
|
|
23
|
-
|
|
24
|
-
/** Extract `file:line: content` citations from a review text. */
|
|
25
|
-
export function extractCitations(text) {
|
|
26
|
-
const out = []
|
|
27
|
-
for (const m of text.matchAll(CITATION_RE)) {
|
|
28
|
-
out.push({ file: m[1], line: Number(m[2]), content: m[3].trim() })
|
|
29
|
-
}
|
|
30
|
-
return out
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 候选解析根(§14.5——纯路径派生,候选顺序 = cwd → 各声明路径的派生根):
|
|
34
|
-
* 声明仓根(`cwd/<segs[0]>`)与 声明文件目录 / 声明目录本身。声明在 cwd 之外 → 不派生
|
|
35
|
-
* (relative 越出 cwd 或跨盘符——候选与声明脱节即假命中面)。 */
|
|
36
|
-
function citationRoots(cwd, scope) {
|
|
37
|
-
const base = resolve(cwd)
|
|
38
|
-
const roots = [base]
|
|
39
|
-
for (const s of Array.isArray(scope) ? scope : []) {
|
|
40
|
-
if (typeof s !== "string" || !s.trim()) continue
|
|
41
|
-
const abs = resolve(base, s)
|
|
42
|
-
const rel = relative(base, abs)
|
|
43
|
-
if (!rel || rel.startsWith("..") || isAbsolute(rel)) continue
|
|
44
|
-
const segs = rel.split(/[\\/]/).filter(Boolean)
|
|
45
|
-
if (segs.length === 0) continue
|
|
46
|
-
const repoRoot = join(base, segs[0])
|
|
47
|
-
const declaredDir = extname(abs) ? dirname(abs) : abs
|
|
48
|
-
for (const r of [repoRoot, declaredDir]) if (!roots.includes(r)) roots.push(r)
|
|
49
|
-
}
|
|
50
|
-
return roots
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 单引文解析(§14.5):按候选顺序试 `resolve(root, file)`;三条件全中才算命中。
|
|
54
|
-
* @returns {{matched: true, root: string, resolved: string}|{matched: false, reason: string}} */
|
|
55
|
-
function resolveCitation(citation, roots, base) {
|
|
56
|
-
const fence = base + sep
|
|
57
|
-
let mismatch = null
|
|
58
|
-
let traversal = false
|
|
59
|
-
for (const root of roots) {
|
|
60
|
-
let real
|
|
61
|
-
try {
|
|
62
|
-
// Path confinement: citation paths are LLM-generated — never trust them.
|
|
63
|
-
// A hallucinated "../config.json" would otherwise read (and leak via the
|
|
64
|
-
// report) files outside the project, including API-key configs.
|
|
65
|
-
// realpathSync resolves symlinks too — a link inside the project that
|
|
66
|
-
// points outside must not pass the prefix check.
|
|
67
|
-
real = realpathSync(resolve(root, citation.file))
|
|
68
|
-
} catch {
|
|
69
|
-
continue // 该候选无此文件(或不可解析)——试下一候选
|
|
70
|
-
}
|
|
71
|
-
if (!real.startsWith(fence)) { traversal = true; continue }
|
|
72
|
-
let line = ""
|
|
73
|
-
try {
|
|
74
|
-
line = readFileSync(real, "utf8").split("\n")[citation.line - 1] ?? ""
|
|
75
|
-
} catch {
|
|
76
|
-
continue // 存在但读不了(目录等)——按未命中处理,试下一候选
|
|
77
|
-
}
|
|
78
|
-
if (line.includes(citation.content)) {
|
|
79
|
-
return { matched: true, root, resolved: relative(base, real).split(sep).join("/") }
|
|
80
|
-
}
|
|
81
|
-
mismatch ??= real
|
|
82
|
-
}
|
|
83
|
-
if (mismatch) return { matched: false, reason: `content mismatch @ ${relative(base, mismatch).split(sep).join("/")}` }
|
|
84
|
-
if (traversal) return { matched: false, reason: "path traversal" }
|
|
85
|
-
return { matched: false, reason: "file unreadable" }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Mechanically verify citations against the CURRENT file state: read the file,
|
|
90
|
-
* take the exact line, check it CONTAINS the quoted content. Reports
|
|
91
|
-
* N/M matched + the mismatches. Unverified citations cannot support a
|
|
92
|
-
* push-back — the evidence rule becomes a host fact, not a prompt wish.
|
|
93
|
-
* @param {string} text — the review text
|
|
94
|
-
* @param {string} cwd — the agent's working directory (workspace root)
|
|
95
|
-
* @param {{scope?: string[]}} [opts] — scope = 评审对象声明路径(documents + paths);
|
|
96
|
-
* 省略 ⇒ 旧行为(仅 cwd 候选——签名向后兼容)。
|
|
97
|
-
*/
|
|
98
|
-
export function verifyCitations(text, cwd, opts = {}) {
|
|
99
|
-
const citations = extractCitations(text)
|
|
100
|
-
const matched = []
|
|
101
|
-
const failed = []
|
|
102
|
-
const base = resolve(cwd)
|
|
103
|
-
const roots = citationRoots(cwd, opts?.scope)
|
|
104
|
-
for (const c of citations) {
|
|
105
|
-
const r = resolveCitation(c, roots, base)
|
|
106
|
-
if (r.matched) {
|
|
107
|
-
// 命中根记录(§14.5):经派生根解析(非 cwd 直解)的命中在报告中注明解析路径。
|
|
108
|
-
matched.push(r.root === base ? c : { ...c, root: r.root, resolved: r.resolved })
|
|
109
|
-
} else {
|
|
110
|
-
failed.push({ ...c, reason: r.reason })
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return { total: citations.length, matched, failed }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Append the verification report to the review text (visible to the parent agent). */
|
|
117
|
-
export function appendCitationReport(text, cwd, opts = {}) {
|
|
118
|
-
const { total, matched, failed } = verifyCitations(text, cwd, opts)
|
|
119
|
-
if (total === 0) return text // no citations — nothing to verify
|
|
120
|
-
const lines = [
|
|
121
|
-
"",
|
|
122
|
-
"---",
|
|
123
|
-
`[host-verified] ${matched.length}/${total} citations match current file state.`,
|
|
124
|
-
]
|
|
125
|
-
// 命中根透明(F14/§14.5):经声明范围派生根解析的命中逐条注明解析路径(cwd 直解的不列
|
|
126
|
-
// ——零噪音;列的正是修复前会被误报为 unreadable 的裸路径引用)。
|
|
127
|
-
const derived = matched.filter((c) => c.resolved)
|
|
128
|
-
if (derived.length > 0) {
|
|
129
|
-
lines.push("Citations resolved via the declared review scope (bare path — resolved root noted):")
|
|
130
|
-
for (const c of derived.slice(0, 10)) lines.push(`- ${c.file}:${c.line} → ${c.resolved}`)
|
|
131
|
-
}
|
|
132
|
-
if (failed.length > 0) {
|
|
133
|
-
lines.push("Citations that do NOT match the current file state (treat their claims as unverified):")
|
|
134
|
-
for (const f of failed.slice(0, 10)) {
|
|
135
|
-
lines.push(`- ${f.file}:${f.line}: ${f.content.slice(0, 80)}${f.reason ? ` (${f.reason})` : ""}`)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return text + lines.join("\n")
|
|
139
|
-
}
|