thincoder 0.12.62 → 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.
Files changed (228) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +53 -27
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +25 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-reindex.mjs +1 -1
  31. package/src/tui/cmd-restore.mjs +2 -2
  32. package/src/tui/cmd-session.mjs +24 -1
  33. package/src/tui/cmd-skills.mjs +1 -1
  34. package/src/tui/cmd-think.mjs +22 -9
  35. package/src/tui/config-helpers.mjs +1 -1
  36. package/src/tui/display-budget.mjs +33 -11
  37. package/src/tui/index.mjs +9 -9
  38. package/src/tui/interaction.mjs +16 -7
  39. package/src/tui/key-modes.mjs +9 -4
  40. package/src/tui/ledger-surface.mjs +26 -10
  41. package/src/tui/model-catalog.mjs +4 -4
  42. package/src/tui/model-picker.mjs +8 -7
  43. package/src/tui/mouse.mjs +11 -6
  44. package/src/tui/pickers.mjs +15 -2
  45. package/src/tui/render-conversation.mjs +1 -1
  46. package/src/tui/render-frame.mjs +10 -5
  47. package/src/tui/render-loop.mjs +1 -1
  48. package/src/tui/render-segments.mjs +3 -1
  49. package/src/tui/slash-commands.mjs +1 -1
  50. package/src/tui/startup.mjs +14 -14
  51. package/src/tui/subagent-blocks.mjs +20 -3
  52. package/src/tui/subagent-freeze.mjs +73 -2
  53. package/src/tui/suspension-drive.mjs +46 -22
  54. package/src/tui/tool-events.mjs +11 -8
  55. package/src/tui/wizard.mjs +3 -3
  56. package/src/abort-provenance.mjs +0 -116
  57. package/src/advisor/citations.mjs +0 -139
  58. package/src/advisor/compaction.mjs +0 -174
  59. package/src/advisor/convergence.mjs +0 -80
  60. package/src/advisor/history.mjs +0 -77
  61. package/src/advisor/loop.mjs +0 -293
  62. package/src/advisor/messages.mjs +0 -299
  63. package/src/advisor/project-context.mjs +0 -194
  64. package/src/advisor/repos.mjs +0 -150
  65. package/src/advisor/run.mjs +0 -293
  66. package/src/advisor/truncate.mjs +0 -57
  67. package/src/advisor.mjs +0 -290
  68. package/src/agent/completion.mjs +0 -146
  69. package/src/agent/dispatch.mjs +0 -489
  70. package/src/agent/helpers.mjs +0 -384
  71. package/src/agent/post-turn.mjs +0 -70
  72. package/src/agent/record-results.mjs +0 -174
  73. package/src/agent/relay-prefix.mjs +0 -39
  74. package/src/agent/run-stages.mjs +0 -244
  75. package/src/agent/setup-reminders.mjs +0 -69
  76. package/src/agent/setup.mjs +0 -354
  77. package/src/agent/spawn-child.mjs +0 -243
  78. package/src/agent-tools/advisor-async.mjs +0 -346
  79. package/src/agent-tools/advisor-settle.mjs +0 -231
  80. package/src/agent-tools/advisor.mjs +0 -260
  81. package/src/agent-tools/async-settle.mjs +0 -204
  82. package/src/agent-tools/batch-segment.mjs +0 -195
  83. package/src/agent-tools/consult.mjs +0 -473
  84. package/src/agent-tools/design-token.mjs +0 -117
  85. package/src/agent-tools/digest-budget.mjs +0 -76
  86. package/src/agent-tools/eng.mjs +0 -67
  87. package/src/agent-tools/escalate-async.mjs +0 -295
  88. package/src/agent-tools/goal.mjs +0 -119
  89. package/src/agent-tools/plan.mjs +0 -81
  90. package/src/agent-tools/read-history.mjs +0 -309
  91. package/src/agent-tools/recent-changes.mjs +0 -24
  92. package/src/agent-tools/review-streak.mjs +0 -93
  93. package/src/agent-tools/settings.mjs +0 -265
  94. package/src/agent-tools/skill.mjs +0 -47
  95. package/src/agent-tools/subagent-actions.mjs +0 -482
  96. package/src/agent-tools/subagent-async.mjs +0 -434
  97. package/src/agent-tools/subagent-panel.mjs +0 -160
  98. package/src/agent-tools/subagent-run.mjs +0 -205
  99. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  100. package/src/agent-tools/subagent-spawn.mjs +0 -459
  101. package/src/agent-tools/subagent.mjs +0 -404
  102. package/src/agent-tools/task.mjs +0 -87
  103. package/src/agent-tools/timer.mjs +0 -46
  104. package/src/agent-tools/verify.mjs +0 -271
  105. package/src/agent-tools.mjs +0 -17
  106. package/src/agent.mjs +0 -417
  107. package/src/auto-think.mjs +0 -115
  108. package/src/config-migrate.mjs +0 -70
  109. package/src/config.mjs +0 -496
  110. package/src/context.mjs +0 -392
  111. package/src/conventions.mjs +0 -223
  112. package/src/embedding.mjs +0 -120
  113. package/src/escape.mjs +0 -152
  114. package/src/expand-home.mjs +0 -16
  115. package/src/explore-distill.mjs +0 -155
  116. package/src/generate-title.mjs +0 -88
  117. package/src/git/checkpoint.mjs +0 -448
  118. package/src/git/gitmem.mjs +0 -100
  119. package/src/hooks.mjs +0 -97
  120. package/src/ledger.mjs +0 -227
  121. package/src/log.mjs +0 -195
  122. package/src/markdown.mjs +0 -106
  123. package/src/mcp/helpers.mjs +0 -51
  124. package/src/mcp/transport-http.mjs +0 -248
  125. package/src/mcp/transport-stdio.mjs +0 -140
  126. package/src/mcp/transport-ws.mjs +0 -122
  127. package/src/mcp.mjs +0 -295
  128. package/src/memory/code-index.mjs +0 -219
  129. package/src/memory/code-sync.mjs +0 -415
  130. package/src/memory/core.mjs +0 -299
  131. package/src/memory/delete.mjs +0 -236
  132. package/src/memory/docs.mjs +0 -419
  133. package/src/memory/file-walk.mjs +0 -109
  134. package/src/memory/scan.mjs +0 -95
  135. package/src/memory/schema.mjs +0 -452
  136. package/src/memory.mjs +0 -21
  137. package/src/model-ref.mjs +0 -66
  138. package/src/model-specs.mjs +0 -179
  139. package/src/peer-domains.mjs +0 -265
  140. package/src/peer-instances.mjs +0 -231
  141. package/src/prompt-overlays.mjs +0 -82
  142. package/src/prompts/advisor-design.md +0 -41
  143. package/src/prompts/advisor-round1.md +0 -41
  144. package/src/prompts/advisor-round2.md +0 -46
  145. package/src/prompts/advisor-round3.md +0 -42
  146. package/src/prompts/common.md +0 -115
  147. package/src/prompts/consult-base.md +0 -19
  148. package/src/prompts/discipline-engineering.md +0 -258
  149. package/src/prompts/discipline-normal.md +0 -185
  150. package/src/prompts/persona-coder.md +0 -21
  151. package/src/prompts/persona-eng-coder.md +0 -37
  152. package/src/prompts/persona-eng-designer.md +0 -60
  153. package/src/prompts/persona-engineering.md +0 -55
  154. package/src/prompts/persona-explore.md +0 -15
  155. package/src/prompts/persona-normal.md +0 -27
  156. package/src/prompts/persona-plan.md +0 -26
  157. package/src/provider/anthropic.mjs +0 -225
  158. package/src/provider/core.mjs +0 -476
  159. package/src/provider/errors.mjs +0 -101
  160. package/src/provider/google.mjs +0 -257
  161. package/src/provider/index.mjs +0 -7
  162. package/src/provider/list-models.mjs +0 -93
  163. package/src/provider/normalize.mjs +0 -81
  164. package/src/provider/rate.mjs +0 -108
  165. package/src/provider/responses.mjs +0 -495
  166. package/src/provider/retry.mjs +0 -88
  167. package/src/provider/sse.mjs +0 -264
  168. package/src/proxy.mjs +0 -261
  169. package/src/rules.mjs +0 -53
  170. package/src/session-gc.mjs +0 -221
  171. package/src/session-guard.mjs +0 -59
  172. package/src/session-migrate.mjs +0 -48
  173. package/src/session-rename.mjs +0 -38
  174. package/src/session-segments.mjs +0 -100
  175. package/src/session-slots.mjs +0 -492
  176. package/src/session-store.mjs +0 -441
  177. package/src/session.mjs +0 -492
  178. package/src/skills.mjs +0 -153
  179. package/src/text-budget.mjs +0 -46
  180. package/src/token-ttl.mjs +0 -274
  181. package/src/tools/apply_patch.md +0 -15
  182. package/src/tools/bash.md +0 -37
  183. package/src/tools/bash.mjs +0 -268
  184. package/src/tools/checklist-sync.mjs +0 -181
  185. package/src/tools/checklist.md +0 -13
  186. package/src/tools/checklist.mjs +0 -299
  187. package/src/tools/delete.md +0 -13
  188. package/src/tools/edit-batch.mjs +0 -191
  189. package/src/tools/edit-diff.mjs +0 -348
  190. package/src/tools/edit.md +0 -30
  191. package/src/tools/execute.md +0 -21
  192. package/src/tools/execute.mjs +0 -228
  193. package/src/tools/fetch.md +0 -12
  194. package/src/tools/file.mjs +0 -469
  195. package/src/tools/file_ops.md +0 -17
  196. package/src/tools/get_current_time.md +0 -8
  197. package/src/tools/git-checkpoint.mjs +0 -143
  198. package/src/tools/git-ext.mjs +0 -173
  199. package/src/tools/git.md +0 -54
  200. package/src/tools/git.mjs +0 -356
  201. package/src/tools/glob-dialect.mjs +0 -130
  202. package/src/tools/glob.md +0 -11
  203. package/src/tools/grep.md +0 -19
  204. package/src/tools/hashline_edit.md +0 -14
  205. package/src/tools/index.mjs +0 -36
  206. package/src/tools/insert_after.md +0 -15
  207. package/src/tools/lint.md +0 -10
  208. package/src/tools/linter.mjs +0 -128
  209. package/src/tools/ls.md +0 -12
  210. package/src/tools/lsp.md +0 -10
  211. package/src/tools/lsp.mjs +0 -316
  212. package/src/tools/ops.mjs +0 -299
  213. package/src/tools/patch.mjs +0 -282
  214. package/src/tools/process.md +0 -10
  215. package/src/tools/question.md +0 -16
  216. package/src/tools/question.mjs +0 -26
  217. package/src/tools/read.md +0 -20
  218. package/src/tools/read_image.md +0 -8
  219. package/src/tools/repomap.mjs +0 -314
  220. package/src/tools/search.mjs +0 -236
  221. package/src/tools/shared.mjs +0 -446
  222. package/src/tools/tree.md +0 -14
  223. package/src/tools/tree.mjs +0 -66
  224. package/src/tools/wait_for.md +0 -22
  225. package/src/tools/web.mjs +0 -224
  226. package/src/tools/websearch.md +0 -16
  227. package/src/tools/write.md +0 -11
  228. package/src/traces/trace-store.mjs +0 -355
package/src/tools/web.mjs DELETED
@@ -1,224 +0,0 @@
1
- import { DESC, truncate, stripTags, htmlToText, isPrivateHost } from "./shared.mjs";
2
- import { URL } from "node:url";
3
- import { proxyFetch } from "../proxy.mjs";
4
-
5
- export const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
6
- const FETCH_TIMEOUT = 15_000
7
- // §14 D-TF3:网络失败错误文本追加 proxy 提示——纯文本提示,不自动路由(2026-08-31 裁定:proxy 显式传不自动应用)
8
- const PROXY_HINT = "network failure — retry with proxy: 'http://host:port' if the target is blocked"
9
-
10
- // ── Web search (Bing; direct by default, through proxy when configured and web toggle on) ──
11
-
12
- function extractBing(html) {
13
- const results = []
14
- // RSS format: <item><title>..</title><link>..</link><description>..</description></item>
15
- for (const m of html.matchAll(/<item>([\s\S]*?)<\/item>/gi)) {
16
- const block = m[1]
17
- const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1] ?? ""
18
- const href = block.match(/<link>([\s\S]*?)<\/link>/)?.[1] ?? ""
19
- const snippet = block.match(/<description>([\s\S]*?)<\/description>/)?.[1] ?? ""
20
- if (!href) continue
21
- results.push({ href, title: stripTags(title), snippet: stripTags(snippet) })
22
- }
23
- // Fallback: HTML b_algo blocks (older server-rendered pages)
24
- if (results.length === 0) {
25
- const blocks = html.split('<li class="b_algo"').slice(1)
26
- for (const block of blocks) {
27
- const link = block.match(/<h2[^>]*><a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/)
28
- if (!link) continue
29
- const snippet = block.match(/<p[^>]*>([\s\S]*?)<\/p>/)
30
- results.push({ href: link[1], title: stripTags(link[2]), snippet: snippet ? stripTags(snippet[1]) : "" })
31
- }
32
- }
33
- return results
34
- }
35
-
36
- function bingUrl(query, page) {
37
- let u = `https://www.bing.com/search?q=${encodeURIComponent(query)}&format=rss&setlang=en`
38
- if (page > 1) u += `&first=${(page - 1) * 10 + 1}`
39
- return u
40
- }
41
-
42
- const ENGINES = [{ name: "bing", label: "Bing", url: bingUrl, extract: extractBing, ua: UA }]
43
- const ENGINE_NAMES = ENGINES.map(e => e.name)
44
-
45
- /** Structured search via Tavily (optional — config.websearch.apiKey). Returns
46
- * { engine, results } or null to fall back to Bing HTML scraping.
47
- * proxyUri: explicit per-call proxy (args.proxy) — never the config.json one. */
48
- async function fetchTavily(query, limit, ctx, proxyUri) {
49
- const apiKey = ctx?.agent?.config?.websearch?.apiKey
50
- if (!apiKey) return null
51
- const ctrl = new AbortController()
52
- const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT)
53
- try {
54
- const response = await proxyFetch("https://api.tavily.com/search", {
55
- method: "POST",
56
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
57
- body: JSON.stringify({ query, search_depth: "basic", max_results: limit, include_answer: false, include_raw_content: false }),
58
- signal: ctrl.signal,
59
- }, proxyUri)
60
- if (!response.ok) return null
61
- const data = await response.json()
62
- const results = (Array.isArray(data.results) ? data.results : []).map((r) => ({
63
- href: r.url, title: r.title ?? "", snippet: r.content ?? "", _engine: "tavily",
64
- }))
65
- return { engine: "tavily", results }
66
- } catch { return null }
67
- finally { clearTimeout(timer) }
68
- }
69
-
70
- async function fetchEngine(engine, query, page, proxyUri) {
71
- const ctrl = new AbortController()
72
- const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT)
73
- try {
74
- const response = await proxyFetch(engine.url(query, page), {
75
- headers: { "User-Agent": engine.ua, "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8" },
76
- signal: ctrl.signal,
77
- }, proxyUri)
78
- if (!response.ok) return null
79
- const html = await response.text()
80
- const results = engine.extract(html)
81
- return { engine: engine.name, results }
82
- } catch { return null }
83
- finally { clearTimeout(timer) }
84
- }
85
-
86
- export const websearchTool = {
87
- name: "websearch",
88
- description: DESC("websearch"),
89
- parameters: {
90
- type: "object",
91
- properties: {
92
- query: { type: "string", description: "Search query" },
93
- limit: { type: "number", description: "Max results (default 8, max 20)" },
94
- engine: { type: "string", enum: ENGINE_NAMES, description: "Specific engine — \"bing\" (Bing). Omit to search all engines concurrently." },
95
- page: { type: "number", description: "Page number for pagination (1-based, default 1). Only used when engine is specified." },
96
- proxy: { type: "string", description: "http://host:port explicit proxy (optional) — use ONLY when passed; no proxy = direct. config.json proxy is NOT auto-applied (2026-08-31 ruling: fixed config breaks domestic sites)" },
97
- },
98
- required: ["query"],
99
- },
100
- readonly: true,
101
- async execute(args, ctx) {
102
- const limit = Math.min(args.limit ?? 8, 20)
103
- const page = Math.max(1, args.page ?? 1)
104
- // 2026-08-31 ruling: proxy is a PER-CALL decision (args.proxy), never the config.json
105
- // fixed configuration. Model picks by target: github/foreign → pass proxy; gitee/domestic → omit.
106
- const proxyUri = args.proxy ?? null
107
- // Structured search first when a Tavily key is configured — stable, dated,
108
- // no HTML scraping. Falls back to Bing silently.
109
- const tavily = await fetchTavily(args.query, limit, ctx, proxyUri)
110
- if (tavily && tavily.results.length > 0) {
111
- return truncate(tavily.results.slice(0, limit).map((r, i) => `${i + 1}. [tavily] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
112
- }
113
- if (args.engine) {
114
- const engine = ENGINES.find(e => e.name === args.engine)
115
- if (!engine) return `Unknown engine '${args.engine}'. Available: ${ENGINE_NAMES.join(", ")}`
116
- const fetched = await fetchEngine(engine, args.query, page, proxyUri)
117
- if (!fetched || fetched.results.length === 0) return "(no results)"
118
- return truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. [${engine.label}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
119
- }
120
- const promises = ENGINES.map(e => fetchEngine(e, args.query, 1, proxyUri))
121
- const fetched = (await Promise.all(promises)).filter(Boolean)
122
- if (fetched.length === 0) return "(no results)"
123
- const merged = [], indexes = fetched.map(() => 0)
124
- let done = false
125
- while (!done && merged.length < limit) {
126
- done = true
127
- for (let i = 0; i < fetched.length; i++) {
128
- if (indexes[i] < fetched[i].results.length) {
129
- merged.push({ ...fetched[i].results[indexes[i]], _engine: fetched[i].engine })
130
- indexes[i]++; done = false
131
- if (merged.length >= limit) break
132
- }
133
- }
134
- }
135
- return truncate(merged.slice(0, limit).map((r, i) => `${i + 1}. [${r._engine}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
136
- },
137
- }
138
-
139
- // ── Fetch tool (with proxy support) ──────
140
-
141
- function isPrivateUrl(urlStr) {
142
- let u; try { u = new URL(urlStr) } catch { return true }
143
- return isPrivateHost(u.hostname)
144
- }
145
-
146
- // proxyFetch returns a native Response (Headers object, needs .get()) without proxy,
147
- // but a Response-like with a plain lowercase-keyed Record through the CONNECT tunnel — handle both.
148
- function headerOf(res, name) {
149
- const h = res.headers
150
- if (!h) return null
151
- if (typeof h.get === "function") return h.get(name)
152
- return h[name.toLowerCase()] ?? null
153
- }
154
-
155
- /** Validate a redirect target: resolve relative → absolute, http/https only, and
156
- * SSRF-checked. Returns { target } on success or { error } (never follows into a
157
- * private host — the redirect SSRF bypass). */
158
- export function resolveRedirectTarget(loc, baseUrl) {
159
- let target
160
- try { target = new URL(loc, baseUrl).toString() } catch { return { error: "invalid redirect location" } }
161
- if (!/^https?:\/\//.test(target)) return { error: "redirect target must be http/https" }
162
- if (isPrivateUrl(target)) return { error: "redirect target is internal/private/metadata" }
163
- return { target }
164
- }
165
-
166
- /** Heuristic: a fetch result that is a near-empty shell or a region-block page hides
167
- * the real content. Flags the pattern so the model switches strategy instead of
168
- * re-fetching blind (2026-08-31: 21-char MiniMax SPA shell + "App unavailable in
169
- * region" Claude page both wasted an hour of round trips). */
170
- export function detectSparseHtml(html, text) {
171
- if (text.length >= 300) return ""
172
- if (/unavailable in (your )?region|not available in (your )?region|app-unavailable|enable.?javascript|just a moment|attention required|cf-browser-verification/i.test(html)) {
173
- return "\n\n[fetch hint: page looks region-blocked or JS-gated (body <300 chars). Try a search MCP tool (configured provider) or the site's .md/raw/mirror endpoint]"
174
- }
175
- if (/<div[^>]+id="(app|root)"[^>]*>\s*<\/div>|id="app"|id="root"/i.test(html)) {
176
- return "\n\n[fetch hint: page is a JS-rendered SPA shell (body <300 chars) — content loads client-side. Try a search MCP tool or the site's .md/API endpoint]"
177
- }
178
- return ""
179
- }
180
-
181
- export const fetchTool = {
182
- name: "fetch",
183
- description: DESC("fetch"),
184
- parameters: {
185
- type: "object",
186
- properties: {
187
- url: { type: "string", description: "http/https URL" },
188
- proxy: { type: "string", description: "http://host:port explicit proxy (optional) — use ONLY when passed; no proxy = direct. config.json proxy is NOT auto-applied (2026-08-31 ruling); pick per target (github/foreign sites need a proxy, gitee/domestic don't)" },
189
- },
190
- required: ["url"],
191
- },
192
- readonly: true,
193
- async execute(args, _ctx) {
194
- if (!/^https?:\/\//.test(args.url)) throw new Error("url must start with http:// or https://")
195
- if (isPrivateUrl(args.url)) throw new Error("fetch blocked: internal/private/metadata addresses are not allowed")
196
- try {
197
- // 2026-08-31 ruling: per-call decision — args.proxy when passed, direct otherwise.
198
- const proxyUri = args.proxy ?? null
199
- const response = await proxyFetch(args.url, { headers: { "User-Agent": UA } }, proxyUri)
200
- if (!response.ok) {
201
- if ([301, 302, 307, 308].includes(response.status)) {
202
- const loc = headerOf(response, "location")
203
- if (loc) {
204
- // SSRF-check the redirect target (resolve relative → absolute) before
205
- // following — a 3xx must not bounce a public URL into a private host.
206
- const r = resolveRedirectTarget(loc, args.url)
207
- if (r.error) throw new Error(`fetch failed: ${r.error}`)
208
- const r2 = await proxyFetch(r.target, { headers: { "User-Agent": UA } }, proxyUri)
209
- if (!r2.ok) throw new Error(`fetch failed: HTTP ${r2.status}\n${PROXY_HINT}`)
210
- const ct2 = headerOf(r2, "content-type") ?? ""
211
- const b2 = await r2.text()
212
- if (ct2.includes("text/html")) { const t = htmlToText(b2); return truncate(t + detectSparseHtml(b2, t)) }
213
- return truncate(b2)
214
- }
215
- }
216
- throw new Error(`fetch failed: HTTP ${response.status}\n${PROXY_HINT}`)
217
- }
218
- const ct = headerOf(response, "content-type") ?? ""
219
- const body = await response.text()
220
- if (ct.includes("text/html")) { const t = htmlToText(body); return truncate(t + detectSparseHtml(body, t)) }
221
- return truncate(body)
222
- } catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}\n${PROXY_HINT}`, { cause: e }) }
223
- },
224
- }
@@ -1,16 +0,0 @@
1
- Search the web via Bing. Returns result titles, URLs, and snippets. Use for looking up current information, docs, error messages.
2
-
3
- Parameters:
4
- - query (required): Search query
5
- - limit: Max results (default 8, max 20)
6
- - engine: Specific engine to use — "bing" (Bing). Omit to search all engines concurrently.
7
- - page: Page number for pagination (1-based, default 1). Only used when engine is specified.
8
- - proxy: http://host:port explicit proxy (optional) — use ONLY when passed; no proxy = direct. config.json proxy is NOT auto-applied (2026-08-31 ruling); Bing/foreign sites usually need a proxy, domestic targets don't
9
-
10
- Notes:
11
- - Before searching the web, call the memory tool (action: search) first — you may already know the answer from a previous session. Only reach for websearch if memory comes up empty.
12
- - Runs synchronously — returns when the search completes (all selected engines run in parallel).
13
- - Use this for information that is NOT in the local codebase — current docs, error messages, API references
14
- - Follow up with `fetch` to read full pages from the results
15
- - **Weak engine warning**: Bing's index is noisy for technical queries — if a first websearch returns irrelevant/townhall-grade results, DO NOT retry the same query. Configure a search MCP tool (e.g. `glm-websearch` via the MCP config) for technical lookups; websearch is the fallback.
16
- - Proxy: NOT auto-applied from config.json (2026-08-31 ruling). Pass `proxy: "http://host:port"` explicitly when the target needs one; omit for domestic targets.
@@ -1,11 +0,0 @@
1
- Write content to a file. Creates parent directories; overwrites existing file.
2
-
3
- Parameters:
4
- - path (required): File path, relative to cwd or absolute
5
- - content (required): Full content to write
6
-
7
- Notes:
8
- - write replaces the WHOLE file — read it first and confirm you intend to rewrite it entirely; for a small change use edit / insert_after.
9
- - Returns `Wrote N chars to <path>` + git diff + syntax-check note.
10
- - This overwrites the entire file — use `edit` for targeted changes
11
- - The file is atomic: it either writes completely or fails
@@ -1,355 +0,0 @@
1
- /**
2
- * trace-store.mjs — §18.6 完整轨迹存档(AGENT-LOOP.md §18.6 D-TR1..TR8 权威规格)。
3
- *
4
- * 完整请求-响应轨迹落盘:每次 chat() 调用一个 JSONL 文件到
5
- * ~/.thincoder/traces/YYYY-MM-DD/<sessionKey>-<seq>.jsonl。采集点唯一 = core.mjs
6
- * chat() 导出出口(所有模型调用——主回合/消化轮/compress/distill/advisor/子代理/
7
- * auto-think/consult——都经该函数)——续写/重试在出口已合并(reasoning 全量)。
8
- *
9
- * 纪律(与 log.mjs 同源惯例):
10
- * - 真 fire-and-forget(F-TR3——模型调用路径零额外阻塞):seq 同步预留(原子号位),
11
- * 写盘异步(node:fs/promises——不 await);落盘失败静默降级(不抛错、不阻塞
12
- * chat() 返回)。recordChatTrace 返回落盘 promise——仅供测试/显式消费方 await。
13
- * - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
14
- * 轨迹目录(既有测试跑真实 agent 管线会产生数百次 chat 调用);显式设置
15
- * THINCODER_TRACES_DIR 强制写入该目录(traces.test.mjs 用它隔离临时目录——
16
- * 与 log.mjs 的 THINCODER_LOG_DIR 同惯例)。
17
- * - 脱敏(D-TR2):复用 log.mjs 字段名黑名单(apikey/designtoken/password/secret/
18
- * token/authorization/proxyuri/proxy)+ SECRET_FORM 形态扫描(redactSecret)——对
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)。
28
- * - seq = 当日目录内最大已有 seq + 1(D-TR3——跨会话/进程重启不覆写既有旧轨迹——
29
- * 18.6.1 评审 #3);**进程内 seqCache(§23.3.2——消除逐调用同步扫目录)**:首次
30
- * readdirSync 后进程内递增预留,目录被清理(retention)后取下界重扫一次(防御)。
31
- * 多进程语义(修正轮 #10——登记):同 cwd 多实例各自进程内缓存——seq 可撞、同
32
- * sessionKey 记录并入同名文件(append 不覆写);**可容忍**(本机 traces = 诊断面、
33
- * 默认 OFF、撞号不损坏数据);不做跨进程协调/落盘校验。
34
- * - 日期分日按本地时区(D-TR3——2026-09-04 fix round1:初版 toISOString()=UTC——
35
- * 本地 00:00-08:00 的调用会落进前一日目录——改本地日期字符串 YYYY-MM-DD——T-TR12)。
36
- * - isContinuation(D-TR1——2026-09-04 fix round1):续写/重试链标记——true = 该调用
37
- * 是续写链的一环(core.mjs 续写递归传出 logCtx.isContinuation);新调用 false。
38
- * 记录不输出 round 字段(全设计无 round 定义——删——不发明无来源字段)。
39
- */
40
- import { readdirSync, existsSync } from "node:fs"
41
- import { appendFile, mkdir, readdir, stat, unlink, rmdir } from "node:fs/promises"
42
- import { join } from "node:path"
43
- import { createHash } from "node:crypto"
44
- import { configDir } from "../config.mjs"
45
- import { redactSecret, errText, classifyErr } from "../log.mjs"
46
- import { normalizeCwd } from "../session-slots.mjs"
47
- import { capText } from "../text-budget.mjs"
48
-
49
- /** 轨迹根目录:THINCODER_TRACES_DIR(测试隔离/override——同 THINCODER_LOG_DIR
50
- * 惯例)> ~/.thincoder/traces(configDir——D-TR3——与 sessions/ 同域)。 */
51
- export function tracesRoot() {
52
- return process.env.THINCODER_TRACES_DIR ?? join(configDir, "traces")
53
- }
54
-
55
- /** 写门(测试隔离):test runner 进程(NODE_TEST_CONTEXT)默认跳过——除显式
56
- * THINCODER_TRACES_DIR override(traces.test.mjs 隔离临时目录)。 */
57
- function writeEnabled() {
58
- if (process.env.NODE_TEST_CONTEXT && !process.env.THINCODER_TRACES_DIR) return false
59
- return true
60
- }
61
-
62
- /** sessionKey = sha1(normalizeCwd(cwd))[:12](D-TR3——与 session-slots sessionPath
63
- * 同算法同 cwd 归一——两端 hash 一致)。 */
64
- export function traceSessionKey(cwd) {
65
- return createHash("sha1").update(normalizeCwd(cwd)).digest("hex").slice(0, 12)
66
- }
67
-
68
- /** 本地时区日期字符串 YYYY-MM-DD(D-TR3——2026-09-04 fix round1:初版
69
- * toISOString()=UTC 日期——本地 00:00-08:00 落前一日目录;本地日期才是用户视角的
70
- * "今天"——T-TR12)。recordChatTrace 与测试读取 helpers 共用同一实现——不双写。 */
71
- export function localDateStr(date = new Date()) {
72
- const y = date.getFullYear()
73
- const m = String(date.getMonth() + 1).padStart(2, "0")
74
- const d = String(date.getDate()).padStart(2, "0")
75
- return `${y}-${m}-${d}`
76
- }
77
-
78
- /** Test hooks(_rateHooks 同惯例——rate.mjs):now/readdir/exists/mkdir/append 注入缝
79
- * (T-TR5 计数 readdirSync;T-TR4 慢写替身;T-TR12 注入本地日 ≠ UTC 日的时刻)。
80
- * 生产永远走真实实现。 */
81
- export const _traceHooks = {
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"),
87
- }
88
-
89
- /** 当日轨迹目录(D-TR3:traces/YYYY-MM-DD——分日组织——N-TR1)。 */
90
- export function tracesDirFor(dateStr) {
91
- return join(tracesRoot(), dateStr)
92
- }
93
-
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
- }
145
-
146
- /** seq = max(当日目录最大已有 seq, 进程内已预留) + 1(D-TR3——跨会话/进程重启
147
- * 不覆写——T-TR8/T-TR10;同步预留 = 异步写盘启动前的原子号位分配)。
148
- * §23.3.2:seqCache 进程内递增——不逐调用 readdirSync(目录被清理/缺失 → 重扫一次)。 */
149
- export function nextTraceSeq(dateStr) {
150
- const dir = tracesDirFor(dateStr)
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)
156
- }
157
- const seq = max + 1
158
- _seqCache.set(dir, seq)
159
- return seq
160
- }
161
-
162
- /** 开关(D-TR6):traces.enabled 缺省 on(默认全采集);logCtx.traces === false
163
- * 显式关闭(调用点读取 agent.config.traces.enabled——D-TR6)。 */
164
- export function tracesEnabled(logCtx) {
165
- return logCtx?.traces !== false
166
- }
167
-
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
217
- }
218
- return false // OMIT
219
- }
220
-
221
- /**
222
- * 收集一次 chat 调用轨迹(D-TR1 字段集)——fire-and-forget(F-TR3)。
223
- * 由 core.mjs chat() 导出出口调用(唯一采集点——N-TR2);签名零参数膨胀——
224
- * 数据全部来自调用方已传入的 opts(logCtx 元数据——D-TR4)+ provider + result/error。
225
- *
226
- * @param {Object} provider chat() 的 provider 参数(provider/model 字段来源)
227
- * @param {Object} opts chat() 原始 opts —— messages(输入)与 logCtx(元数据:
228
- * role/depth/kind/session/cwd/stage/turn/traces——调用点增补,D-TR4)
229
- * @param {Object|null} result chatImpl 返回值(成功路径——content/reasoning/
230
- * toolCalls/usage/finishReason;N-TR3:出口汇总——续写/重试后全量)
231
- * @param {Error|null} error chat() 抛出的错误(失败路径——D-TR5:error(errText
232
- * 截断 + 类别)+ finishReason:null——失败轨迹恰是分析纠结点最有效的材料)
233
- */
234
- export function recordChatTrace(provider, opts = {}, result = null, error = null) {
235
- if (!writeEnabled()) return
236
- const logCtx = opts.logCtx ?? {}
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
- }
248
- // cwd/session 经 logCtx 增补(调用点传 agent.cwd / agent._sessionStart——
249
- // D-TR3 的 cwdHash/命名与 D-TR1 的 session 字段所需);无 agent 作用域的调用点
250
- // 回退 process.cwd()——CLI 会话即工作区。该类点开关状态:distill 已含
251
- // traces 开关(D-TR6 fix round1);auto-think 已闭环(D-TS12——chat 调用点
252
- // logCtx 全字段补传:traces/session/cwd/role/depth/kind——无残留点)。
253
- const cwd = logCtx.cwd ?? process.cwd()
254
- // D-TR3(fix round1):分日按本地时区(_traceHooks.now——T-TR12 注入点);
255
- // ts 与 dateStr 同源(同一时刻)——记录时间戳与目录日不撕裂。
256
- const now = _traceHooks.now()
257
- const dateStr = localDateStr(now)
258
- const seq = nextTraceSeq(dateStr)
259
- const sessionKey = traceSessionKey(cwd)
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)
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)
291
- if (error != null) {
292
- // D-TR5:错误路径轨迹——error(errText 截断 + 类别)+ finishReason:null
293
- put("error", {
294
- err: redactSecret("err", errText(error, 500)),
295
- kind: classifyErr(error, opts.signal),
296
- })
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
-
312
- // 真 fire-and-forget(F-TR3——模型调用路径零额外阻塞):seq 已在上面同步预留
313
- // (原子号位——写盘在途并发不撞号);写盘异步(不 await——chat() 出口立即返回)。
314
- // 返回值 = 落盘 promise(测试 await 用;chat() 不消费——fire-and-forget 语义)。
315
- _pendingWrites++
316
- return (async () => {
317
- try {
318
- const dir = tracesDirFor(dateStr)
319
- await _traceHooks.mkdir(dir) // D-TR3:写前建目录(与 sessions/tool-results 同惯例)
320
- await _traceHooks.append(join(dir, `${sessionKey}-${seq}.jsonl`), record + "\n")
321
- } catch {
322
- // F-TR3:落盘失败静默降级——不抛错、不阻塞 chat() 返回
323
- } finally {
324
- _pendingWrites--
325
- if (_pendingWrites === 0) _saturationNotified = false // 排空 → 下个饱和段可再报一次
326
- }
327
- })()
328
- }
329
-
330
- /**
331
- * D-TR10(2026-09-05 用户裁定——发布隐私 + 磁盘卫生):启动清理——删除 traces 根下
332
- * mtime 超过保留期的轨迹文件(保留期 = config.traces.retentionHours,默认 24h);
333
- * 删空的日期目录(YYYY-MM-DD)。目录里非 .jsonl 文件不碰。CLI 启动点 fire-and-forget
334
- * 调用(不 await——不阻塞启动——失败静默——与轨迹写盘同纪律)。返回删除文件数。
335
- */
336
- export async function cleanupTraces({ dir = tracesRoot(), retentionHours = 24 } = {}) {
337
- const cutoff = Date.now() - retentionHours * 3_600_000
338
- let days
339
- try { days = await readdir(dir) } catch { return 0 } // 目录不存在/不可读 → 无事可做
340
- let removed = 0
341
- for (const day of days) {
342
- const dayDir = join(dir, day)
343
- try { if (!(await stat(dayDir)).isDirectory()) continue } catch { continue }
344
- let names
345
- try { names = await readdir(dayDir) } catch { continue }
346
- for (const n of names) {
347
- if (!n.endsWith(".jsonl")) continue
348
- try {
349
- if ((await stat(join(dayDir, n))).mtimeMs < cutoff) { await unlink(join(dayDir, n)); removed++ }
350
- } catch { /* 单个文件失败不影响其余 */ }
351
- }
352
- try { if ((await readdir(dayDir)).length === 0) await rmdir(dayDir) } catch {}
353
- }
354
- return removed
355
- }