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.
- package/CHANGELOG.md +9 -0
- package/README.md +13 -12
- package/bin/thincoder.mjs +53 -27
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -379
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +1 -1
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +1 -1
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +6 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +24 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +33 -11
- package/src/tui/index.mjs +9 -9
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +26 -10
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +10 -5
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +14 -14
- package/src/tui/subagent-blocks.mjs +20 -3
- package/src/tui/subagent-freeze.mjs +73 -2
- package/src/tui/suspension-drive.mjs +46 -22
- package/src/tui/tool-events.mjs +11 -8
- package/src/tui/wizard.mjs +3 -3
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -244
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -243
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -204
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -473
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -295
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -309
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -482
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -459
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -417
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -392
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -88
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/ledger.mjs +0 -227
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -415
- package/src/memory/core.mjs +0 -299
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -419
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/scan.mjs +0 -95
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -258
- package/src/prompts/discipline-normal.md +0 -185
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -60
- package/src/prompts/persona-engineering.md +0 -55
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -221
- package/src/session-guard.mjs +0 -59
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-segments.mjs +0 -100
- package/src/session-slots.mjs +0 -492
- package/src/session-store.mjs +0 -441
- package/src/session.mjs +0 -492
- package/src/skills.mjs +0 -153
- package/src/text-budget.mjs +0 -46
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- 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
|
-
}
|
package/src/expand-home.mjs
DELETED
|
@@ -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
|
-
}
|
package/src/explore-distill.mjs
DELETED
|
@@ -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
|
-
}
|
package/src/generate-title.mjs
DELETED
|
@@ -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
|
-
}
|