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
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DESC,
|
|
3
|
+
truncate,
|
|
4
|
+
resolveInCwd,
|
|
5
|
+
globToRegex,
|
|
6
|
+
splitGlobPatterns,
|
|
7
|
+
compileGlobMatchers,
|
|
8
|
+
normalizeEOL,
|
|
9
|
+
IGNORED_DIRS,
|
|
10
|
+
} from "./shared.mjs";
|
|
11
|
+
import { readFile, readdir, stat, lstat } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
/** Escape a string for literal regex matching (grep literal=true). */
|
|
15
|
+
function escapeRegExp(s) {
|
|
16
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------- glob
|
|
20
|
+
|
|
21
|
+
export const globTool = {
|
|
22
|
+
name: "glob",
|
|
23
|
+
description: DESC("glob"),
|
|
24
|
+
parameters: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
pattern: { type: "string", description: "Glob pattern — supports **, *, ?, [..], {a,b} braces, and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
28
|
+
path: { type: "string", description: "Directory to search in (default cwd)" },
|
|
29
|
+
},
|
|
30
|
+
required: ["pattern"],
|
|
31
|
+
},
|
|
32
|
+
readonly: true,
|
|
33
|
+
async execute(args, ctx) {
|
|
34
|
+
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
35
|
+
// §17 调用侧拆分(评审 #5 职责分层):空格分隔 include !exclude 多模式在这里拆;
|
|
36
|
+
// globToRegex 只收单个模式(数组由 compileGlobMatchers 收)。
|
|
37
|
+
let match
|
|
38
|
+
try {
|
|
39
|
+
const parts = splitGlobPatterns(args.pattern)
|
|
40
|
+
if (parts.length === 0) return "Error: pattern is required"
|
|
41
|
+
match = compileGlobMatchers(parts).test
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return `glob error: invalid pattern "${args.pattern}": ${e.message}`
|
|
44
|
+
}
|
|
45
|
+
const results = []
|
|
46
|
+
for await (const relPath of walkFiles(base)) {
|
|
47
|
+
if (match(relPath)) {
|
|
48
|
+
results.push(relPath)
|
|
49
|
+
if (results.length >= 1000) break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (results.length === 0) return "(no matches)"
|
|
53
|
+
return truncate(results.sort().join("\n"))
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Recursively traverse files, yield relative paths (skip IGNORED_DIRS) */
|
|
58
|
+
async function* walkFiles(dir, rel = "") {
|
|
59
|
+
let entries
|
|
60
|
+
try {
|
|
61
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
62
|
+
} catch {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
// Skip ignored dirs AND symbolic links (symlinks to directories would cause infinite loops)
|
|
67
|
+
if (e.isSymbolicLink()) continue
|
|
68
|
+
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
69
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
70
|
+
if (e.isDirectory()) {
|
|
71
|
+
yield* walkFiles(join(dir, e.name), relPath)
|
|
72
|
+
} else {
|
|
73
|
+
yield relPath
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------- grep
|
|
79
|
+
|
|
80
|
+
export const grepTool = {
|
|
81
|
+
name: "grep",
|
|
82
|
+
description: DESC("grep"),
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
pattern: { type: "string", description: "Regular expression, or a literal string when literal=true" },
|
|
87
|
+
path: { type: "string", description: "Directory or file to search (default cwd)" },
|
|
88
|
+
glob: { type: "string", description: "Only search files matching this glob — supports **, *, ?, [..], {a,b} braces and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
89
|
+
ignoreCase: { type: "boolean", description: "Case-insensitive match (default false)" },
|
|
90
|
+
literal: { type: "boolean", description: "Literal string match — no regex interpretation (default false)" },
|
|
91
|
+
before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
|
|
92
|
+
after: { type: "integer", description: "Lines of context to show after each match (grep -A). Default 0" },
|
|
93
|
+
},
|
|
94
|
+
required: ["pattern"],
|
|
95
|
+
},
|
|
96
|
+
readonly: true,
|
|
97
|
+
async execute(args, ctx) {
|
|
98
|
+
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
99
|
+
let regex
|
|
100
|
+
try {
|
|
101
|
+
const pat = args.literal ? escapeRegExp(String(args.pattern)) : args.pattern
|
|
102
|
+
regex = new RegExp(pat, args.ignoreCase ? "i" : "")
|
|
103
|
+
} catch (e) {
|
|
104
|
+
throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`, { cause: e })
|
|
105
|
+
}
|
|
106
|
+
// §17 调用侧拆分:glob 参数支持空格分隔 include !exclude 多模式(include/exclude 求交)。
|
|
107
|
+
let fileTest = null
|
|
108
|
+
if (args.glob) {
|
|
109
|
+
try {
|
|
110
|
+
const parts = splitGlobPatterns(args.glob)
|
|
111
|
+
if (parts.length > 0) fileTest = compileGlobMatchers(parts).test
|
|
112
|
+
} catch (e) {
|
|
113
|
+
throw new Error(`grep glob /${args.glob}/ is invalid: ${e.message}`, { cause: e })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const before = Math.max(0, Math.floor(args.before ?? 0))
|
|
117
|
+
const after = Math.max(0, Math.floor(args.after ?? 0))
|
|
118
|
+
const wantCtx = before > 0 || after > 0
|
|
119
|
+
const hits = [] // { file, line(1-based), text }
|
|
120
|
+
const fileLines = new Map() // file -> string[] (cached only when context lines requested)
|
|
121
|
+
|
|
122
|
+
async function search(file) {
|
|
123
|
+
let content
|
|
124
|
+
try {
|
|
125
|
+
// Large file guard: skip files over 10MB to prevent OOM
|
|
126
|
+
const fst = await stat(file)
|
|
127
|
+
if (fst.size > 10_000_000) return
|
|
128
|
+
content = normalizeEOL(await readFile(file, "utf8"))
|
|
129
|
+
} catch {
|
|
130
|
+
return // Skip unreadable files; binary files will be read as UTF-8 and searched (may produce garbled matches)
|
|
131
|
+
}
|
|
132
|
+
const lines = content.split("\n")
|
|
133
|
+
if (wantCtx) fileLines.set(file, lines)
|
|
134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135
|
+
if (regex.test(lines[i])) {
|
|
136
|
+
hits.push({ file, line: i + 1, text: lines[i] })
|
|
137
|
+
if (hits.length >= 200) return
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function walk(target, rel) {
|
|
143
|
+
if (hits.length >= 200) return
|
|
144
|
+
// Use lstat to avoid following symlinks — prevents ./evil → /etc from making grep scan the entire system
|
|
145
|
+
let s
|
|
146
|
+
try { s = await lstat(target) } catch { return }
|
|
147
|
+
if (!s.isDirectory()) {
|
|
148
|
+
// rel 为相对搜索基的路径(排除前缀如 !test/** 必须按目录路径作用——不能用裸文件名);
|
|
149
|
+
// 单文件目标(path 指向文件)时 rel 为空 → 退化为按文件名过滤(既有语义)。
|
|
150
|
+
if (!fileTest || fileTest(rel || target.split(/[\\/]/).pop())) await search(target)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
let entries
|
|
154
|
+
try {
|
|
155
|
+
entries = await readdir(target, { withFileTypes: true })
|
|
156
|
+
} catch {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
for (const e of entries) {
|
|
160
|
+
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
161
|
+
await walk(join(target, e.name), rel ? `${rel}/${e.name}` : e.name)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await walk(base, "")
|
|
166
|
+
if (hits.length === 0) return "(no matches)"
|
|
167
|
+
|
|
168
|
+
// No context: keep original path:line: content format
|
|
169
|
+
if (!wantCtx) {
|
|
170
|
+
return truncate(hits.map((h) => `${h.file}:${h.line}: ${h.text}`).join("\n"))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// With context: matching lines use ':', context lines use '-' (like ripgrep); overlapping ranges in same file are merged and de-duplicated
|
|
174
|
+
const fileMatched = new Map() // file -> Set<line>
|
|
175
|
+
for (const h of hits) {
|
|
176
|
+
if (!fileMatched.has(h.file)) fileMatched.set(h.file, new Set())
|
|
177
|
+
fileMatched.get(h.file).add(h.line)
|
|
178
|
+
}
|
|
179
|
+
const out = []
|
|
180
|
+
for (const [file, matchedLines] of fileMatched) {
|
|
181
|
+
const lines = fileLines.get(file) ?? []
|
|
182
|
+
const lineSet = new Set()
|
|
183
|
+
for (const ml of matchedLines) {
|
|
184
|
+
for (let l = Math.max(1, ml - before); l <= Math.min(lines.length, ml + after); l++) lineSet.add(l)
|
|
185
|
+
}
|
|
186
|
+
for (const l of [...lineSet].sort((a, b) => a - b)) {
|
|
187
|
+
const sep = matchedLines.has(l) ? ":" : "-"
|
|
188
|
+
out.push(`${file}${sep}${l}${sep} ${lines[l - 1]}`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return truncate(out.join("\n"))
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const lsTool = {
|
|
196
|
+
name: "ls",
|
|
197
|
+
description: DESC("ls"),
|
|
198
|
+
parameters: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {
|
|
201
|
+
path: { type: "string", description: "Directory path (default cwd)" },
|
|
202
|
+
filter: { type: "string", description: "Only list entries matching this glob (e.g. '*.mjs', '*test*')" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
readonly: true,
|
|
206
|
+
async execute(args, ctx) {
|
|
207
|
+
const abs = resolveInCwd(ctx, args.path ?? ".")
|
|
208
|
+
const filterRe = args.filter ? globToRegex(args.filter) : null
|
|
209
|
+
let entries
|
|
210
|
+
try {
|
|
211
|
+
entries = await readdir(abs, { withFileTypes: true })
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") throw new Error(`ls: ${args.path ?? "."} — ${e.code === "ENOTDIR" ? "not a directory" : "not found"}`, { cause: e })
|
|
214
|
+
throw e
|
|
215
|
+
}
|
|
216
|
+
const rows = await Promise.all(
|
|
217
|
+
entries
|
|
218
|
+
.filter((e) => !filterRe || filterRe.test(e.name))
|
|
219
|
+
.slice(0, 500)
|
|
220
|
+
.map(async (e) => {
|
|
221
|
+
const s = await stat(join(abs, e.name)).catch(() => null)
|
|
222
|
+
const isDir = e.isDirectory()
|
|
223
|
+
return {
|
|
224
|
+
dir: isDir,
|
|
225
|
+
name: e.name + (isDir ? "/" : ""),
|
|
226
|
+
size: s?.size ?? 0,
|
|
227
|
+
mtime: s ? s.mtime.toISOString().slice(0, 16).replace("T", " ") : "?",
|
|
228
|
+
}
|
|
229
|
+
}),
|
|
230
|
+
)
|
|
231
|
+
rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
|
|
232
|
+
if (rows.length === 0) return "(no entries)"
|
|
233
|
+
const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
|
|
234
|
+
return truncate(out.join("\n"))
|
|
235
|
+
},
|
|
236
|
+
}
|
|
@@ -16,12 +16,21 @@
|
|
|
16
16
|
* 与 log.mjs 的 THINCODER_LOG_DIR 同惯例)。
|
|
17
17
|
* - 脱敏(D-TR2):复用 log.mjs 字段名黑名单(apikey/designtoken/password/secret/
|
|
18
18
|
* token/authorization/proxyuri/proxy)+ SECRET_FORM 形态扫描(redactSecret)——对
|
|
19
|
-
* messages/content/reasoning/toolCalls/error
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* messages/content/reasoning/toolCalls/error 全字段应用;不发明新遮蔽模式。
|
|
20
|
+
* **单遍序列化(TUI-OOM-ROOTCAUSE——§23.3.2:修正轮 #2 使「不截断」表述作废)**:
|
|
21
|
+
* 脱敏内联进输出缓冲(不再构造复制图 + 二次全量字符串)——字段名/次序与既有
|
|
22
|
+
* `JSON.stringify` 形态同构(T-TR1 等价断言)。
|
|
23
|
+
* - 容量有界(N-TR1 修订):单消息字符串超 `TRACE_MESSAGE_MAX_CHARS` 截断(头 16K +
|
|
24
|
+
* 中段标记 + 尾 48K);序列化总长超 `TRACE_RECORD_MAX_CHARS` → `messages` 降级为
|
|
25
|
+
* stub 串(其余字段保留);在途写盘份数 ≤ `TRACE_PENDING_MAX`(超限丢弃并计数——
|
|
26
|
+
* 尽力面,可观测)。目录按日组织(YYYY-MM-DD),可按日/会话过滤;保留期清理见
|
|
27
|
+
* `cleanupTraces`(D-TR8)。
|
|
23
28
|
* - seq = 当日目录内最大已有 seq + 1(D-TR3——跨会话/进程重启不覆写既有旧轨迹——
|
|
24
|
-
* 18.6.1 评审 #3
|
|
29
|
+
* 18.6.1 评审 #3);**进程内 seqCache(§23.3.2——消除逐调用同步扫目录)**:首次
|
|
30
|
+
* readdirSync 后进程内递增预留,目录被清理(retention)后取下界重扫一次(防御)。
|
|
31
|
+
* 多进程语义(修正轮 #10——登记):同 cwd 多实例各自进程内缓存——seq 可撞、同
|
|
32
|
+
* sessionKey 记录并入同名文件(append 不覆写);**可容忍**(本机 traces = 诊断面、
|
|
33
|
+
* 默认 OFF、撞号不损坏数据);不做跨进程协调/落盘校验。
|
|
25
34
|
* - 日期分日按本地时区(D-TR3——2026-09-04 fix round1:初版 toISOString()=UTC——
|
|
26
35
|
* 本地 00:00-08:00 的调用会落进前一日目录——改本地日期字符串 YYYY-MM-DD——T-TR12)。
|
|
27
36
|
* - isContinuation(D-TR1——2026-09-04 fix round1):续写/重试链标记——true = 该调用
|
|
@@ -35,6 +44,7 @@ import { createHash } from "node:crypto"
|
|
|
35
44
|
import { configDir } from "../config.mjs"
|
|
36
45
|
import { redactSecret, errText, classifyErr } from "../log.mjs"
|
|
37
46
|
import { normalizeCwd } from "../session-slots.mjs"
|
|
47
|
+
import { capText } from "../text-budget.mjs"
|
|
38
48
|
|
|
39
49
|
/** 轨迹根目录:THINCODER_TRACES_DIR(测试隔离/override——同 THINCODER_LOG_DIR
|
|
40
50
|
* 惯例)> ~/.thincoder/traces(configDir——D-TR3——与 sessions/ 同域)。 */
|
|
@@ -65,10 +75,15 @@ export function localDateStr(date = new Date()) {
|
|
|
65
75
|
return `${y}-${m}-${d}`
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
/** Test hooks(_rateHooks 同惯例——rate.mjs
|
|
69
|
-
* UTC
|
|
78
|
+
/** Test hooks(_rateHooks 同惯例——rate.mjs):now/readdir/exists/mkdir/append 注入缝
|
|
79
|
+
* (T-TR5 计数 readdirSync;T-TR4 慢写替身;T-TR12 注入本地日 ≠ UTC 日的时刻)。
|
|
80
|
+
* 生产永远走真实实现。 */
|
|
70
81
|
export const _traceHooks = {
|
|
71
82
|
now: () => new Date(),
|
|
83
|
+
readdir: (dir) => readdirSync(dir),
|
|
84
|
+
exists: (p) => existsSync(p),
|
|
85
|
+
mkdir: (dir) => mkdir(dir, { recursive: true }),
|
|
86
|
+
append: (path, text) => appendFile(path, text, "utf8"),
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
/** 当日轨迹目录(D-TR3:traces/YYYY-MM-DD——分日组织——N-TR1)。 */
|
|
@@ -76,32 +91,71 @@ export function tracesDirFor(dateStr) {
|
|
|
76
91
|
return join(tracesRoot(), dateStr)
|
|
77
92
|
}
|
|
78
93
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
// ─── 额度常量(§23.3.2——编译期常量,不新增配置项 D-TR4)─────────────────────
|
|
95
|
+
|
|
96
|
+
/** 单消息字符串额度(消息内容/推理/工具参数串)——超限:头 16K + 中段标记 + 尾 48K。 */
|
|
97
|
+
export const TRACE_MESSAGE_MAX_CHARS = 65_536
|
|
98
|
+
/** 单记录总长额度——超限:`messages` 字段整体降级为 stub 串(元数据保留)。 */
|
|
99
|
+
export const TRACE_RECORD_MAX_CHARS = 4_000_000
|
|
100
|
+
/** 在途写盘份数上限(超限丢弃本记录 + 计数——不阻塞模型调用路径)。 */
|
|
101
|
+
export const TRACE_PENDING_MAX = 8
|
|
102
|
+
|
|
103
|
+
/** 内容额度标记(逐字——进测试断言;同族工具预览形态)。 */
|
|
104
|
+
const MSG_MARKER = "… [trace truncated: N chars omitted] …"
|
|
105
|
+
const HEAD_CHARS = 16_384
|
|
106
|
+
const TAIL_CHARS = 49_152
|
|
107
|
+
|
|
108
|
+
/** 受额度约束的字段名(消息内容/推理/工具参数串——§23.3.2 字面集)。 */
|
|
109
|
+
const CAPPED_KEYS = new Set(["content", "reasoning", "reasoning_content", "reasoningContent", "arguments"])
|
|
110
|
+
|
|
111
|
+
// ─── 在途计数(§23.3.2 表 4 候选 1:待写计数上限 8 + 超限丢弃计数)────────────
|
|
112
|
+
|
|
113
|
+
let _pendingWrites = 0
|
|
114
|
+
let _droppedRecords = 0
|
|
115
|
+
let _saturationNotified = false
|
|
116
|
+
|
|
117
|
+
// 进程内 seq 缓存(§23.3.2:dir → 已见/已预留 max——首次 readdirSync 后递增预留;
|
|
118
|
+
// 目录被清理(retention)后取下界重扫一次(防御))。
|
|
119
|
+
const _seqCache = new Map()
|
|
120
|
+
|
|
121
|
+
/** 在途/丢弃观测(测试缝——T-TR4)。 */
|
|
122
|
+
export function _traceStoreStats() {
|
|
123
|
+
return { pending: _pendingWrites, dropped: _droppedRecords }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 测试缝:清进程内状态(seqCache + 在途/丢弃计数)。 */
|
|
127
|
+
export function _resetTraceStateForTest() {
|
|
128
|
+
_seqCache.clear()
|
|
129
|
+
_pendingWrites = 0
|
|
130
|
+
_droppedRecords = 0
|
|
131
|
+
_saturationNotified = false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function scanMaxSeq(dir) {
|
|
135
|
+
if (!_traceHooks.exists(dir)) return 0
|
|
136
|
+
let names
|
|
137
|
+
try { names = _traceHooks.readdir(dir) } catch { return 0 }
|
|
138
|
+
let max = 0
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
const m = name.match(/-(\d+)\.jsonl$/)
|
|
141
|
+
if (m) max = Math.max(max, Number(m[1]))
|
|
142
|
+
}
|
|
143
|
+
return max
|
|
144
|
+
}
|
|
82
145
|
|
|
83
146
|
/** seq = max(当日目录最大已有 seq, 进程内已预留) + 1(D-TR3——跨会话/进程重启
|
|
84
|
-
* 不覆写——T-TR8/T-TR10;同步预留 = 异步写盘启动前的原子号位分配)。
|
|
147
|
+
* 不覆写——T-TR8/T-TR10;同步预留 = 异步写盘启动前的原子号位分配)。
|
|
148
|
+
* §23.3.2:seqCache 进程内递增——不逐调用 readdirSync(目录被清理/缺失 → 重扫一次)。 */
|
|
85
149
|
export function nextTraceSeq(dateStr) {
|
|
86
150
|
const dir = tracesDirFor(dateStr)
|
|
87
|
-
let max =
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} catch {
|
|
93
|
-
// 目录不可读——按预留表续号(不静默撞号)
|
|
94
|
-
max = _reservedSeq.get(dir) ?? 0
|
|
95
|
-
_reservedSeq.set(dir, max + 1)
|
|
96
|
-
return max + 1
|
|
97
|
-
}
|
|
98
|
-
for (const name of names) {
|
|
99
|
-
const m = name.match(/-(\d+)\.jsonl$/)
|
|
100
|
-
if (m) max = Math.max(max, Number(m[1]))
|
|
101
|
-
}
|
|
151
|
+
let max = _seqCache.get(dir)
|
|
152
|
+
if (max === undefined || !_traceHooks.exists(dir)) {
|
|
153
|
+
// 首次调用(或目录被 retention 清理/缺失)——重扫磁盘取真实下界(防御:清除后重号安全)
|
|
154
|
+
const disk = scanMaxSeq(dir)
|
|
155
|
+
max = Math.max(disk, max ?? 0)
|
|
102
156
|
}
|
|
103
|
-
const seq =
|
|
104
|
-
|
|
157
|
+
const seq = max + 1
|
|
158
|
+
_seqCache.set(dir, seq)
|
|
105
159
|
return seq
|
|
106
160
|
}
|
|
107
161
|
|
|
@@ -111,18 +165,57 @@ export function tracesEnabled(logCtx) {
|
|
|
111
165
|
return logCtx?.traces !== false
|
|
112
166
|
}
|
|
113
167
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
// ─── 单遍序列化(§23.3.2 D-TR1——脱敏内联、零复制图、字段名/次序同构)────────
|
|
169
|
+
|
|
170
|
+
const OMIT = Symbol("omit") // JSON.stringify 语义:对象属性省略(数组元素 → null)
|
|
171
|
+
|
|
172
|
+
function scalarJson(key, v) {
|
|
173
|
+
if (typeof v === "string") {
|
|
174
|
+
// 额度(§23.3.2)+ 脱敏(D-TR2)单点:先限长(标记随附)再遮蔽形态扫描——
|
|
175
|
+
// 输出与既有 redactValue(key, s) 串值路径逐字同形(除外显式截断标记)。
|
|
176
|
+
if (v.length > TRACE_MESSAGE_MAX_CHARS && CAPPED_KEYS.has(key)) {
|
|
177
|
+
return JSON.stringify(redactSecret(key, capText(v, { max: TRACE_MESSAGE_MAX_CHARS, keepHead: HEAD_CHARS, keepTail: TAIL_CHARS, marker: MSG_MARKER })))
|
|
178
|
+
}
|
|
179
|
+
return JSON.stringify(redactSecret(key, v))
|
|
180
|
+
}
|
|
181
|
+
if (v === null) return "null"
|
|
182
|
+
if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null" // JSON.stringify(NaN/±Infinity) === "null"
|
|
183
|
+
if (typeof v === "boolean") return v ? "true" : "false"
|
|
184
|
+
return undefined // undefined/function/symbol/bigint → 省略(JSON.stringify 同语义)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function writeValue(out, key, v) {
|
|
188
|
+
const s = scalarJson(key, v)
|
|
189
|
+
if (s !== undefined) { out.push(s); return true }
|
|
190
|
+
if (Array.isArray(v)) {
|
|
191
|
+
out.push("[")
|
|
192
|
+
for (let i = 0; i < v.length; i++) {
|
|
193
|
+
if (i) out.push(",")
|
|
194
|
+
if (!writeValue(out, key, v[i])) out.push("null") // 数组元素不可省略
|
|
195
|
+
}
|
|
196
|
+
out.push("]")
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
if (v && typeof v === "object") {
|
|
200
|
+
if (typeof v.toJSON === "function") { // Date/宿主对象:保持 JSON.stringify 等价
|
|
201
|
+
const js = JSON.stringify(v)
|
|
202
|
+
out.push(js === undefined ? "null" : js)
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
out.push("{")
|
|
206
|
+
let first = true
|
|
207
|
+
for (const [k, val] of Object.entries(v)) {
|
|
208
|
+
const omit = val === undefined || typeof val === "function" || typeof val === "symbol" || typeof val === "bigint"
|
|
209
|
+
if (omit) continue
|
|
210
|
+
if (!first) out.push(",")
|
|
211
|
+
first = false
|
|
212
|
+
out.push(JSON.stringify(k), ":")
|
|
213
|
+
writeValue(out, k, val)
|
|
214
|
+
}
|
|
215
|
+
out.push("}")
|
|
216
|
+
return true
|
|
124
217
|
}
|
|
125
|
-
return
|
|
218
|
+
return false // OMIT
|
|
126
219
|
}
|
|
127
220
|
|
|
128
221
|
/**
|
|
@@ -142,6 +235,16 @@ export function recordChatTrace(provider, opts = {}, result = null, error = null
|
|
|
142
235
|
if (!writeEnabled()) return
|
|
143
236
|
const logCtx = opts.logCtx ?? {}
|
|
144
237
|
if (!tracesEnabled(logCtx)) return
|
|
238
|
+
// §23.3.2 在途上界(表 4 候选 1):饱和即丢弃本记录(尽力面——丢弃可观测),
|
|
239
|
+
// 首次饱和打一行 stderr(每饱和段一次;排空后复位)。
|
|
240
|
+
if (_pendingWrites >= TRACE_PENDING_MAX) {
|
|
241
|
+
_droppedRecords++
|
|
242
|
+
if (!_saturationNotified) {
|
|
243
|
+
_saturationNotified = true
|
|
244
|
+
console.error(`[trace] pending write queue full — ${_droppedRecords} record(s) dropped`)
|
|
245
|
+
}
|
|
246
|
+
return
|
|
247
|
+
}
|
|
145
248
|
// cwd/session 经 logCtx 增补(调用点传 agent.cwd / agent._sessionStart——
|
|
146
249
|
// D-TR3 的 cwdHash/命名与 D-TR1 的 session 字段所需);无 agent 作用域的调用点
|
|
147
250
|
// 回退 process.cwd()——CLI 会话即工作区。该类点开关状态:distill 已含
|
|
@@ -154,44 +257,72 @@ export function recordChatTrace(provider, opts = {}, result = null, error = null
|
|
|
154
257
|
const dateStr = localDateStr(now)
|
|
155
258
|
const seq = nextTraceSeq(dateStr)
|
|
156
259
|
const sessionKey = traceSessionKey(cwd)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
stage: logCtx.stage ?? null,
|
|
167
|
-
kind: logCtx.kind ?? null,
|
|
168
|
-
// D-TR1(fix round1):续写/重试链标记——true = 该调用是续写链的一环
|
|
169
|
-
// (core.mjs 续写递归传出);新调用缺省 false——round 字段已删(无来源)。
|
|
170
|
-
isContinuation: logCtx.isContinuation === true,
|
|
171
|
-
messages: redactValue("messages", opts.messages ?? []),
|
|
172
|
-
content: redactValue("content", result?.content ?? null),
|
|
173
|
-
reasoning: redactValue("reasoning", result?.reasoning ?? null),
|
|
174
|
-
toolCalls: redactValue("toolCalls", result?.toolCalls ?? null),
|
|
175
|
-
usage: result?.usage ?? null,
|
|
176
|
-
finishReason: result?.finishReason ?? null,
|
|
260
|
+
|
|
261
|
+
// 单遍序列化:头段字段 + messages(独立缓冲——记录级额度降级用)+ 尾段字段。
|
|
262
|
+
const parts = ["{"]
|
|
263
|
+
let first = true
|
|
264
|
+
const put = (k, v) => {
|
|
265
|
+
if (!first) parts.push(",")
|
|
266
|
+
first = false
|
|
267
|
+
parts.push(JSON.stringify(k), ":")
|
|
268
|
+
writeValue(parts, k, v)
|
|
177
269
|
}
|
|
270
|
+
put("ts", now.toISOString())
|
|
271
|
+
put("session", logCtx.session ?? null)
|
|
272
|
+
put("cwdHash", createHash("sha1").update(normalizeCwd(cwd)).digest("hex"))
|
|
273
|
+
put("role", logCtx.role ?? null)
|
|
274
|
+
put("depth", logCtx.depth ?? null)
|
|
275
|
+
put("turn", logCtx.turn ?? null)
|
|
276
|
+
put("provider", provider?.name ?? provider?.model ?? "unknown")
|
|
277
|
+
put("model", provider?.model ?? "")
|
|
278
|
+
put("stage", logCtx.stage ?? null)
|
|
279
|
+
put("kind", logCtx.kind ?? null)
|
|
280
|
+
// D-TR1(fix round1):续写/重试链标记——true = 该调用是续写链的一环
|
|
281
|
+
// (core.mjs 续写递归传出);新调用缺省 false——round 字段已删(无来源)。
|
|
282
|
+
put("isContinuation", logCtx.isContinuation === true)
|
|
283
|
+
const msgAt = parts.length
|
|
284
|
+
const msgParts = []
|
|
285
|
+
writeValue(msgParts, "messages", opts.messages ?? [])
|
|
286
|
+
put("content", result?.content ?? null)
|
|
287
|
+
put("reasoning", result?.reasoning ?? null)
|
|
288
|
+
put("toolCalls", result?.toolCalls ?? null)
|
|
289
|
+
put("usage", result?.usage ?? null)
|
|
290
|
+
put("finishReason", result?.finishReason ?? null)
|
|
178
291
|
if (error != null) {
|
|
179
292
|
// D-TR5:错误路径轨迹——error(errText 截断 + 类别)+ finishReason:null
|
|
180
|
-
|
|
181
|
-
err:
|
|
293
|
+
put("error", {
|
|
294
|
+
err: redactSecret("err", errText(error, 500)),
|
|
182
295
|
kind: classifyErr(error, opts.signal),
|
|
183
|
-
}
|
|
296
|
+
})
|
|
184
297
|
}
|
|
298
|
+
const headStr = parts.slice(0, msgAt).join("")
|
|
299
|
+
const tailStr = parts.slice(msgAt).join("")
|
|
300
|
+
const msgStr = msgParts.join("")
|
|
301
|
+
const msgsArr = Array.isArray(opts.messages) ? opts.messages : []
|
|
302
|
+
const record = (() => {
|
|
303
|
+
const total = headStr.length + msgStr.length + tailStr.length + 1
|
|
304
|
+
if (total > TRACE_RECORD_MAX_CHARS) {
|
|
305
|
+
// 单记录额度(§23.3.2):messages 整体降级 stub——元数据字段保留、标记含计数
|
|
306
|
+
const stub = `[trace record truncated for size: ${total} chars / ${msgsArr.length} messages]`
|
|
307
|
+
return `${headStr},${JSON.stringify("messages")}:${JSON.stringify(stub)}${tailStr}}`
|
|
308
|
+
}
|
|
309
|
+
return `${headStr},${JSON.stringify("messages")}:${msgStr}${tailStr}}`
|
|
310
|
+
})()
|
|
311
|
+
|
|
185
312
|
// 真 fire-and-forget(F-TR3——模型调用路径零额外阻塞):seq 已在上面同步预留
|
|
186
313
|
// (原子号位——写盘在途并发不撞号);写盘异步(不 await——chat() 出口立即返回)。
|
|
187
314
|
// 返回值 = 落盘 promise(测试 await 用;chat() 不消费——fire-and-forget 语义)。
|
|
315
|
+
_pendingWrites++
|
|
188
316
|
return (async () => {
|
|
189
317
|
try {
|
|
190
318
|
const dir = tracesDirFor(dateStr)
|
|
191
|
-
await mkdir(dir
|
|
192
|
-
await
|
|
319
|
+
await _traceHooks.mkdir(dir) // D-TR3:写前建目录(与 sessions/tool-results 同惯例)
|
|
320
|
+
await _traceHooks.append(join(dir, `${sessionKey}-${seq}.jsonl`), record + "\n")
|
|
193
321
|
} catch {
|
|
194
322
|
// F-TR3:落盘失败静默降级——不抛错、不阻塞 chat() 返回
|
|
323
|
+
} finally {
|
|
324
|
+
_pendingWrites--
|
|
325
|
+
if (_pendingWrites === 0) _saturationNotified = false // 排空 → 下个饱和段可再报一次
|
|
195
326
|
}
|
|
196
327
|
})()
|
|
197
328
|
}
|
package/src/tui/agent-turn.mjs
CHANGED
|
@@ -21,13 +21,25 @@ import { buildToolCallbacks, sweepToolBlocks } from "./tool-events.mjs"
|
|
|
21
21
|
import { freezeAllSubTasks } from "./subagent-blocks.mjs" // freezeReclaimDigestedBlocks 随 §17 段迁 suspension-drive.mjs
|
|
22
22
|
import { ensureSessionTitle } from "../generate-title.mjs"
|
|
23
23
|
import { logEvent, errText } from "../log.mjs"
|
|
24
|
-
import { suspensionSession, poolLive
|
|
24
|
+
import { suspensionSession, poolLive } from "./suspension-drive.mjs"
|
|
25
25
|
|
|
26
26
|
/** Exit-flush bound for the async end-of-run distillation (SEND-STALL-DISTILL §2.5):
|
|
27
27
|
* wait at most this long for the in-flight distill before the final session save —
|
|
28
28
|
* never let shutdown hang on the background summary call. */
|
|
29
29
|
const DISTILL_FLUSH_TIMEOUT_MS = 5000
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 第 33 批(TUI §14.3(d)——纯函数,供测试直驱):回合链尾「需要用户」谓词——agent 已停
|
|
33
|
+
* 且无自动续跑 ⇒ 置 attention 位。排除项语义(F13):`skipSession`(digest / 会话内回合
|
|
34
|
+
* ——由外层链尾统一置位)· 挂起两态 / 池 live / 队列非空(自动续跑中——不由用户接手)·
|
|
35
|
+
* processing(回合在跑)。
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
export function userNeededAtTurnEnd(state, agent, skipSession) {
|
|
39
|
+
return !skipSession && !state.suspended && !state._suspPending && !poolLive(agent)
|
|
40
|
+
&& state.queue.length === 0 && !state.processing
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
/**
|
|
32
44
|
* LOGGING(docs/design/LOGGING.md)包装:回合骨架事件(turn:start/turn:end——kind
|
|
33
45
|
* user/auto;result ok/stopped/error)。内层经 _logOutcome 载具回传终止原因(中止/
|
|
@@ -283,26 +295,25 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
283
295
|
render()
|
|
284
296
|
}
|
|
285
297
|
|
|
286
|
-
// §17 偏差 #1
|
|
287
|
-
|
|
288
|
-
//
|
|
298
|
+
// §17 偏差 #1 兜底(INPUT-LOCK 单槽化——2026-09-09):释放窗口期 Enter 已入
|
|
299
|
+
// pendingInput 单槽——链条走到此处若池已空(挂起会话不会启动,下方 while 是最后一个
|
|
300
|
+
// 消费点)则转正队列照常续发,消息不滞留不并发。池非空时挂起会话先消费 pendingInput
|
|
301
|
+
// (D-S5 输入优先),无需此处处理。单槽语义下残余至多一条。
|
|
289
302
|
if (!skipSession && !state._suspAborted && (state.pendingInput?.length ?? 0) > 0 && !poolLive(agent)) {
|
|
290
|
-
state.queue.push(
|
|
303
|
+
state.queue.push({ text: String(state.pendingInput.shift()) })
|
|
291
304
|
}
|
|
292
305
|
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
306
|
+
// 交接消息自动续发(INPUT-LOCK 单消息——2026-09-09):submit 不再排队(busy 提交吞)
|
|
307
|
+
// + R15 攒批删——state.queue 只剩残项单消息(释放窗口兜底/挂起中止残余——零丢失
|
|
308
|
+
// 承诺)——逐条直发;斜杠命令直接执行(保序);回合后的续发由递归层同循环续取。
|
|
296
309
|
while (state.queue.length > 0 && !state.processing) {
|
|
297
|
-
const head =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (head.kind === "slash") {
|
|
310
|
+
const head = state.queue.shift()
|
|
311
|
+
// 残项斜杠命令直接执行——保序、绝不合并
|
|
312
|
+
if (head.text.startsWith("/")) {
|
|
301
313
|
await handleSlash(head.text)
|
|
302
314
|
render()
|
|
303
315
|
continue
|
|
304
316
|
}
|
|
305
|
-
pushLabel(`❯ You: (from queue)`, ansi.bold + C.user)
|
|
306
317
|
await runAgentTurn(ctx, head.text)
|
|
307
318
|
return
|
|
308
319
|
}
|
|
@@ -321,4 +332,12 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
321
332
|
// 竞态复现)。
|
|
322
333
|
state._suspAborted = false
|
|
323
334
|
}
|
|
335
|
+
|
|
336
|
+
// 第 33 批(TUI §14.3(d)):顶层链尾(队列续发循环与挂起会话退出**之后**)——无人自动
|
|
337
|
+
// 接手 ⇒ 置 attention 位(渲染层实时派生 blocked/awaiting;用户任意输入清位)。D-AT6:
|
|
338
|
+
// 不放在回合末 finally——那之后还有队列续发 / 挂起会话(digest 自动消费),不是「需要用户」。
|
|
339
|
+
if (userNeededAtTurnEnd(state, agent, skipSession)) {
|
|
340
|
+
state.attentionAwaiting = true
|
|
341
|
+
render()
|
|
342
|
+
}
|
|
324
343
|
}
|
package/src/tui/ansi.mjs
CHANGED
|
@@ -31,6 +31,7 @@ export const ansi = {
|
|
|
31
31
|
dim: `${ESC}[2m`,
|
|
32
32
|
bold: `${ESC}[1m`,
|
|
33
33
|
fg: (n) => `${ESC}[${30 + n}m`,
|
|
34
|
+
bg: (n) => `${ESC}[${40 + n}m`,
|
|
34
35
|
gray: `${ESC}[90m`,
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -44,5 +45,6 @@ export const C = {
|
|
|
44
45
|
dim: ansi.gray,
|
|
45
46
|
warn: ansi.fg(3),
|
|
46
47
|
advisor: `${ESC}[92m`, // bright green — visible on dark backgrounds
|
|
48
|
+
attention: ansi.bg(3) + ansi.fg(0), // 第 33 批(TUI §14.3/D-AT3):attention 态整行色对——黄底黑字(\x1b[43m\x1b[30m,与 VSC warningBackground 同色系;浅深终端均可见——不依赖主题配色)
|
|
47
49
|
fold: ansi.bold + ansi.fg(6), // bold cyan — fold markers must stay visible on light AND dark themes (dim white vanished on light backgrounds)
|
|
48
50
|
}
|