thincoder 0.12.59 → 0.12.61
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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- 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 +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -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 +55 -0
- package/src/prompts/persona-engineering.md +54 -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 +18 -98
- package/src/provider/errors.mjs +101 -0
- 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/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- 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-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +40 -38
- package/src/tui/interaction.mjs +3 -3
- 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/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agent-tools/read-history.mjs — read_history tool (SESSION.md §9).
|
|
2
|
+
* agent-tools/read-history.mjs — read_history tool (SESSION.md §9 + §13 R19 cross-session).
|
|
3
3
|
*
|
|
4
|
-
* Query THIS session
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
4
|
+
* Query message history — THIS session by default, any session on disk with `path`
|
|
5
|
+
* (SESSION.md §13 R19): an explicit session file path deep-queries that file's
|
|
6
|
+
* history line; "cwd:<dir>" discovers the sessions stored for that directory.
|
|
7
|
+
*
|
|
8
|
+
* Default (no path) — THIS session's full human-readable record (agent._fullHistory
|
|
9
|
+
* — NEVER compacted, audit-complete). Use to recall what was said or done earlier:
|
|
10
|
+
* design decisions, tool-call timing, past rulings.
|
|
7
11
|
*
|
|
8
12
|
* Filters AND together: role / keyword (message text) / tool (tool messages by
|
|
9
13
|
* name + assistant messages that declared the call) / since-until (epoch ms
|
|
@@ -17,17 +21,44 @@
|
|
|
17
21
|
* session file. assistant tool_calls are summarized to a name list (arguments
|
|
18
22
|
* never expanded).
|
|
19
23
|
*
|
|
24
|
+
* Cross-session (SESSION.md §13 D-R19a): path = a session file path (absolute, or
|
|
25
|
+
* relative to the project cwd) → read that file's history line and apply the SAME
|
|
26
|
+
* filter surface; path = "cwd:<dir>" → list every slot stored for that directory
|
|
27
|
+
* (slot number + full file path + title/message count/updatedAt — no dead-slot
|
|
28
|
+
* filtering, v1 decision). Single-file retrieval is guarded by a line-scan cap
|
|
29
|
+
* (READ_HISTORY_SCAN_MAX — an oversized file is refused before it is read whole)
|
|
30
|
+
* plus a message-count cap (READ_HISTORY_MAX_MESSAGES = 50,000 — L24 双保险第二道).
|
|
31
|
+
*
|
|
20
32
|
* readonly: true — planMode pass / no permission ask. Registered depth-0 only:
|
|
21
33
|
* subagents get their own throwaway history, so querying "the session" from a
|
|
22
|
-
* child would be semantically confusing (SESSION.md §9.5 refinement 1).
|
|
23
|
-
*
|
|
34
|
+
* child would be semantically confusing (SESSION.md §9.5 refinement 1 + §13 T-R19.4).
|
|
35
|
+
* §13 R19 extension mirrored per SESSION.md §13 — double-end isomorphic, no
|
|
36
|
+
* cross-end byte test (thincoder-vscode/src/agent-tools/read-history.mjs).
|
|
24
37
|
*/
|
|
25
38
|
|
|
39
|
+
import { openSync, readSync, closeSync, readFileSync, existsSync, statSync } from "node:fs"
|
|
40
|
+
import { isAbsolute, resolve } from "node:path"
|
|
41
|
+
import { listSlots, slotPath } from "../session-slots.mjs"
|
|
42
|
+
|
|
26
43
|
const DEFAULT_LIMIT = 50
|
|
27
44
|
const MAX_LIMIT = 200
|
|
28
45
|
const CONTENT_CAP = 500
|
|
29
46
|
const VALID_ROLES = new Set(["user", "assistant", "tool"])
|
|
30
47
|
|
|
48
|
+
/** 单槽检索行扫护栏(SESSION.md §13 D-R19a——评审 #3 定稿:超限不再读全文,返回定稿错误文案)。 */
|
|
49
|
+
export const READ_HISTORY_SCAN_MAX = 200_000
|
|
50
|
+
|
|
51
|
+
/** L24 消息数预算(评审 #2 钉死——双保险第二道):行扫按物理 \n 行计——JSON 单行槽
|
|
52
|
+
* 行扫不设防——parse 后 history 数组长度超限即拒(同款定稿文案——双端同常量同文案)。 */
|
|
53
|
+
export const READ_HISTORY_MAX_MESSAGES = 50_000
|
|
54
|
+
|
|
55
|
+
/** 超限错误文案(SESSION.md §13——逐字定稿——T-R19.7 断言)。 */
|
|
56
|
+
const TOO_LARGE_ERROR = JSON.stringify({ error: "session too large — refine keyword or since/until" })
|
|
57
|
+
|
|
58
|
+
/** 检索/记忆族消歧总纲(SESSION.md §13 D-R19b——逐字定稿——read_history 描述尾段——T-R19.5 锚)。 */
|
|
59
|
+
const SEARCH_FAMILY_GUIDE =
|
|
60
|
+
"检索/记忆族选哪个:查**本会话**说过/裁定过 → read_history(默认);查**别的会话/项目**旧对话 → read_history 带 path/cwd 参数;查**本 run 改过哪些文件** → recent_changes;查**跨会话已存知识/约定**(memory)→ memory search;查**项目设计文档** → doc_search;查**代码实现** → code_search;查 git 历史快照 → checkpoint cat/versions。read_history 只查会话消息——文件级改动用 recent_changes——知识与约定用 memory——互相不替代。"
|
|
61
|
+
|
|
31
62
|
/** Message text for keyword matching + output: strings pass through; multimodal content arrays → text parts joined (never crashes, empty parts skipped, images ignored). */
|
|
32
63
|
function messageText(m) {
|
|
33
64
|
if (typeof m?.content === "string") return m.content
|
|
@@ -80,17 +111,129 @@ function toEntry(m) {
|
|
|
80
111
|
return entry
|
|
81
112
|
}
|
|
82
113
|
|
|
114
|
+
/** AND-filter one history message (role/keyword/tool/since-until) — shared by the in-memory
|
|
115
|
+
* default and the cross-session file query (SESSION.md §13 D-R19a: 同 filter 面应用). */
|
|
116
|
+
function matches(m, { role, kwRe, tool, since, until }) {
|
|
117
|
+
if (!m || typeof m !== "object") return false
|
|
118
|
+
if (role !== undefined && m.role !== role) return false
|
|
119
|
+
if (kwRe) {
|
|
120
|
+
const text = messageText(m)
|
|
121
|
+
if (!kwRe.test(text)) return false
|
|
122
|
+
}
|
|
123
|
+
if (tool) {
|
|
124
|
+
const byName = m.role === "tool" && m.name === tool
|
|
125
|
+
const byDeclaration = m.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.some((tc) => toolCallName(tc) === tool)
|
|
126
|
+
if (!byName && !byDeclaration) return false
|
|
127
|
+
}
|
|
128
|
+
const ts = m.ts
|
|
129
|
+
if (since !== null || until !== null) {
|
|
130
|
+
if (typeof ts !== "number") return false // no ts → no time-window match
|
|
131
|
+
if (since !== null && ts < since) return false
|
|
132
|
+
if (until !== null && ts > until) return false
|
|
133
|
+
}
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Direction picks the END of the matched set; output stays chronological either way. */
|
|
138
|
+
function formatMatches(matched, direction, limit) {
|
|
139
|
+
const windowed = direction === "oldest" ? matched.slice(0, limit) : matched.slice(-limit)
|
|
140
|
+
return JSON.stringify(windowed.map(toEntry))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Line-scan guard: stream-count physical newlines, bailing the moment the cap is crossed —
|
|
144
|
+
* an oversized file is refused BEFORE it is read whole ("不再读全文"——SESSION.md §13 D-R19a). */
|
|
145
|
+
function exceedsScanMax(file) {
|
|
146
|
+
const CHUNK = 64 * 1024
|
|
147
|
+
let fd = null
|
|
148
|
+
try {
|
|
149
|
+
fd = openSync(file, "r")
|
|
150
|
+
const buf = Buffer.alloc(CHUNK)
|
|
151
|
+
let newlines = 0
|
|
152
|
+
for (;;) {
|
|
153
|
+
const n = readSync(fd, buf, 0, CHUNK, null)
|
|
154
|
+
if (n <= 0) break
|
|
155
|
+
for (let i = 0; i < n; i++) {
|
|
156
|
+
if (buf[i] === 0x0a) newlines++
|
|
157
|
+
}
|
|
158
|
+
if (newlines > READ_HISTORY_SCAN_MAX) return true
|
|
159
|
+
}
|
|
160
|
+
return false
|
|
161
|
+
} catch {
|
|
162
|
+
return false // 行扫失败 → 交由后续读取/解析路径给出真实错误
|
|
163
|
+
} finally {
|
|
164
|
+
if (fd !== null) {
|
|
165
|
+
try { closeSync(fd) } catch { /* ignore */ }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Cross-session deep query: one session file, same filter surface (§13 D-R19a). */
|
|
171
|
+
function querySessionFile(pathArg, { role, kwRe, tool, since, until, direction, limit }, baseCwd) {
|
|
172
|
+
const file = isAbsolute(pathArg) ? pathArg : resolve(baseCwd ?? process.cwd(), pathArg)
|
|
173
|
+
if (!existsSync(file)) return `Error: session file not found: ${file}`
|
|
174
|
+
if (exceedsScanMax(file)) return TOO_LARGE_ERROR
|
|
175
|
+
let text
|
|
176
|
+
try {
|
|
177
|
+
text = readFileSync(file, "utf8")
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return `Error: failed to read session file ${file}: ${e.message}`
|
|
180
|
+
}
|
|
181
|
+
let data
|
|
182
|
+
try {
|
|
183
|
+
data = JSON.parse(text)
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return `Error: ${file} is not a valid session file (corrupt JSON: ${e.message})`
|
|
186
|
+
}
|
|
187
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.history)) {
|
|
188
|
+
return `Error: ${file} is not a valid session file (no history array)`
|
|
189
|
+
}
|
|
190
|
+
if (data.history.length > READ_HISTORY_MAX_MESSAGES) {
|
|
191
|
+
// L24 消息数第二道(parse 后——行扫按物理行、单行 JSON 槽行扫不设防——超限同款拒绝)。
|
|
192
|
+
return TOO_LARGE_ERROR
|
|
193
|
+
}
|
|
194
|
+
const matched = data.history.filter((m) => matches(m, { role, kwRe, tool, since, until }))
|
|
195
|
+
return formatMatches(matched, direction, limit)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Discovery surface (path = "cwd:<dir>"): list every slot stored for that directory, one line
|
|
199
|
+
* per slot — slot number + FULL session file path + title/message count/updatedAt(§13 D-R19a
|
|
200
|
+
* ——评审 #2:摘要必须含寻址字段——模型第二步深查 = 复制行内文件路径重调 path=)。 */
|
|
201
|
+
function discoverCwd(raw, baseCwd) {
|
|
202
|
+
const dir = resolve(baseCwd ?? process.cwd(), raw)
|
|
203
|
+
let st = null
|
|
204
|
+
try {
|
|
205
|
+
st = statSync(dir)
|
|
206
|
+
} catch { /* fallthrough to the explicit error below */ }
|
|
207
|
+
if (st === null || !st.isDirectory()) {
|
|
208
|
+
return `Error: unknown cwd "${raw}" — no session directory for this cwd (directory not found: ${dir})`
|
|
209
|
+
}
|
|
210
|
+
const slots = listSlots(dir) // 时间序(updatedAt 降序)——含 manifest 记录的全部槽(v1 不做死槽过滤)
|
|
211
|
+
if (slots.length === 0) {
|
|
212
|
+
return `(no session slots found for cwd: ${dir} — no sessions started there yet)`
|
|
213
|
+
}
|
|
214
|
+
const lines = slots.map((s) => {
|
|
215
|
+
const title = s.title ? `"${s.title}"` : "(untitled)"
|
|
216
|
+
return `slot ${s.slot}: ${slotPath(dir, s.slot)} — title: ${title}, messages: ${s.messageCount}, updatedAt: ${s.updatedAt}`
|
|
217
|
+
})
|
|
218
|
+
return `Session slots for cwd: ${dir} (newest first):\n${lines.join("\n")}`
|
|
219
|
+
}
|
|
220
|
+
|
|
83
221
|
export const readHistoryTool = {
|
|
84
222
|
name: "read_history",
|
|
85
223
|
description:
|
|
86
|
-
"Query THIS session
|
|
87
|
-
"
|
|
224
|
+
"Query message history — THIS session by default, any session on disk with `path`. " +
|
|
225
|
+
"Default (no path): THIS session's full record (never compacted, audit-complete) — recall what " +
|
|
226
|
+
"was said or done earlier: design decisions, tool-call timing, past rulings. " +
|
|
88
227
|
"Filters combine with AND: role / keyword (case-insensitive substring of message text) / " +
|
|
89
228
|
"tool (tool result messages by name AND the assistant messages that declared the call — pair with tool_call_id / ts for timing) / " +
|
|
90
229
|
"since-until (epoch ms time window; only messages with ts can match) / limit (default 50, clamped to 200) / direction (which end of the matches to take). " +
|
|
91
230
|
"Returns a JSON array in chronological order: [{ts, role, name?, tool_call_id?, content (≈500 chars, truncated marker), tool_calls (names only)}]. " +
|
|
92
231
|
"Messages without ts return ts:null. Content is truncated — the full text is in the session file. " +
|
|
93
|
-
"
|
|
232
|
+
"Cross-session (path, optional): a session file path deep-queries THAT session's history with the same filters " +
|
|
233
|
+
"(relative paths resolve against the project cwd); \"cwd:<dir>\" lists every session slot stored for that directory — " +
|
|
234
|
+
"one line per slot: slot number + full session file path + title + message count + updatedAt; copy a listed file path into path= to deep-query it. " +
|
|
235
|
+
"A session file over 50,000 messages or 200,000 lines is refused (\"session too large\") instead of being read whole.\n" +
|
|
236
|
+
SEARCH_FAMILY_GUIDE,
|
|
94
237
|
parameters: {
|
|
95
238
|
type: "object",
|
|
96
239
|
properties: {
|
|
@@ -101,11 +244,15 @@ export const readHistoryTool = {
|
|
|
101
244
|
until: { type: "integer", description: "Latest ts to match, epoch ms, INCLUSIVE. since > until yields an empty result." },
|
|
102
245
|
limit: { type: "integer", description: "Maximum messages to return (default 50; larger values are clamped to 200)." },
|
|
103
246
|
direction: { type: "string", enum: ["oldest", "newest"], description: "Take the limit window from the oldest or newest end of the matched set (default newest)." },
|
|
247
|
+
path: { type: "string", description: "Optional — query another session instead of this one: a session file path (as listed by a \"cwd:<dir>\" call) deep-queries that session; \"cwd:<dir>\" lists that directory's session slots (slot number + full file path + title + message count + updatedAt)." },
|
|
104
248
|
},
|
|
105
249
|
},
|
|
106
250
|
readonly: true,
|
|
107
251
|
execute(args, ctx) {
|
|
108
252
|
const a = args ?? {}
|
|
253
|
+
if (a.path !== undefined && (typeof a.path !== "string" || a.path.trim().length === 0)) {
|
|
254
|
+
return `Error: invalid path "${a.path}" — must be a session file path or "cwd:<dir>"`
|
|
255
|
+
}
|
|
109
256
|
const role = a.role
|
|
110
257
|
if (role !== undefined && (typeof role !== "string" || !VALID_ROLES.has(role))) {
|
|
111
258
|
return `Error: invalid role "${role}" — valid roles: user, assistant, tool`
|
|
@@ -131,30 +278,17 @@ export const readHistoryTool = {
|
|
|
131
278
|
// keyword stays a literal substring).
|
|
132
279
|
const kwRe = keyword ? new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : null
|
|
133
280
|
const tool = typeof a.tool === "string" && a.tool.length > 0 ? a.tool : null
|
|
281
|
+
const baseCwd = ctx.agent?.cwd ?? process.cwd()
|
|
282
|
+
|
|
283
|
+
const pathArg = typeof a.path === "string" && a.path.trim().length > 0 ? a.path.trim() : null
|
|
284
|
+
if (pathArg !== null) {
|
|
285
|
+
return pathArg.startsWith("cwd:")
|
|
286
|
+
? discoverCwd(pathArg.slice("cwd:".length), baseCwd)
|
|
287
|
+
: querySessionFile(pathArg, { role, kwRe, tool, since, until, direction, limit }, baseCwd)
|
|
288
|
+
}
|
|
134
289
|
|
|
135
290
|
const history = Array.isArray(ctx.agent?._fullHistory) ? ctx.agent._fullHistory : []
|
|
136
|
-
const matched = history.filter((m) => {
|
|
137
|
-
|
|
138
|
-
if (role !== undefined && m.role !== role) return false
|
|
139
|
-
if (kwRe) {
|
|
140
|
-
const text = messageText(m)
|
|
141
|
-
if (!kwRe.test(text)) return false
|
|
142
|
-
}
|
|
143
|
-
if (tool) {
|
|
144
|
-
const byName = m.role === "tool" && m.name === tool
|
|
145
|
-
const byDeclaration = m.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.some((tc) => toolCallName(tc) === tool)
|
|
146
|
-
if (!byName && !byDeclaration) return false
|
|
147
|
-
}
|
|
148
|
-
const ts = m.ts
|
|
149
|
-
if (since !== null || until !== null) {
|
|
150
|
-
if (typeof ts !== "number") return false // no ts → no time-window match
|
|
151
|
-
if (since !== null && ts < since) return false
|
|
152
|
-
if (until !== null && ts > until) return false
|
|
153
|
-
}
|
|
154
|
-
return true
|
|
155
|
-
})
|
|
156
|
-
// Direction picks the END of the matched set; output stays chronological either way.
|
|
157
|
-
const windowed = direction === "oldest" ? matched.slice(0, limit) : matched.slice(-limit)
|
|
158
|
-
return JSON.stringify(windowed.map(toEntry))
|
|
291
|
+
const matched = history.filter((m) => matches(m, { role, kwRe, tool, since, until }))
|
|
292
|
+
return formatMatches(matched, direction, limit)
|
|
159
293
|
},
|
|
160
294
|
}
|
|
@@ -8,7 +8,8 @@ export const recentChangesTool = {
|
|
|
8
8
|
description:
|
|
9
9
|
"Show files modified in this agent run (write/edit/insert_after/delete). " +
|
|
10
10
|
"Use when you need to remember which files you've already touched — during long multi-file tasks, " +
|
|
11
|
-
"it's easy to lose track. This is scoped to the current run, unlike git status which shows all uncommitted changes."
|
|
11
|
+
"it's easy to lose track. This is scoped to the current run, unlike git status which shows all uncommitted changes. " +
|
|
12
|
+
"For session-level history (what was said in a session), use read_history.",
|
|
12
13
|
parameters: {
|
|
13
14
|
type: "object",
|
|
14
15
|
properties: {},
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-streak.mjs — design-review failure streak guard(第 33 批——2026-09-11)。
|
|
3
|
+
*
|
|
4
|
+
* 同一 doc-set 的 design 评审**连续**未产出可用结算(宿主截断尾五 kind + 陈旧结算 +
|
|
5
|
+
* 凭证落盘失败 + 无报告结算)达 `MAX_DESIGN_REVIEW_STREAK` 次 ⇒ 后续发起被拒
|
|
6
|
+
* (工具层预检 + `runAdvisorReview` 内防线两级——零 LLM / 不耗轮次 / 不置「评审已覆盖」)。
|
|
7
|
+
* 设计权威 = `docs/design/ADVISOR-CONVERGENCE.md` §17(需求 = requirements 档 §12 F28/F29 + N20/N21)。
|
|
8
|
+
*
|
|
9
|
+
* 中立模块(**不 import 任何 src/ 模块**——打断潜在环,§17.5 模块图):
|
|
10
|
+
* - `normAbs` 自 advisor-settle.mjs 迁入(原处 re-export——既有 import 面零变);
|
|
11
|
+
* - `docSetKey` 自 advisor-async.mjs 迁入(原为私有——零 import 面)。
|
|
12
|
+
* 分类单源:`designReviewOutcome` 为异步结算 / 同步面两计数点共用(D-SK8——防两处分类漂移)。
|
|
13
|
+
* 载体 = 会话级内存 `agent._designReviewStreaks`(Map——不落盘、不进 session 文件;
|
|
14
|
+
* eng 模式切换不清护栏——与 `_advisorRuns` 刻意不同步,§17.9 #5 定案)。
|
|
15
|
+
*/
|
|
16
|
+
import { join } from "node:path"
|
|
17
|
+
|
|
18
|
+
/** 连续未产出可用结算的阈值(三振——§17.2 表 1 选定 N=3;`MAX_ADVISOR_ROUNDS` 零改动)。 */
|
|
19
|
+
export const MAX_DESIGN_REVIEW_STREAK = 3
|
|
20
|
+
|
|
21
|
+
/** ABS 归一(cwd 相对 → cwd 拼接)——陈旧判定 / 冻结拦截 / doc-set 键同源(§14.14 E-4)。 */
|
|
22
|
+
export function normAbs(p, cwd) {
|
|
23
|
+
const s = String(p)
|
|
24
|
+
return /^[a-zA-Z]:[\\/]/.test(s) || s.startsWith("/") || s.startsWith("\\\\") ? s : join(cwd, s)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Canonical scope key for design reviews — the document multi-set
|
|
28
|
+
* (order-insensitive, ABS-path normalized — launch 与 continuation 的写法差异
|
|
29
|
+
* ("./docs/x.md" vs "docs/x.md"、反斜杠) 不误建新实例).
|
|
30
|
+
* 第 33 批自 advisor-async.mjs 逐字迁入(护栏与实例续跑同锚单源)。 */
|
|
31
|
+
export function docSetKey(documents, cwd) {
|
|
32
|
+
const list = [...new Set((documents ?? [])
|
|
33
|
+
.filter((d) => typeof d === "string" && d.trim())
|
|
34
|
+
.map((d) => normAbs(d, cwd)))]
|
|
35
|
+
return JSON.stringify(list.sort())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 护栏适用的键判据:空清单(`[]` / `null` 的 `docSetKey` 产出 = "[]")不适用——不计数、
|
|
39
|
+
* 不停止(§17.9 #3 登记;fail-open 于此面)。 */
|
|
40
|
+
export function designReviewStreakApplies(key) {
|
|
41
|
+
return typeof key === "string" && key.length > 0 && key !== "[]"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 结算分类(纯函数单源——§17.3 优先级表,自上而下首个命中;输入 = 宿主可见事实,
|
|
46
|
+
* **零 LLM 输出解析**,N20)。neutral = 无信息事件(取消 / 中断 / 拒发)——不打断连续计数。
|
|
47
|
+
* @param {{launchRefused?: boolean, stale?: boolean, hasResult?: boolean,
|
|
48
|
+
* incomplete?: string|null, persistFailed?: boolean}} input
|
|
49
|
+
* @returns {{reset: boolean, count: string|null}} reset=true 计数复位;count=需加一的类名;
|
|
50
|
+
* 两者皆空 = neutral(不动计数)。
|
|
51
|
+
*/
|
|
52
|
+
export function designReviewOutcome(input) {
|
|
53
|
+
const { launchRefused = false, stale = false, hasResult = true, incomplete = null, persistFailed = false } = input ?? {}
|
|
54
|
+
if (launchRefused) return { reset: false, count: null } // 1. 未发起请求(无尝试发生——§14.4 既有语义)
|
|
55
|
+
if (stale) return { reset: false, count: "stale" } // 2. 陈旧结算(未产出可用凭证)
|
|
56
|
+
if (!hasResult) return { reset: false, count: "no_report" } // 3. 无报告(fail-closed)
|
|
57
|
+
if (incomplete && incomplete !== "interrupted") return { reset: false, count: String(incomplete) } // 4. 宿主截断尾五 kind
|
|
58
|
+
if (incomplete === "interrupted") return { reset: false, count: null } // 5. 用户 / 系统中断类(与 cancelled 同族)
|
|
59
|
+
if (persistFailed) return { reset: false, count: "no_credential" } // 6. pass 但槽落盘失败(无可用凭证)
|
|
60
|
+
return { reset: true, count: null } // 7. 可用判决(pass 且落盘成功 / changes-required)——连续链断点
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 记录只读视图(停止判定与结论表的数据源);空清单键 / 无载体 ⇒ null。 */
|
|
64
|
+
export function designReviewStreakRecord(agent, key) {
|
|
65
|
+
if (!designReviewStreakApplies(key)) return null
|
|
66
|
+
const streaks = agent?._designReviewStreaks
|
|
67
|
+
return streaks instanceof Map ? (streaks.get(key) ?? null) : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 计数落账(§17.4):reset ⇒ 删除该键记录;count ⇒ `{count: prev+1, log: […].slice(-N)}`;
|
|
71
|
+
* neutral ⇒ 不动。载体懒初始化。 */
|
|
72
|
+
export function noteDesignReviewOutcome(agent, key, outcome) {
|
|
73
|
+
if (!agent || !designReviewStreakApplies(key) || !outcome) return
|
|
74
|
+
if (outcome.reset) {
|
|
75
|
+
if (agent._designReviewStreaks instanceof Map) agent._designReviewStreaks.delete(key)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
if (typeof outcome.count !== "string" || !outcome.count) return
|
|
79
|
+
const streaks = agent._designReviewStreaks instanceof Map
|
|
80
|
+
? agent._designReviewStreaks
|
|
81
|
+
: (agent._designReviewStreaks = new Map())
|
|
82
|
+
const prev = streaks.get(key)
|
|
83
|
+
streaks.set(key, {
|
|
84
|
+
count: (prev?.count ?? 0) + 1,
|
|
85
|
+
log: [...(prev?.log ?? []), outcome.count].slice(-MAX_DESIGN_REVIEW_STREAK),
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 停止判定:记录达阈值即停;空清单键恒 false。停止不可自解除(复位仅经可用判决——被拒后
|
|
90
|
+
* 无法发生;会话结束随载体清零)。 */
|
|
91
|
+
export function designReviewStreakStopped(agent, key) {
|
|
92
|
+
return (designReviewStreakRecord(agent, key)?.count ?? 0) >= MAX_DESIGN_REVIEW_STREAK
|
|
93
|
+
}
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* 语义:set = 写盘(config.json——磁盘真相最小化:只写被设的键——默认值不固化)+
|
|
5
5
|
* 热应用(ctx.agent.config 内存对象立即更新——回合边界键下回合生效)。
|
|
6
6
|
* 护栏:敏感键(路径段含 apiKey/key/token/secret/password)回显/错误文本永不出现明文
|
|
7
|
-
* (••••(masked)——防密钥泄漏进会话历史/trace
|
|
8
|
-
* config.mjs DEFAULTS
|
|
7
|
+
* (••••(masked)——防密钥泄漏进会话历史/trace);已知键类型校验——非 null 叶子类型表
|
|
8
|
+
* 自动派生自 config.mjs DEFAULTS(不手写防漂移——F-S1.5/N-S1.5);null 默认值键与同族键
|
|
9
|
+
* 走显式形状表(_NULL_LEAF_SHAPES 相等面 / _SIBLING_SHAPES 存在性面——按各自真实消费形态
|
|
10
|
+
* 校验,完备性机械锁 + 一次性警告;SETTINGS-TOOL.md §8);set 侧效走审批门(dispatch 动作级分类)。
|
|
9
11
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import { join } from "node:path"
|
|
12
|
-
import { DEFAULTS, configPath } from "../config.mjs"
|
|
12
|
+
import { DEFAULTS, configPath, writeConfigAtomic } from "../config.mjs"
|
|
13
13
|
|
|
14
14
|
/** 敏感键段判定(完整点分路径的段级匹配——apiKey/api_key/api-key/token/secret/password 形态) */
|
|
15
15
|
const SENSITIVE_SEGMENT = /(^|[._-])(api[_-]?key|key|token|secret|password)($|[._-])/i
|
|
@@ -19,19 +19,128 @@ function isSensitiveKey(path) {
|
|
|
19
19
|
return SENSITIVE_SEGMENT.test(path)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/* ─── 形状层(第 8 批 2026-09-11——SETTINGS-TOOL.md §8.3;两表结构:相等面 / 存在性面) ─── */
|
|
23
|
+
|
|
22
24
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* null 叶子形状表——唯一「逐键手写形状」面(完备性锁的**相等面**:键集 == _nullLeafPaths(DEFAULTS))。
|
|
26
|
+
* 表值 = 形状规格:接受集 = 该键真实消费形态 ∪ `null`(显式清除——消费面均有「未设置」态);
|
|
27
|
+
* 不可消费形态(应用侧读不出写入值)→ 拒绝(违例抛错,磁盘/内存零变化)。
|
|
25
28
|
*/
|
|
26
|
-
|
|
29
|
+
const _NULL_LEAF_SHAPES = {
|
|
30
|
+
defaultModel: { kind: "nonEmptyString", providerModel: true, expects: 'non-empty string ("provider:model" composite)' },
|
|
31
|
+
"agent.subagentModel": { kind: "nonEmptyString", expects: "non-empty string" },
|
|
32
|
+
shell: { kind: "nonEmptyString", expects: "non-empty string" },
|
|
33
|
+
"memory.team": { kind: "team", expects: "object { repo: string, name?, dir? }" },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 同族/跨端形状表(**存在性断言面**——不参与完备性锁的集合相等)。W3 同族:`agent.subagentModels`
|
|
37
|
+
* 默认 `{}` ⇒ 派生表零条目 ⇒ 零约束(字符串被静默忽略——回落 subagentModel)。 */
|
|
38
|
+
const _SIBLING_SHAPES = {
|
|
39
|
+
"agent.subagentModels": { kind: "roleMap", expects: "object of role→non-empty string" },
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 派生:递归遍历默认值对象——叶子(非对象/数组值)记 `路径 → typeof 默认值`(旧语义逐字保留:
|
|
43
|
+
* null 叶子记 "object",其约束由形状表接管);数组不递归(下标元素无类型约束——全量域)。 */
|
|
44
|
+
function _deriveTypes(obj, prefix = "", out = {}) {
|
|
27
45
|
for (const [k, v] of Object.entries(obj)) {
|
|
28
46
|
const p = prefix ? `${prefix}.${k}` : k
|
|
29
|
-
if (v !== null && typeof v === "object" && !Array.isArray(v))
|
|
47
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) _deriveTypes(v, p, out)
|
|
30
48
|
else out[p] = Array.isArray(v) ? "array" : typeof v
|
|
31
49
|
}
|
|
32
50
|
return out
|
|
33
51
|
}
|
|
34
|
-
|
|
52
|
+
|
|
53
|
+
/** 类型表 = 派生 + 可选 `patch` 覆盖(测试缝——非 null 叶子逐键与派生结果相等;T-S2.15)。 */
|
|
54
|
+
function _buildShapeTable(obj, patch = null) {
|
|
55
|
+
const out = _deriveTypes(obj)
|
|
56
|
+
return patch ? Object.assign(out, patch) : out
|
|
57
|
+
}
|
|
58
|
+
const TYPE_MAP = _buildShapeTable(DEFAULTS)
|
|
59
|
+
|
|
60
|
+
/** null 叶子路径枚举(数组不递归——与派生表同域)。 */
|
|
61
|
+
function _nullLeafPaths(obj, prefix = "", out = []) {
|
|
62
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
63
|
+
const p = prefix ? `${prefix}.${k}` : k
|
|
64
|
+
if (v === null) out.push(p)
|
|
65
|
+
else if (typeof v === "object" && !Array.isArray(v)) _nullLeafPaths(v, p, out)
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 完备性锁(N-S1.5):未声明的 null 叶子 = 形状表漂移——返回键名 + 一次性警告列出键名。 */
|
|
71
|
+
let _shapeWarned = false
|
|
72
|
+
function _checkShapeCompleteness(defaults, warn = console.warn) {
|
|
73
|
+
const declared = new Set(Object.keys(_NULL_LEAF_SHAPES))
|
|
74
|
+
const missing = _nullLeafPaths(defaults).filter((p) => !declared.has(p))
|
|
75
|
+
if (missing.length && !_shapeWarned) {
|
|
76
|
+
_shapeWarned = true
|
|
77
|
+
warn(`[settings] null-default keys missing from _NULL_LEAF_SHAPES: ${missing.join(", ")} — declare their real consumption shape (SETTINGS-TOOL.md §8.3; N-S1.5 completeness lock)`)
|
|
78
|
+
}
|
|
79
|
+
return missing
|
|
80
|
+
}
|
|
81
|
+
_checkShapeCompleteness(DEFAULTS) // 装载自检——形状表必须覆盖全部 null 叶子(漂移即警告)
|
|
82
|
+
|
|
83
|
+
/** 值位渲染(错误文本——敏感键命中时遮罩:N-S1.2 / D-S2.4——明文零进错误句)。 */
|
|
84
|
+
function _shownValue(path, value) {
|
|
85
|
+
return isSensitiveKey(path) ? MASKED : JSON.stringify(value)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _kindOf(value) {
|
|
89
|
+
return value === null ? "null" : Array.isArray(value) ? "array" : typeof value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _expectsError(path, expects, value) {
|
|
93
|
+
return new Error(`settings set: "${path}" expects ${expects} — got ${_kindOf(value)} (${_shownValue(path, value)})`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 形状校验(形状表命中键——`null` 在 _checkKnownKeyValue 已放行)。 */
|
|
97
|
+
function _checkShape(path, spec, value) {
|
|
98
|
+
if (spec.kind === "team") {
|
|
99
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw _expectsError(path, spec.expects, value)
|
|
100
|
+
if (typeof value.repo !== "string" || !value.repo.trim()) {
|
|
101
|
+
throw new Error(`settings set: "${path}" requires a non-empty "repo" (team layer stays off without it) — got ${_shownValue(path, value)}`)
|
|
102
|
+
}
|
|
103
|
+
for (const k of ["name", "dir"]) {
|
|
104
|
+
if (value[k] !== undefined && (typeof value[k] !== "string" || !value[k].trim())) throw _expectsError(path, spec.expects, value)
|
|
105
|
+
}
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (spec.kind === "roleMap") {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw _expectsError(path, spec.expects, value)
|
|
110
|
+
for (const v of Object.values(value)) {
|
|
111
|
+
if (typeof v !== "string" || !v.trim()) throw _expectsError(path, spec.expects, value)
|
|
112
|
+
}
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
// nonEmptyString —— defaultModel 另加 provider:model 形态面(与 model-ref.mjs:25-36 同判据;
|
|
116
|
+
// 存在性面不查——D-S2.5)
|
|
117
|
+
if (typeof value !== "string" || !value.trim()) throw _expectsError(path, spec.expects, value)
|
|
118
|
+
if (spec.providerModel) {
|
|
119
|
+
const sep = value.indexOf(":")
|
|
120
|
+
if (sep <= 0 || !value.slice(sep + 1)) throw _expectsError(path, spec.expects, value)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 已知键值校验(set 写盘前;违例抛错——磁盘/内存零变化):
|
|
126
|
+
* ① 形状表命中键(`_NULL_LEAF_SHAPES` / `_SIBLING_SHAPES` **两表都查**——表值 = 形状规格)→ 形状校验
|
|
127
|
+
* (`null` = 显式清除,放行);② 非 null 叶子(派生表值 = `typeof` 串)→ 既有语义逐字保留
|
|
128
|
+
* (`want === "array"` 跳过 / `null` + `want === "object"` 放行);③ 未知键 → 原样通过(全量域)。
|
|
129
|
+
*/
|
|
130
|
+
function _checkKnownKeyValue(path, value) {
|
|
131
|
+
const spec = _NULL_LEAF_SHAPES[path] ?? _SIBLING_SHAPES[path]
|
|
132
|
+
if (spec) {
|
|
133
|
+
if (value === null) return // 显式清除 = 有效动作(消费面均有「未设置」态——D-S2.3)
|
|
134
|
+
return _checkShape(path, spec, value)
|
|
135
|
+
}
|
|
136
|
+
const want = TYPE_MAP[path]
|
|
137
|
+
if (want && want !== "array") {
|
|
138
|
+
const got = value === null ? "null" : typeof value
|
|
139
|
+
if (got !== want && !(value === null && want === "object")) throw _expectsError(path, want, value)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ─── 既有工具面 helpers(第 8 批零改动) ─── */
|
|
35
144
|
|
|
36
145
|
/** 点分路径解析(含数组数字段)——逐段下钻;返回 { ok, value } 或缺失段信息 */
|
|
37
146
|
function resolvePath(obj, path) {
|
|
@@ -82,7 +191,9 @@ function parseValue(raw) {
|
|
|
82
191
|
const v = JSON.parse(s)
|
|
83
192
|
// JSON 顶层标量都接受;解析失败(裸字符串)→ 字符串字面
|
|
84
193
|
if (typeof v === "number" || typeof v === "boolean" || v === null || Array.isArray(v) || typeof v === "object") return v
|
|
85
|
-
|
|
194
|
+
// "abc"(带引号传的字符串)——JSON.parse 成功但返回字符串:返回**解析值**(去引号)
|
|
195
|
+
// ——两端统一 ①(2026-09-11 用户裁定;SETTINGS-TOOL.md §9.3)。
|
|
196
|
+
return v
|
|
86
197
|
} catch {
|
|
87
198
|
return s // 裸 abc → 字符串字面
|
|
88
199
|
}
|
|
@@ -91,23 +202,13 @@ function parseValue(raw) {
|
|
|
91
202
|
export function settingsTool(opts = {}) {
|
|
92
203
|
const cfgPath = opts.configPath ?? configPath // 测试注入 tmp 文件;默认全局 configPath
|
|
93
204
|
|
|
94
|
-
async function readDisk() {
|
|
95
|
-
let text
|
|
96
|
-
try { text = readFileSync(cfgPath, "utf8") } catch { return {} } // 不存在 → 空对象(写盘最小化——默认不固化)
|
|
97
|
-
try { return JSON.parse(text) } catch { throw new Error(`settings: config file not parseable — refusing to overwrite: ${cfgPath}`) }
|
|
98
|
-
}
|
|
99
|
-
async function writeDisk(disk) {
|
|
100
|
-
mkdirSync(join(cfgPath, ".."), { recursive: true })
|
|
101
|
-
writeFileSync(cfgPath, JSON.stringify(disk, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
|
|
102
|
-
}
|
|
103
|
-
|
|
104
205
|
return {
|
|
105
206
|
name: "settings",
|
|
106
207
|
description:
|
|
107
208
|
"Adjust ThinCoder runtime configuration — persisted to config.json AND hot-applied to the live agent config.\n" +
|
|
108
209
|
"Actions: list (all keys + values, flattened) | get <key> | set <key> <value> — dot paths into config.json (agent.maxTurns, traces.enabled, providers.0.model, any nesting).\n" +
|
|
109
210
|
"set persists to disk (only the set key is written — defaults are never baked in) and takes effect in the running session immediately (turn-boundary keys apply next turn); the value survives restarts.\n" +
|
|
110
|
-
"Known keys are type-checked against the built-in defaults (agent.maxTurns
|
|
211
|
+
"Known keys are type-checked: scalar keys against the built-in defaults (agent.maxTurns a number, traces.enabled a boolean); keys whose default is null against their real consumption shape — defaultModel \"provider:model\", agent.subagentModel / shell non-empty string, memory.team object with a repo — so a value the app would silently drop is refused (null clears the key). Unknown keys under a known section are stored as given. Values parse as JSON first (true/false/numbers/objects/arrays), else stay strings.\n" +
|
|
111
212
|
"SENSITIVE keys (path segment contains apiKey/key/token/secret/password) are NEVER echoed in plaintext — list/get/set replies show ••••(masked); setting a sensitive key is allowed and stored, but never echoed back.\n" +
|
|
112
213
|
"list/get are read-only (planMode ok, no approval); set is a side effect (approval gate). The /config TUI command is the human equivalent.",
|
|
113
214
|
parameters: {
|
|
@@ -142,21 +243,23 @@ export function settingsTool(opts = {}) {
|
|
|
142
243
|
// set
|
|
143
244
|
if (!args.key || args.value === undefined) throw new Error("settings set: key and value are required")
|
|
144
245
|
const value = parseValue(args.value)
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// 写盘(磁盘真相最小化:只改被设键——默认不固化)+ 热应用(内存对象)
|
|
154
|
-
const disk = await readDisk()
|
|
155
|
-
setKeyPath(disk, args.key, value)
|
|
156
|
-
await writeDisk(disk)
|
|
246
|
+
// 已知键校验(形状表两表都查 + 派生类型表——SETTINGS-TOOL.md §8.3;未知键 JSON 原样)
|
|
247
|
+
_checkKnownKeyValue(String(args.key), value)
|
|
248
|
+
// 写盘(D-F5b:磁盘真相最小化——writeConfigAtomic 磁盘新鲜读 + 只改被设键 + mtime
|
|
249
|
+
// 门控——默认不固化;冲突/畸形抛错,内存不热应用(零虚假成功))+ 热应用(内存对象)
|
|
250
|
+
const r = await writeConfigAtomic(cfgPath, (disk) => {
|
|
251
|
+
setKeyPath(disk, args.key, value)
|
|
252
|
+
})
|
|
253
|
+
if (!r.ok) throw new Error("config changed on disk concurrently — retry (settings set not applied)")
|
|
157
254
|
setKeyPath(config, args.key, value)
|
|
158
255
|
const shown = isSensitiveKey(String(args.key)) ? MASKED : value
|
|
159
256
|
return `settings set: ${args.key} = ${shown} (${Array.isArray(value) ? "array" : typeof value})${isSensitiveKey(String(args.key)) ? " — stored(值不回显)" : " — persisted + hot-applied(运行中已生效)"}`
|
|
160
257
|
},
|
|
161
258
|
}
|
|
162
259
|
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 测试缝导出(`_` 前缀——config.mjs `_setConfigPathForTest` 先例;SETTINGS-TOOL.md §8.3 第 6 条):
|
|
263
|
+
* `_checkShapeCompleteness` = 完备性锁的运行时面(T-S2.14「一次性警告列出键名」的机械断言缝)。
|
|
264
|
+
*/
|
|
265
|
+
export { _buildShapeTable, _nullLeafPaths, _NULL_LEAF_SHAPES, _SIBLING_SHAPES, _checkKnownKeyValue, _checkShapeCompleteness }
|