thincoder 0.12.62 → 0.12.64

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 (231) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +46 -49
  3. package/bin/thincoder.mjs +68 -34
  4. package/package.json +7 -6
  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 +257 -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 +60 -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 +35 -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-plan.mjs +9 -0
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +24 -1
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +33 -11
  38. package/src/tui/index.mjs +18 -10
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-modes.mjs +9 -4
  41. package/src/tui/ledger-surface.mjs +22 -59
  42. package/src/tui/model-catalog.mjs +4 -4
  43. package/src/tui/model-picker.mjs +8 -7
  44. package/src/tui/mouse.mjs +11 -6
  45. package/src/tui/pickers.mjs +15 -2
  46. package/src/tui/render-conversation.mjs +1 -1
  47. package/src/tui/render-frame.mjs +15 -6
  48. package/src/tui/render-loop.mjs +1 -1
  49. package/src/tui/render-segments.mjs +3 -1
  50. package/src/tui/slash-commands.mjs +1 -1
  51. package/src/tui/startup.mjs +14 -14
  52. package/src/tui/subagent-blocks.mjs +20 -3
  53. package/src/tui/subagent-freeze.mjs +73 -2
  54. package/src/tui/suspension-drive.mjs +57 -23
  55. package/src/tui/tool-events.mjs +11 -8
  56. package/src/tui/tui-lifecycle.mjs +9 -0
  57. package/src/tui/wizard.mjs +3 -3
  58. package/src/tui/wrapped-spawn.mjs +6 -2
  59. package/src/abort-provenance.mjs +0 -116
  60. package/src/advisor/citations.mjs +0 -139
  61. package/src/advisor/compaction.mjs +0 -174
  62. package/src/advisor/convergence.mjs +0 -80
  63. package/src/advisor/history.mjs +0 -77
  64. package/src/advisor/loop.mjs +0 -293
  65. package/src/advisor/messages.mjs +0 -299
  66. package/src/advisor/project-context.mjs +0 -194
  67. package/src/advisor/repos.mjs +0 -150
  68. package/src/advisor/run.mjs +0 -293
  69. package/src/advisor/truncate.mjs +0 -57
  70. package/src/advisor.mjs +0 -290
  71. package/src/agent/completion.mjs +0 -146
  72. package/src/agent/dispatch.mjs +0 -489
  73. package/src/agent/helpers.mjs +0 -384
  74. package/src/agent/post-turn.mjs +0 -70
  75. package/src/agent/record-results.mjs +0 -174
  76. package/src/agent/relay-prefix.mjs +0 -39
  77. package/src/agent/run-stages.mjs +0 -244
  78. package/src/agent/setup-reminders.mjs +0 -69
  79. package/src/agent/setup.mjs +0 -354
  80. package/src/agent/spawn-child.mjs +0 -243
  81. package/src/agent-tools/advisor-async.mjs +0 -346
  82. package/src/agent-tools/advisor-settle.mjs +0 -231
  83. package/src/agent-tools/advisor.mjs +0 -260
  84. package/src/agent-tools/async-settle.mjs +0 -204
  85. package/src/agent-tools/batch-segment.mjs +0 -195
  86. package/src/agent-tools/consult.mjs +0 -473
  87. package/src/agent-tools/design-token.mjs +0 -117
  88. package/src/agent-tools/digest-budget.mjs +0 -76
  89. package/src/agent-tools/eng.mjs +0 -67
  90. package/src/agent-tools/escalate-async.mjs +0 -295
  91. package/src/agent-tools/goal.mjs +0 -119
  92. package/src/agent-tools/plan.mjs +0 -81
  93. package/src/agent-tools/read-history.mjs +0 -309
  94. package/src/agent-tools/recent-changes.mjs +0 -24
  95. package/src/agent-tools/review-streak.mjs +0 -93
  96. package/src/agent-tools/settings.mjs +0 -265
  97. package/src/agent-tools/skill.mjs +0 -47
  98. package/src/agent-tools/subagent-actions.mjs +0 -482
  99. package/src/agent-tools/subagent-async.mjs +0 -434
  100. package/src/agent-tools/subagent-panel.mjs +0 -160
  101. package/src/agent-tools/subagent-run.mjs +0 -205
  102. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  103. package/src/agent-tools/subagent-spawn.mjs +0 -459
  104. package/src/agent-tools/subagent.mjs +0 -404
  105. package/src/agent-tools/task.mjs +0 -87
  106. package/src/agent-tools/timer.mjs +0 -46
  107. package/src/agent-tools/verify.mjs +0 -271
  108. package/src/agent-tools.mjs +0 -17
  109. package/src/agent.mjs +0 -417
  110. package/src/auto-think.mjs +0 -115
  111. package/src/config-migrate.mjs +0 -70
  112. package/src/config.mjs +0 -496
  113. package/src/context.mjs +0 -392
  114. package/src/conventions.mjs +0 -223
  115. package/src/embedding.mjs +0 -120
  116. package/src/escape.mjs +0 -152
  117. package/src/expand-home.mjs +0 -16
  118. package/src/explore-distill.mjs +0 -155
  119. package/src/generate-title.mjs +0 -88
  120. package/src/git/checkpoint.mjs +0 -448
  121. package/src/git/gitmem.mjs +0 -100
  122. package/src/hooks.mjs +0 -97
  123. package/src/ledger.mjs +0 -227
  124. package/src/log.mjs +0 -195
  125. package/src/markdown.mjs +0 -106
  126. package/src/mcp/helpers.mjs +0 -51
  127. package/src/mcp/transport-http.mjs +0 -248
  128. package/src/mcp/transport-stdio.mjs +0 -140
  129. package/src/mcp/transport-ws.mjs +0 -122
  130. package/src/mcp.mjs +0 -295
  131. package/src/memory/code-index.mjs +0 -219
  132. package/src/memory/code-sync.mjs +0 -415
  133. package/src/memory/core.mjs +0 -299
  134. package/src/memory/delete.mjs +0 -236
  135. package/src/memory/docs.mjs +0 -419
  136. package/src/memory/file-walk.mjs +0 -109
  137. package/src/memory/scan.mjs +0 -95
  138. package/src/memory/schema.mjs +0 -452
  139. package/src/memory.mjs +0 -21
  140. package/src/model-ref.mjs +0 -66
  141. package/src/model-specs.mjs +0 -179
  142. package/src/peer-domains.mjs +0 -265
  143. package/src/peer-instances.mjs +0 -231
  144. package/src/prompt-overlays.mjs +0 -82
  145. package/src/prompts/advisor-design.md +0 -41
  146. package/src/prompts/advisor-round1.md +0 -41
  147. package/src/prompts/advisor-round2.md +0 -46
  148. package/src/prompts/advisor-round3.md +0 -42
  149. package/src/prompts/common.md +0 -115
  150. package/src/prompts/consult-base.md +0 -19
  151. package/src/prompts/discipline-engineering.md +0 -258
  152. package/src/prompts/discipline-normal.md +0 -185
  153. package/src/prompts/persona-coder.md +0 -21
  154. package/src/prompts/persona-eng-coder.md +0 -37
  155. package/src/prompts/persona-eng-designer.md +0 -60
  156. package/src/prompts/persona-engineering.md +0 -55
  157. package/src/prompts/persona-explore.md +0 -15
  158. package/src/prompts/persona-normal.md +0 -27
  159. package/src/prompts/persona-plan.md +0 -26
  160. package/src/provider/anthropic.mjs +0 -225
  161. package/src/provider/core.mjs +0 -476
  162. package/src/provider/errors.mjs +0 -101
  163. package/src/provider/google.mjs +0 -257
  164. package/src/provider/index.mjs +0 -7
  165. package/src/provider/list-models.mjs +0 -93
  166. package/src/provider/normalize.mjs +0 -81
  167. package/src/provider/rate.mjs +0 -108
  168. package/src/provider/responses.mjs +0 -495
  169. package/src/provider/retry.mjs +0 -88
  170. package/src/provider/sse.mjs +0 -264
  171. package/src/proxy.mjs +0 -261
  172. package/src/rules.mjs +0 -53
  173. package/src/session-gc.mjs +0 -221
  174. package/src/session-guard.mjs +0 -59
  175. package/src/session-migrate.mjs +0 -48
  176. package/src/session-rename.mjs +0 -38
  177. package/src/session-segments.mjs +0 -100
  178. package/src/session-slots.mjs +0 -492
  179. package/src/session-store.mjs +0 -441
  180. package/src/session.mjs +0 -492
  181. package/src/skills.mjs +0 -153
  182. package/src/text-budget.mjs +0 -46
  183. package/src/token-ttl.mjs +0 -274
  184. package/src/tools/apply_patch.md +0 -15
  185. package/src/tools/bash.md +0 -37
  186. package/src/tools/bash.mjs +0 -268
  187. package/src/tools/checklist-sync.mjs +0 -181
  188. package/src/tools/checklist.md +0 -13
  189. package/src/tools/checklist.mjs +0 -299
  190. package/src/tools/delete.md +0 -13
  191. package/src/tools/edit-batch.mjs +0 -191
  192. package/src/tools/edit-diff.mjs +0 -348
  193. package/src/tools/edit.md +0 -30
  194. package/src/tools/execute.md +0 -21
  195. package/src/tools/execute.mjs +0 -228
  196. package/src/tools/fetch.md +0 -12
  197. package/src/tools/file.mjs +0 -469
  198. package/src/tools/file_ops.md +0 -17
  199. package/src/tools/get_current_time.md +0 -8
  200. package/src/tools/git-checkpoint.mjs +0 -143
  201. package/src/tools/git-ext.mjs +0 -173
  202. package/src/tools/git.md +0 -54
  203. package/src/tools/git.mjs +0 -356
  204. package/src/tools/glob-dialect.mjs +0 -130
  205. package/src/tools/glob.md +0 -11
  206. package/src/tools/grep.md +0 -19
  207. package/src/tools/hashline_edit.md +0 -14
  208. package/src/tools/index.mjs +0 -36
  209. package/src/tools/insert_after.md +0 -15
  210. package/src/tools/lint.md +0 -10
  211. package/src/tools/linter.mjs +0 -128
  212. package/src/tools/ls.md +0 -12
  213. package/src/tools/lsp.md +0 -10
  214. package/src/tools/lsp.mjs +0 -316
  215. package/src/tools/ops.mjs +0 -299
  216. package/src/tools/patch.mjs +0 -282
  217. package/src/tools/process.md +0 -10
  218. package/src/tools/question.md +0 -16
  219. package/src/tools/question.mjs +0 -26
  220. package/src/tools/read.md +0 -20
  221. package/src/tools/read_image.md +0 -8
  222. package/src/tools/repomap.mjs +0 -314
  223. package/src/tools/search.mjs +0 -236
  224. package/src/tools/shared.mjs +0 -446
  225. package/src/tools/tree.md +0 -14
  226. package/src/tools/tree.mjs +0 -66
  227. package/src/tools/wait_for.md +0 -22
  228. package/src/tools/web.mjs +0 -224
  229. package/src/tools/websearch.md +0 -16
  230. package/src/tools/write.md +0 -11
  231. package/src/traces/trace-store.mjs +0 -355
package/src/embedding.mjs DELETED
@@ -1,120 +0,0 @@
1
- /**
2
- * embedding.mjs — vector embeddings
3
- * OpenAI-compatible /v1/embeddings (SiliconFlow bge-m3 / Ollama / OpenAI all supported),
4
- * reuses provider.mjs fetch + retry pattern, zero dependencies.
5
- * Vectors are normalized before storage; dot product then equals cosine similarity.
6
- */
7
-
8
- import { RETRYABLE_STATUS } from "./provider/index.mjs"
9
- const MAX_RETRIES = 3
10
- const BATCH_SIZE = 32 // max texts per request (within SiliconFlow limits)
11
-
12
- /** Create an embedder. config: { baseURL, apiKey, model } */
13
- export function createEmbedder(config) {
14
- if (!config?.baseURL) throw new Error("embedding config: baseURL is required — configure embedding.baseURL in ~/.thincoder/config.json")
15
- if (!config?.apiKey) throw new Error("embedding config: apiKey is required — configure embedding.apiKey in ~/.thincoder/config.json")
16
- if (!config?.model) throw new Error("embedding config: model is required — configure embedding.model in ~/.thincoder/config.json")
17
- return {
18
- baseURL: config.baseURL.replace(/\/+$/, ""),
19
- apiKey: config.apiKey,
20
- model: config.model,
21
- }
22
- }
23
-
24
- /**
25
- * Batch embedding. texts: string[] → Float32Array[] (normalized)
26
- * Auto-batches, retries on failure (exponential backoff).
27
- */
28
- export async function embed(embedder, texts, { signal } = {}) {
29
- if (texts.length === 0) return []
30
- const vectors = []
31
- for (let i = 0; i < texts.length; i += BATCH_SIZE) {
32
- const batch = texts.slice(i, i + BATCH_SIZE)
33
- const data = await requestWithRetry(embedder, batch, signal)
34
- // Mismatched count is a hard error — silently accepting would misalign vectors with texts, poisoning the entire index
35
- if (!Array.isArray(data.data) || data.data.length !== batch.length) {
36
- throw new Error(`Embedding API returned ${data.data?.length ?? 0} vectors for ${batch.length} inputs`)
37
- }
38
- // Spec says data[] order matches input, but sort by index field if present — don't bet on server implementation
39
- const items = data.data.every((d) => typeof d.index === "number")
40
- ? [...data.data].sort((a, b) => a.index - b.index)
41
- : data.data
42
- for (const item of items) {
43
- vectors.push(normalize(Float32Array.from(item.embedding)))
44
- }
45
- }
46
- return vectors
47
- }
48
-
49
- /** Cosine similarity (inputs are normalized, dot product equals cosine) */
50
- export function cosine(a, b) {
51
- if (a.length !== b.length) return 0
52
- let sum = 0
53
- const n = a.length
54
- for (let i = 0; i < n; i++) sum += a[i] * b[i]
55
- return sum
56
- }
57
-
58
- /** Float32Array → Buffer suitable for sqlite BLOB storage */
59
- export function toBlob(vec) {
60
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength)
61
- }
62
-
63
- /** sqlite BLOB → Float32Array */
64
- export function fromBlob(buf) {
65
- // BLOB may come from Buffer pool where byteOffset isn't 4-aligned; creating a view directly would RangeError — copy to align first
66
- if (buf.byteOffset % 4 !== 0) buf = new Uint8Array(buf)
67
- if (buf.byteLength % 4 !== 0) return new Float32Array(0)
68
- return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4)
69
- }
70
-
71
- // ---------------------------------------------------------------- internal
72
-
73
- async function requestWithRetry(embedder, input, signal) {
74
- let lastError
75
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
76
- if (attempt > 0) await sleep(2 ** (attempt - 1) * 1000)
77
-
78
- let response
79
- try {
80
- response = await fetch(`${embedder.baseURL}/embeddings`, {
81
- method: "POST",
82
- headers: {
83
- "Content-Type": "application/json",
84
- Authorization: `Bearer ${embedder.apiKey}`,
85
- },
86
- body: JSON.stringify({ model: embedder.model, input }),
87
- signal: signal
88
- ? AbortSignal.any([signal, AbortSignal.timeout(60_000)])
89
- : AbortSignal.timeout(60_000),
90
- })
91
- } catch (error) {
92
- if (error.name === "AbortError") throw error
93
- lastError = error
94
- continue
95
- }
96
-
97
- if (response.ok) return response.json()
98
-
99
- const text = await response.text().catch(() => "")
100
- const message = `Embedding API error ${response.status}: ${text}`
101
- if (RETRYABLE_STATUS.has(response.status)) {
102
- lastError = new Error(message)
103
- continue
104
- }
105
- throw new Error(message)
106
- }
107
- throw lastError
108
- }
109
-
110
- function normalize(vec) {
111
- let sum = 0
112
- for (let i = 0; i < vec.length; i++) sum += vec[i] * vec[i]
113
- const norm = Math.sqrt(sum) || 1
114
- for (let i = 0; i < vec.length; i++) vec[i] /= norm
115
- return vec
116
- }
117
-
118
- function sleep(ms) {
119
- return new Promise((resolve) => setTimeout(resolve, ms))
120
- }
package/src/escape.mjs DELETED
@@ -1,152 +0,0 @@
1
- /**
2
- * escape.mjs — 中和 OpenAI 兼容服务端在 message content 内做的非标二次转义解析 + 孤立代理净化。
3
- *
4
- * 两个独立毒源(均真机实证):
5
- *
6
- * ① 字面 hex 转义二次解析(2026-08-06 Kimi 首观察):Kimi/deepseek 等网关会把 content 里的字面
7
- * "\x5Cx" / "\x5Cu" 当作 hex escape 再解释一遍,不足位时 400("unexpected end of hex escape")。
8
- * 对策:不足位序列前 double 反斜杠(\\x5CxNN 形态还原为字面量);合法完整序列放行。
9
- * Known limitation(2026-09-01 v3 修复):反斜杠 run ≥3 时(如 "\\\x5Cu" 三反斜杠+u)v1 的
10
- * lookbehind 只看前 1 字符会整体放行,但二次解析按配对消费后尾部的 \x5Cu 仍裸露 → 炸。
11
- * 修复:按 run 奇偶判断——run 为奇数时尾部的 \x5Cu/\x5Cx 裸露(需 double),偶数已配对(放行)。
12
- *
13
- * ② 孤立 UTF-16 代理对(2026-09-02 deepseek 真机实锤):content 里的**真实孤立代理字符**
14
- * (高代理 U+D800-DBFF 无低代理跟随,或低代理 U+DC00-DFFF 无高代理前置)——JSON.stringify
15
- * 输出 \ud83d(合法 JSON),但 deepseek 解析器严格 UTF-16 解码,孤立代理 → 400
16
- * ("unexpected end of hex escape" / "lone leading surrogate in hex escape")。
17
- * 来源实证:doc_search 结果预览 `slice(0, N)` 按 UTF-16 码元截断,emoji 🔴(代理对)恰在
18
- * 截断边界被切成孤立高代理 → 注入 system reminder → 每轮发送 → deepseek 400。
19
- * 对策:发送前把孤立代理替换为 U+FFFD(任何来源安全兜底);源头截断点另修 UTF-16 安全切。
20
- */
21
-
22
- /** 中和非法字面 hex 转义序列(毒源①)。 */
23
- export function escapeLiteralEscapes(text) {
24
- text = String(text ?? "")
25
- let out = ""
26
- let i = 0
27
- const n = text.length
28
- while (i < n) {
29
- const ch = text[i]
30
- if (ch !== "\\") { out += ch; i++; continue }
31
- // 数反斜杠 run 长度
32
- let run = 0
33
- while (i + run < n && text[i + run] === "\\") run++
34
- const next = text[i + run]
35
- if ((next === "x" || next === "u") && run % 2 === 1) {
36
- // run 奇数 → 二次解析配对消费后尾部 \x5Cx/\x5Cu 裸露——hex 不足则网关炸 → 前插反斜杠 double
37
- const need = next === "u" ? 4 : 2
38
- const after = text.slice(i + run + 1, i + run + 1 + need)
39
- if (!new RegExp(`^[0-9a-fA-F]{${need}}$`).test(after)) {
40
- out += "\\".repeat(run + 1) + next
41
- i += run + 1
42
- continue
43
- }
44
- // 合法完整:输出全序列并跳过(hex 尾不重新扫描)
45
- out += text.slice(i, i + run + 1 + need)
46
- i += run + 1 + need
47
- continue
48
- }
49
- out += "\\".repeat(run)
50
- i += run
51
- }
52
- return out
53
- }
54
-
55
- /** 净化孤立 UTF-16 代理对(毒源②):高代理无低代理跟随 / 低代理无高代理前置 → 替换为 。 */
56
- export function sanitizeLoneSurrogates(text) {
57
- text = String(text ?? "")
58
- let out = ""
59
- let i = 0
60
- const n = text.length
61
- while (i < n) {
62
- const cp = text.charCodeAt(i)
63
- if (cp >= 0xd800 && cp <= 0xdbff) {
64
- const next = text.charCodeAt(i + 1)
65
- if (next >= 0xdc00 && next <= 0xdfff) { out += text[i] + text[i + 1]; i += 2; continue }
66
- out += ""; i++; continue // 孤立高代理
67
- }
68
- if (cp >= 0xdc00 && cp <= 0xdfff) { out += ""; i++; continue } // 孤立低代理
69
- out += text[i]; i++
70
- }
71
- return out
72
- }
73
-
74
- /** 发送前文本净化总入口:hex 转义中和 + 孤立代理净化。 */
75
- export function sanitizeText(text) {
76
- return sanitizeLoneSurrogates(escapeLiteralEscapes(text))
77
- }
78
-
79
- /** 对单条消息的 content 应用 sanitizeText(支持字符串或 OpenAI 多模态 part 数组)。
80
- * 2026-08-31 会诊 F5:deepseek-v4-flash 网关对 tool_calls[].function.arguments 与
81
- * reasoning_content 做同样的非标二次转义解析(字面 \\x5Cx/\\x5Cu 经工具参数/思考回传 → 400,
82
- * 列号确定性复现 = 毒序列在 content 之外)——这两个字符串字段同样需要中和。 */
83
- export function escapeMessageContent(message) {
84
- const content = message?.content
85
- let changed = false
86
- let next = message
87
- if (typeof content === "string") {
88
- const escaped = sanitizeText(content)
89
- if (escaped !== content) {
90
- next = { ...next, content: escaped }
91
- changed = true
92
- }
93
- } else if (Array.isArray(content)) {
94
- const parts = content.map((p) => {
95
- if (p && typeof p === "object" && p.type === "text" && typeof p.text === "string") {
96
- const escaped = sanitizeText(p.text)
97
- if (escaped !== p.text) {
98
- changed = true
99
- return { ...p, text: escaped }
100
- }
101
- }
102
- return p
103
- })
104
- if (changed) next = { ...next, content: parts }
105
- }
106
- if (Array.isArray(next.tool_calls)) {
107
- let tcChanged = false
108
- const tool_calls = next.tool_calls.map((tc) => {
109
- const args = tc?.function?.arguments
110
- if (typeof args === "string") {
111
- const escaped = sanitizeText(args)
112
- if (escaped !== args) {
113
- tcChanged = true
114
- return { ...tc, function: { ...tc.function, arguments: escaped } }
115
- }
116
- }
117
- return tc
118
- })
119
- if (tcChanged) {
120
- next = { ...next, tool_calls }
121
- changed = true
122
- }
123
- }
124
- if (typeof next.reasoning_content === "string") {
125
- const escaped = sanitizeText(next.reasoning_content)
126
- if (escaped !== next.reasoning_content) {
127
- next = { ...next, reasoning_content: escaped }
128
- changed = true
129
- }
130
- }
131
- return changed ? next : message
132
- }
133
-
134
- /** IKBGX4 + SESSION.md §9 D-S1:剥离仅本地使用的整消息标记字段(transient/ts)——发送给 provider 前移除。
135
- * 严格 OpenAI 兼容服务端(opencode/LiteLLM 等)会拒绝消息级未知 key
136
- * ("Extra inputs are not permitted, field: 'messages[i].transient'");ts 同理
137
- * (消息时间戳是本地取证字段,不进任何 provider 请求——T-S3)。copy-on-write:
138
- * 历史里的原对象不动(read_history 仍能读到 ts)。 */
139
- export function stripLocalMessageFields(messages) {
140
- return messages.map((m) => {
141
- if (m && typeof m === "object" && ("transient" in m || "ts" in m)) {
142
- const { transient, ts, ...rest } = m
143
- return rest
144
- }
145
- return m
146
- })
147
- }
148
-
149
- /** 对整个 messages 数组逐条应用 escapeMessageContent(先剥离本地字段,再转义)。 */
150
- export function escapeMessages(messages) {
151
- return stripLocalMessageFields(messages).map(escapeMessageContent)
152
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * expand-home.mjs — 配置路径字段家目录展开器(第 29 批 HOME-EXPANSION,2026-09-11)。
3
- * `loadConfig()` 单一规范化点(设计 MEMORY.md §9.3a):纯函数、零依赖、home 可注入。
4
- */
5
-
6
- import { homedir } from "node:os"
7
- import { join } from "node:path"
8
-
9
- /** 展开配置路径字段的前缀 `~`(`~` / `~/` / `~\`)为主目录绝对路径;不识别形态原样返回。
10
- * home 第二参 = 测试注入缝(生产缺省 homedir())。 */
11
- export function expandHome(p, home = homedir()) {
12
- if (typeof p !== "string" || !p.startsWith("~")) return p // 非字符串 / 非 ~ 前缀 → 原样
13
- if (p === "~") return home // 裸 ~ = 主目录
14
- if (p[1] !== "/" && p[1] !== "\\") return p // ~user 等非分隔符 → 原样(不猜用户)
15
- return join(home, p.slice(2).replaceAll("\\", "/")) // 余段分隔符归一(跨端统一)
16
- }
@@ -1,155 +0,0 @@
1
- /**
2
- * explore-distill.mjs — End-of-run exploration distillation (AGENT-LOOP §13 +
3
- * CONTEXT-COMPACTION §5, 2026-08-23). 2026-09-05 module-split: moved verbatim out of
4
- * context.mjs (524 > 500 hard limit). The main agent's machine line is flooded by inline
5
- * step-by-step exploration (read/grep/...). At run end we distill THIS run's exploration
6
- * tool-results into one semantic summary note that replaces them in the machine line,
7
- * while agent._fullHistory (the human line) stays untouched. VS Code compact.mjs
8
- * distillation mirrors this module (same-name file in thincoder-vscode/src, cross-repo
9
- * parity anchors point here).
10
- */
11
-
12
- import { chat } from "./provider/index.mjs"
13
-
14
- /** Read-only knowledge tools counted as "exploration" (execute writes files → never exploration). */
15
- export const EXPLORE_TOOLS = new Set([
16
- "read", "grep", "glob", "ls", "code_search", "doc_search", "repo_outline",
17
- ])
18
-
19
- /** Summary prompt for turning a burst of exploration results into a semantic summary. */
20
- export const EXPLORE_SUMMARY_PROMPT = `You are distilling exploration tool results. Summarize the following read-only codebase exploration into a compact semantic summary for the main agent's own context.
21
-
22
- Requirements:
23
- - Capture WHAT was discovered, WHERE (which files / directories / symbols), and the KEY CONCLUSIONS — do not list tool calls mechanically
24
- - Keep actionable facts the main agent needs to continue: code locations, function names, file paths, structure, and open questions the exploration raised
25
- - Drop raw tool-output noise, repeated lines, and verbatim file dumps — keep only what must be remembered
26
- - Be honest: mark anything not actually verified as "unverified"; do not present guesses as facts
27
- - Use bullet points; aim for information completeness, not a hard word limit
28
-
29
- Exploration log:
30
- `
31
-
32
- /** tool_calls name across both stored shapes ({function:{name}} and flat {name}). */
33
- function toolCallName(tc) {
34
- return tc?.function?.name ?? tc?.name ?? ""
35
- }
36
-
37
- /** Tool that produced a tool-result message (falls back to its owner assistant's tool_call). */
38
- function toolResultName(msg, ownerToolCalls) {
39
- if (typeof msg?.name === "string" && msg.name) return msg.name
40
- const owner = (ownerToolCalls ?? []).find((tc) => tc.id === msg?.tool_call_id)
41
- return owner ? toolCallName(owner) : ""
42
- }
43
-
44
- /**
45
- * Find the pure-exploration "assistant(tool_calls)→tool…" pair blocks added since `start`.
46
- * A block is explorable only when EVERY tool call AND every tool result in it is an exploration
47
- * tool — mixed blocks (read + edit in one turn) stay untouched, or we'd orphan the edit pairing.
48
- */
49
- function findExplorationBlocks(history, start) {
50
- const blocks = []
51
- let i = start
52
- while (i < history.length) {
53
- const m = history[i]
54
- if (m?.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
55
- let j = i + 1
56
- while (j < history.length && history[j]?.role === "tool") j++
57
- const toolMsgs = history.slice(i + 1, j)
58
- const allCallsExplore = m.tool_calls.every((tc) => EXPLORE_TOOLS.has(toolCallName(tc)))
59
- const allResultsExplore = toolMsgs.length > 0 && toolMsgs.every((t) => EXPLORE_TOOLS.has(toolResultName(t, m.tool_calls)))
60
- if (allCallsExplore && allResultsExplore) {
61
- blocks.push({ start: i, end: j, messages: history.slice(i, j), toolCount: toolMsgs.length })
62
- }
63
- i = j
64
- } else {
65
- i++
66
- }
67
- }
68
- return blocks
69
- }
70
-
71
- /** Serialize a batch of exploration messages for the summary LLM (same shape as compaction serialization). */
72
- function serializeExplorationMessages(messages) {
73
- const cap = 8000 // exploration results ARE the signal to distill — generous cap (quality-first, N1)
74
- return messages
75
- .map((m) => {
76
- const toolNote = m.tool_calls ? ` [called tools: ${m.tool_calls.map(toolCallName).join(", ")}]` : ""
77
- let text = ""
78
- if (typeof m.content === "string") text = m.content
79
- else if (Array.isArray(m.content)) text = m.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join(" ")
80
- return `[${m.role}]${toolNote} ${text.slice(0, cap)}`
81
- })
82
- .join("\n")
83
- }
84
-
85
- /**
86
- * Core (shared) distillation: replace this run's pure-exploration pair blocks with a single
87
- * "[Exploration summary]" note placed where the first block was. Returns a NEW history array,
88
- * or null when there is nothing to shrink (<3 exploration results / LLM failure). Pairing-safe:
89
- * whole assistant→tool blocks are removed, so no orphan tool_calls/tool can survive.
90
- */
91
- async function distillExplorations(history, start, provider, signal, agent, depth) {
92
- if (!Array.isArray(history) || history.length - start < 2) return null
93
- const blocks = findExplorationBlocks(history, start)
94
- const resultCount = blocks.reduce((n, b) => n + b.toolCount, 0)
95
- if (resultCount < 3) return null
96
-
97
- const serialized = blocks.map((b) => serializeExplorationMessages(b.messages)).join("\n")
98
-
99
- let summary
100
- try {
101
- // Silent by design (D11): thinking:null and no onToken/onReasoning — this internal
102
- // distillation must not stream to the frontend. signal propagates user cancellation.
103
- const resp = await chat({ ...provider, thinking: null, reasoningEffort: null }, {
104
- messages: [{ role: "user", content: EXPLORE_SUMMARY_PROMPT + serialized }],
105
- signal,
106
- // §18.6 D-TR4:轨迹元数据增补——kind=distill(探索蒸馏面——agent 元数据透出;
107
- // depth 经 summarizeRunExplorations 参数透传——agent.mjs 主作用域传入)
108
- logCtx: {
109
- stage: "distill", child: agent?._logId ?? null, kind: "distill",
110
- role: agent?._role ?? null, depth: depth ?? null,
111
- session: agent?._sessionStart ?? null, cwd: agent?.cwd ?? process.cwd(),
112
- traces: agent?.config?.traces?.enabled !== false,
113
- },
114
- })
115
- summary = resp?.content
116
- } catch {
117
- return null // N3: never block the run's return or lose history — original results stay
118
- }
119
- if (!summary) return null
120
-
121
- const drop = new Set()
122
- for (const b of blocks) for (let k = b.start; k < b.end; k++) drop.add(k)
123
- const note = { role: "user", content: "[Exploration summary]\n" + summary }
124
- const next = []
125
- let inserted = false
126
- for (let k = 0; k < history.length; k++) {
127
- if (drop.has(k)) {
128
- if (!inserted) { next.push(note); inserted = true }
129
- continue
130
- }
131
- next.push(history[k])
132
- }
133
- return next
134
- }
135
-
136
- /**
137
- * End-of-run exploration distillation (runAgent's final return). Shrinks the MACHINE line
138
- * (agent.history) only; agent._fullHistory is never touched. Triggers when this run added ≥3
139
- * exploration tool results; on LLM failure it silently keeps the original history (N3).
140
- * The distillation itself is silent and never streams (D11); `callbacks.onDistilled` fires
141
- * ONLY after the replacement actually lands (never on no-op/failure) — callers persist the
142
- * compressed session (SEND-STALL-DISTILL §2.3).
143
- */
144
- export async function summarizeRunExplorations(agent, callbacks, signal, depth = 0) {
145
- const next = await distillExplorations(agent.history, agent._runStartHistoryLen ?? 0, agent.provider, signal, agent, depth)
146
- if (!next) return
147
- agent.history = next
148
- // The machine line changed shape — the measured token baseline was for the pre-shrink context.
149
- // Invalidate so the next compaction check re-estimates instead of over-counting stale history.
150
- agent._lastPromptTokens = null
151
- agent._usageAtLen = null
152
- // The compressed machine line must reach the disk: the run's own save already happened,
153
- // so without this hook the async distill would leave the session un-compressed on exit.
154
- callbacks.onDistilled?.()
155
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * generate-title.mjs — LLM-generated session titles (CLI side)
3
- * Called after the first user message to auto-title the session.
4
- * Mirrors thincoder-vscode/src/extension/generate-title.mjs but uses the CLI provider shape.
5
- *
6
- * CLI is OpenAI-compatible ONLY: a single direct fetch (no anthropic/google format dispatch) —
7
- * see docs/design/SESSION.md §IK9UZ8-D.
8
- */
9
-
10
- import { proxyFetch } from "./proxy.mjs"
11
-
12
- // Test seam (_-prefix, mirrors run.mjs seams): lets the proxy-branch regression
13
- // test swap the proxy fetch. The branch it exercises used to carry a dynamic
14
- // import("../proxy.mjs") that silently resolved to the REPO ROOT from src/ —
15
- // the thrown ERR_MODULE_NOT_FOUND vanished into the catch, and proxy users
16
- // lost session titles with zero test coverage (2026-08-30 review).
17
- export const _deps = { proxyFetchImpl: proxyFetch }
18
-
19
- const MAX_TITLE_TOKENS = 100
20
-
21
- /** Generate a session title from the first user message using an LLM. Returns title string or null. */
22
- export async function generateTitle(userContent, provider) {
23
- // Extract text even from multimodal content (array of parts)
24
- const userText = Array.isArray(userContent)
25
- ? userContent.find((p) => p.type === "text")?.text || ""
26
- : userContent
27
- if (typeof userText !== "string" || userText.length < 10) return null
28
- if (!provider?.apiKey || !provider?.baseURL || !provider?.model) return null
29
-
30
- try {
31
- const body = JSON.stringify({
32
- model: provider.model,
33
- messages: [
34
- { role: "system", content: "Generate a concise title (max 40 chars, no quotes) for this conversation. Reply ONLY with the title." },
35
- { role: "user", content: userText.slice(0, 200) },
36
- ],
37
- // Disable thinking so reasoning_content doesn't consume the whole output budget and
38
- // leave content empty (IK9UZ8). Providers that don't accept the field ignore it
39
- // (OpenAI-compatible convention). A 40-char title wants ~60–80 tokens, so 100 is
40
- // ~2.5x headroom (design decision — docs/design/SESSION.md §IK9UZ8-D).
41
- thinking: { type: "disabled" },
42
- max_tokens: MAX_TITLE_TOKENS,
43
- stream: false,
44
- })
45
- const chatPath = provider.chatPath ?? "/chat/completions"
46
- const url = `${provider.baseURL.replace(/\/+$/, "")}${chatPath}`
47
- const opts = {
48
- method: "POST",
49
- headers: { ...(provider.headers ?? {}), "Content-Type": "application/json", Authorization: `Bearer ${provider.apiKey}` }, // 定制头展开(PROVIDER.md §21)——定制头在前、内置头在后:内置头胜出
50
- body,
51
- signal: AbortSignal.timeout(10000),
52
- }
53
- const res = provider.proxyUri
54
- ? await _deps.proxyFetchImpl(url, opts, provider.proxyUri)
55
- : await fetch(url, opts)
56
- if (!res.ok) return null
57
- const data = await res.json()
58
- const title = data.choices?.[0]?.message?.content?.trim().slice(0, 40)
59
- return title || null
60
- } catch {
61
- return null
62
- }
63
- }
64
-
65
- /** Derive + assign the session title from the first user message (once per session).
66
- * Extracted from agent-turn.mjs's finally block (2026-08-30): the lookup + call +
67
- * assign belongs beside generateTitle, not in the turn driver. Non-fatal on
68
- * failure — title generation must never break the turn. Returns the title (or null). */
69
- export async function ensureSessionTitle(agent) {
70
- if (agent.title) return agent.title
71
- try {
72
- // TUI-OOM-ROOTCAUSE 批(SESSION.md §14.3.3):绑定态首条 user 消息在记录存储(段 1)——
73
- // 内存窗口可能已滑过它(store.firstUserMessage 首扫一次并缓存);未绑定回退内存查找。
74
- let firstUser = agent._recordStore?.firstUserMessage?.() ?? null
75
- if (!firstUser) {
76
- firstUser = (agent._fullHistory ?? agent.history).find(
77
- (m) => m.role === "user" && typeof m.content === "string" && !m.content.startsWith("[System reminder:"),
78
- )
79
- }
80
- if (firstUser) {
81
- const title = await generateTitle(firstUser.content, agent.provider)
82
- if (title) agent.title = title
83
- }
84
- } catch {
85
- // Title generation failure is non-fatal
86
- }
87
- return agent.title ?? null
88
- }