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
@@ -1,415 +0,0 @@
1
- /**
2
- * memory/code-sync.mjs — code index sync, retrieval, incremental update
3
- */
4
- import { readFile, stat } from "node:fs/promises"
5
- import { join, relative } from "node:path"
6
- import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
7
- import { scanVectors, createTopK } from "./scan.mjs"
8
- import { CODE_EXTS, DOC_EXTS, MAX_CODE_FILE_BYTES, MAX_DOC_FILE_BYTES } from "./schema.mjs"
9
- import { buildFtsQuery, ensureEmbeddings, EMBED_TEXT_MAX_LEN } from "./core.mjs"
10
- import { detectLanguage, _upsertCodeFile, _upsertDocFile, yieldTick } from "./code-index.mjs"
11
- import { walkProjectFiles, isSkippedRelPath, extensionOf, createUnlistedTally, MAX_WALK_FILES } from "./file-walk.mjs"
12
- import { loadConventions } from "../conventions.mjs"
13
- import { logEvent } from "../log.mjs"
14
-
15
- const DIFF_FULL_SYNC_THRESHOLD = 200
16
- const CODE_EMBED_BATCH = 64
17
-
18
- /**
19
- * Index extension sets = built-in tables ∪ project declaration
20
- * (`.thincoder/conventions.json` → index.codeExtensions / index.docExtensions;
21
- * PORTABILITY PO-9/§3.7 — a project may declare extensions this product does not
22
- * ship a default for). Declaration only ADDS (union), never removes.
23
- */
24
- export function indexExtensions(dir) {
25
- const conv = loadConventions(dir)
26
- return {
27
- code: new Set([...CODE_EXTS, ...conv.index.codeExtensions]),
28
- doc: new Set([...DOC_EXTS, ...conv.index.docExtensions]),
29
- }
30
- }
31
-
32
- /**
33
- * git-driven incremental indexing: use git diff to find files changed since
34
- * the last index, and only rebuild FTS5 chunks for those files (vectors are untouched).
35
- * An order of magnitude faster than full mtime scanning.
36
- * Returns { updated, removed, skipped } or null (git unavailable).
37
- */
38
- export async function gitSync(memory, dir, { onProgress } = {}) {
39
- const { execFile: _execFile } = await import("node:child_process")
40
- const { code: codeExts, doc: docExts } = indexExtensions(dir)
41
- const gitRun = (args) => new Promise((resolve, reject) => {
42
- _execFile("git", args, { cwd: dir, encoding: "utf8", timeout: 10000, windowsHide: true }, (err, stdout) => {
43
- if (err) reject(err); else resolve(stdout)
44
- })
45
- })
46
- const mergeDiff = (text) => text.trim().split("\n").filter(Boolean)
47
-
48
- let head
49
- try { head = (await gitRun(["rev-parse", "HEAD"])).trim() } catch { return null }
50
-
51
- const stored = memory.db.prepare(`SELECT value FROM meta WHERE key = 'last_indexed_commit'`).get()?.value
52
- if (!stored) return null
53
-
54
- let diffOut
55
- try {
56
- const committed = mergeDiff(await gitRun(["diff", "--name-only", "--diff-filter=ACMRTD", stored, "HEAD"]))
57
- const dirty = mergeDiff(await gitRun(["diff", "--name-only", "--diff-filter=ACMRTD"]))
58
- const lines = [...new Set([...committed, ...dirty])]
59
- diffOut = lines
60
- } catch {
61
- return null
62
- }
63
-
64
- if (diffOut.length > DIFF_FULL_SYNC_THRESHOLD) {
65
- // diff too large, incremental is useless — fall back to full sync and update anchor
66
- await codeSync(memory, dir, { onProgress })
67
- const { docSync } = await import("./docs.mjs")
68
- await docSync(memory, dir, { onProgress })
69
- return { updated: -1, removed: 0, skipped: 0, failed: 0, errors: [], fallback: true }
70
- }
71
-
72
- let updated = 0, removed = 0, skipped = 0, failed = 0
73
- const errors = []
74
- for (let i = 0; i < diffOut.length; i++) {
75
- const rel = diffOut[i].replaceAll("\\", "/")
76
- const abs = join(dir, rel)
77
- const ext = extensionOf(rel)
78
-
79
- if (isSkippedRelPath(rel)) continue
80
-
81
- if (!codeExts.has(ext) && !docExts.has(ext)) { skipped++; continue }
82
-
83
- try {
84
- const text = await readFile(abs, "utf8")
85
- const lines = text.split("\n")
86
- if (codeExts.has(ext)) {
87
- const lang = detectLanguage(abs)
88
- let mtimeMs = 0
89
- try { mtimeMs = Math.floor((await stat(abs)).mtimeMs) } catch { /* new file */ }
90
- _upsertCodeFile(memory, dir, rel, lines, lang, mtimeMs)
91
- } else {
92
- let mtimeMs = 0
93
- try { mtimeMs = Math.floor((await stat(abs)).mtimeMs) } catch { /* new file */ }
94
- _upsertDocFile(memory, dir, rel, lines, mtimeMs)
95
- }
96
- updated++
97
- } catch (e) {
98
- const isDeleted = e.code === "ENOENT"
99
- if (isDeleted) {
100
- if (codeExts.has(ext)) memory.db.prepare(`DELETE FROM code_chunks WHERE origin = ? AND path = ?`).run(dir, rel)
101
- else memory.db.prepare(`DELETE FROM doc_chunks WHERE origin = ? AND path = ?`).run(dir, rel)
102
- removed++
103
- } else {
104
- failed++
105
- if (errors.length < 5) errors.push(`${rel}: ${e.message}`)
106
- }
107
- }
108
- await yieldTick()
109
- if (onProgress && i % 5 === 0) {
110
- onProgress({ phase: "index", current: i + 1, total: diffOut.length, updated, removed, skipped })
111
- }
112
- }
113
-
114
- if (failed === 0) {
115
- memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('last_indexed_commit', ?)
116
- ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(head)
117
- }
118
-
119
- onProgress?.({ phase: "done", total: diffOut.length, updated, removed, skipped, failed })
120
- return { updated, removed, skipped, failed, errors }
121
- }
122
-
123
- /**
124
- * List project files matching the given extensions.
125
- * Preferred source = git (`git ls-files --cached --others --exclude-standard`:
126
- * tracked + untracked-not-ignored, .gitignore respected). Projects WITHOUT git
127
- * fall back to a filesystem walk (PORTABILITY FR15/P8) — before, the index was
128
- * silently empty there. The walk cannot honour .gitignore (no git → no such
129
- * concept) and skips the shared SKIP_DIRS/dot-directory set instead.
130
- * Returns { entries, unlisted }: `entries` = { abs, rel } pairs (rel relative to
131
- * dir); `unlisted` = files whose extension is in NO index list (count + sample),
132
- * the visible signal behind "my .xyz files are not searchable" (PORTABILITY PO-9).
133
- * `truncated` = the non-git walk hit its file cap (capped trees are logged, never
134
- * silently indexed-partial). opts.maxFiles = the walk cap (test seam).
135
- */
136
- export async function listProjectFiles(dir, exts, { maxFiles } = {}) {
137
- const { execFile: _execFile } = await import("node:child_process")
138
- const { join: joinPath } = await import("node:path")
139
- const { code, doc } = indexExtensions(dir)
140
- const tally = createUnlistedTally(new Set([...code, ...doc]))
141
-
142
- const files = []
143
- const finish = (truncated = false) => ({ entries: files, unlisted: tally.result(), truncated })
144
-
145
- // Git listing when available; a non-git project falls through to the walk.
146
- let gitTop
147
- try {
148
- gitTop = (await new Promise((resolve, reject) => {
149
- _execFile("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf8", timeout: 5000, windowsHide: true },
150
- (err, stdout) => { if (err) reject(err); else resolve(stdout.trim()) })
151
- })).replace(/\\/g, "/")
152
- } catch {
153
- // Not a git repo → filesystem walk (FR15: the index must still be usable).
154
- const walked = await walkProjectFiles(dir, exts, { knownExts: new Set([...code, ...doc]), ...(maxFiles !== undefined ? { maxFiles } : {}) })
155
- files.push(...walked.files)
156
- // The walk's cap must stay visible end-to-end (file-walk.mjs: "never silently
157
- // dropped") — a capped listing says so in the log, not only in a discarded flag.
158
- if (walked.truncated) {
159
- logEvent("index:truncated", { dir, files: walked.files.length, maxFiles: maxFiles ?? MAX_WALK_FILES })
160
- }
161
- return { entries: files, unlisted: walked.unlisted, truncated: walked.truncated }
162
- }
163
-
164
- try {
165
- const raw = await new Promise((resolve, reject) => {
166
- _execFile("git", ["ls-files", "--cached", "--others", "--exclude-standard"],
167
- { cwd: dir, encoding: "utf8", timeout: 15000, windowsHide: true, maxBuffer: 10 * 1024 * 1024 },
168
- (err, stdout) => { if (err) reject(err); else resolve(stdout) })
169
- })
170
- for (const line of raw.trim().split("\n")) {
171
- const p = line.trim()
172
- if (!p) continue
173
- const rel = p.replace(/\\/g, "/")
174
- if (isSkippedRelPath(rel)) continue
175
- const ext = extensionOf(rel)
176
- if (!ext || !exts.has(ext)) { tally.note(rel); continue }
177
- files.push({ abs: joinPath(dir, rel), rel })
178
- }
179
- } catch { /* ls-files failed */ }
180
-
181
- return finish()
182
- }
183
-
184
-
185
- /**
186
- * Sync code index: scan all source files under dir → chunk → upsert into code_chunks.
187
- * Incremental by mtime — only rebuilds chunks for files that have changed.
188
- */
189
- export async function codeSync(memory, dir, { onProgress } = {}) {
190
- const { code: exts } = indexExtensions(dir)
191
- const { entries, unlisted } = await listProjectFiles(dir, exts)
192
- const files = [] // { abs, rel, mtimeMs }
193
- let overSizeSkipped = 0
194
- for (const { abs, rel } of entries) {
195
- let st
196
- try { st = await stat(abs) } catch { continue }
197
- if (st.size > MAX_CODE_FILE_BYTES) { overSizeSkipped++; continue }
198
- files.push({ abs, rel, mtimeMs: Math.floor(st.mtimeMs) })
199
- }
200
-
201
- const indexed = new Map(
202
- memory.db.prepare(`SELECT path, mtime_ms FROM code_chunks WHERE origin = ?`).all(dir).map((r) => [r.path, r.mtime_ms])
203
- )
204
- const seen = new Set()
205
-
206
- onProgress?.({ phase: "scan", total: files.length, overSizeSkipped })
207
-
208
- let updated = 0, removed = 0, skipped = 0, failed = 0
209
- const errors = []
210
- for (let i = 0; i < files.length; i++) {
211
- const { abs, rel, mtimeMs } = files[i]
212
- seen.add(rel)
213
-
214
- if (indexed.get(rel) === mtimeMs) {
215
- skipped++
216
- continue
217
- }
218
-
219
- try {
220
- const text = await readFile(abs, "utf8")
221
- const lines = text.split("\n")
222
- const lang = detectLanguage(abs)
223
- _upsertCodeFile(memory, dir, rel, lines, lang, mtimeMs)
224
- updated++
225
- } catch (e) {
226
- failed++
227
- if (errors.length < 5) errors.push(`${rel}: ${e.message}`)
228
- }
229
- await yieldTick()
230
-
231
- if (onProgress && i % 10 === 0) {
232
- onProgress({ phase: "index", current: i + 1, total: files.length, updated, removed, skipped, failed })
233
- }
234
- }
235
-
236
- for (const stale of indexed.keys()) {
237
- if (!seen.has(stale)) {
238
- memory.db.prepare(`DELETE FROM code_chunks WHERE origin = ? AND path = ?`).run(dir, stale)
239
- removed++
240
- }
241
- }
242
-
243
- onProgress?.({ phase: "done", total: files.length, updated, removed, skipped, failed, overSizeSkipped })
244
- markIndexedCommit(memory, dir)
245
- if (unlisted.count > 0) {
246
- logEvent("index:unlisted", { dir, kind: "code", count: unlisted.count, exts: unlisted.exts.map((e) => e.ext) })
247
- }
248
- return { updated, removed, skipped, failed, errors, total: files.length, overSizeSkipped, unlistedExts: unlisted }
249
- }
250
-
251
- /** Record current HEAD as the index anchor (gitSync incremental diff baseline); silently skip non-git repos */
252
- export async function markIndexedCommit(memory, dir) {
253
- try {
254
- const { execFile } = await import("node:child_process")
255
- const head = await new Promise((resolve, reject) => {
256
- execFile("git", ["rev-parse", "HEAD"], { cwd: dir, encoding: "utf8", timeout: 5000, windowsHide: true }, (err, stdout) => {
257
- if (err) reject(err); else resolve(stdout)
258
- })
259
- })
260
- memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('last_indexed_commit', ?)
261
- ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(head.trim())
262
- } catch { /* not a git repo or git unavailable, skip */ }
263
- }
264
-
265
- /**
266
- * Code search: FTS5(BM25) + optional vector cosine, RRF merged.
267
- * Falls back to pure FTS when no embedder; falls back to pure vector when ftsQuery is empty and embedder is present.
268
- */
269
- export async function codeSearch(memory, query, { limit = 5 } = {}) {
270
- const ftsQuery = buildFtsQuery(query)
271
- if (!ftsQuery && !memory.embedder) return []
272
-
273
- const ftsOriginFilter = memory.codeOrigin ? `AND c.origin = ?` : ""
274
- const vecOriginFilter = memory.codeOrigin ? `AND origin = ?` : ""
275
- const originParams = memory.codeOrigin ? [memory.codeOrigin] : []
276
-
277
- const ftsList = ftsQuery ? memory.db.prepare(`
278
- SELECT c.rowid, c.path, c.language, c.symbol_name, c.content, c.line_start, c.line_end, bm25(code_chunks_fts) AS rank
279
- FROM code_chunks_fts JOIN code_chunks c ON c.rowid = code_chunks_fts.rowid
280
- WHERE code_chunks_fts MATCH ? ${ftsOriginFilter}
281
- ORDER BY rank LIMIT ?
282
- `).all(ftsQuery, ...originParams, Math.max(limit * 4, 20)) : []
283
-
284
- if (!memory.embedder) return ftsList.slice(0, limit)
285
-
286
- try { await ensureEmbeddings(memory) } catch (e) {
287
- console.error(`[code] embedding ensure failed, falling back to FTS-only: ${e.message}`)
288
- return ftsList.slice(0, limit)
289
- }
290
- let qvec
291
- try { [qvec] = await embed(memory.embedder, [query]) } catch (e) {
292
- console.error(`[code] query embedding failed, falling back to FTS-only: ${e.message}`)
293
- return ftsList.slice(0, limit)
294
- }
295
- // TUI-OOM-ROOTCAUSE(MEMORY.md §10.3):分块扫描 + 有界 top-K(原全表 .all()——峰值 = 块 + K)
296
- const top = createTopK(Math.max(limit * 4, 20))
297
- scanVectors(memory.db, `SELECT rowid, embedding FROM code_chunks WHERE embedding IS NOT NULL ${vecOriginFilter}`, originParams, {
298
- onRow: (r) => top.push({ id: r.rowid, rowid: r.rowid, score: cosine(qvec, fromBlob(r.embedding)) }),
299
- })
300
- const vecList = top.list().map((c) => ({ rowid: c.id, score: c.score }))
301
-
302
- const K = 60
303
- const scores = new Map()
304
- ftsList.forEach((r, i) => scores.set(r.rowid, (scores.get(r.rowid) ?? 0) + 1 / (K + i + 1)))
305
- vecList.forEach((r, i) => scores.set(r.rowid, (scores.get(r.rowid) ?? 0) + 1 / (K + i + 1)))
306
-
307
- const fetchChunk = memory.db.prepare(`
308
- SELECT path, language, symbol_name, content, line_start, line_end FROM code_chunks WHERE rowid = ?
309
- `)
310
- const sorted = [...scores.entries()]
311
- .sort((a, b) => b[1] - a[1])
312
- .slice(0, limit)
313
- return sorted
314
- .map(([rowid, score]) => {
315
- const chunk = fetchChunk.get(rowid)
316
- if (!chunk) return null
317
- chunk._score = Math.round(score * 100) / 100
318
- return chunk
319
- })
320
- .filter(Boolean)
321
- }
322
-
323
- /** Lazily backfill missing vectors for code_chunks. Guarded against concurrent calls. */
324
- let _codeEmbedLock = null
325
- export function ensureCodeEmbeddings(memory) {
326
- if (_codeEmbedLock) return _codeEmbedLock
327
- _codeEmbedLock = _runEnsureCodeEmbeddings(memory).finally(() => { _codeEmbedLock = null })
328
- return _codeEmbedLock
329
- }
330
-
331
- async function _runEnsureCodeEmbeddings(memory) {
332
- if (!memory.embedder) return
333
- const modelKey = memory.embedder.model
334
- const stored = memory.db.prepare(`SELECT value FROM meta WHERE key = 'code_embedding_model'`).get()?.value
335
- if (stored !== modelKey) {
336
- memory.db.prepare(`UPDATE code_chunks SET embedding = NULL`).run()
337
- memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('code_embedding_model', ?)
338
- ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(modelKey)
339
- }
340
-
341
- const pending = memory.db.prepare(`SELECT rowid, path, symbol_name, content FROM code_chunks WHERE embedding IS NULL LIMIT ${CODE_EMBED_BATCH}`).all()
342
- if (pending.length === 0) return
343
-
344
- const texts = pending.map((r) => `${r.path}${r.symbol_name ? " :: " + r.symbol_name : ""}\n${r.content.slice(0, EMBED_TEXT_MAX_LEN)}`)
345
- const vecs = await embed(memory.embedder, texts)
346
-
347
- const update = memory.db.prepare(`UPDATE code_chunks SET embedding = ? WHERE rowid = ?`)
348
- pending.forEach((r, i) => update.run(toBlob(vecs[i]), r.rowid))
349
- }
350
-
351
- /** Generate the code_search tool (read-only). */
352
- export function codeSearchTool(memory) {
353
- return {
354
- name: "code_search",
355
- description:
356
- "Search the project's source code for relevant code. Use this to find functions, classes, or code patterns across the codebase. Supports natural language queries and code snippets. Returns matching code chunks with file paths and line numbers. Prefer doc_search for the intended design (design docs, conventions); code_search for the implementation as written. " +
357
- "For what was said in a session (conversation/chat history), use read_history.",
358
- parameters: {
359
- type: "object",
360
- properties: {
361
- query: { type: "string", description: "Natural language or code snippet to search for" },
362
- limit: { type: "number", description: "Max results (default 5)" },
363
- },
364
- required: ["query"],
365
- },
366
- readonly: true,
367
- async execute(args) {
368
- const results = await codeSearch(memory, args.query, { limit: args.limit ?? 5 })
369
- if (results.length === 0) return "(no matching code)"
370
- return results.map((r) =>
371
- `${r.path}${r.symbol_name ? ` :: ${r.symbol_name}` : ""} (L${r.line_start}-L${r.line_end}, relevance ${r._score?.toFixed(2) ?? "?"}):\n${r.content.slice(0, 2000)}`
372
- ).join("\n\n---\n\n")
373
- },
374
- }
375
- }
376
-
377
- /**
378
- * Single-file incremental reindex: called after write/edit/delete, only rebuilds this one path.
379
- */
380
- export async function reindexFile(memory, cwd, absPath) {
381
- const ext = extensionOf(absPath)
382
- const rel = relative(cwd, absPath).replaceAll("\\", "/")
383
- if (rel === ".." || rel.startsWith("../")) return
384
- if (isSkippedRelPath(rel)) return
385
- // Declared extensions count for the single-file path too (union with built-ins).
386
- const { code: codeExts, doc: docExts } = indexExtensions(cwd)
387
-
388
- // skip oversized files (minified bundles, test fixtures, generated code)
389
- const maxBytes = codeExts.has(ext) ? MAX_CODE_FILE_BYTES : docExts.has(ext) ? MAX_DOC_FILE_BYTES : 0
390
- if (maxBytes > 0) {
391
- try { const st = await stat(absPath); if (st.size > maxBytes) return } catch { /* can't stat, proceed */ }
392
- }
393
-
394
- let text
395
- try { text = await readFile(absPath, "utf8") } catch {
396
- if (codeExts.has(ext)) memory.db.prepare(`DELETE FROM code_chunks WHERE origin = ? AND path = ?`).run(cwd, rel)
397
- else if (docExts.has(ext)) memory.db.prepare(`DELETE FROM doc_chunks WHERE origin = ? AND path = ?`).run(cwd, rel)
398
- return
399
- }
400
- const lines = text.split("\n")
401
-
402
- if (codeExts.has(ext)) {
403
- const lang = detectLanguage(absPath)
404
- let mtimeMs = 0
405
- try { mtimeMs = Math.floor((await stat(absPath)).mtimeMs) } catch { /* new file */ }
406
- _upsertCodeFile(memory, cwd, rel, lines, lang, mtimeMs)
407
- } else if (docExts.has(ext)) {
408
- let mtimeMs = 0
409
- try { mtimeMs = Math.floor((await stat(absPath)).mtimeMs) } catch { /* new file */ }
410
- _upsertDocFile(memory, cwd, rel, lines, mtimeMs)
411
- }
412
- if (memory.embedder) {
413
- try { await ensureEmbeddings(memory) } catch { /* embedding failure is non-blocking */ }
414
- }
415
- }