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/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
- }