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,299 +0,0 @@
1
- /**
2
- * memory/core.mjs — memory CRUD, hybrid retrieval, embedding management.
3
- * The disk-truth row-match + deletion family (matchMemoryRows/deleteWhere/deleteByUid/remove/
4
- * assertPathInside) lives in delete.mjs — this module imports nothing from it (delete.mjs
5
- * imports fetchEntry/syncDir from here).
6
- */
7
-
8
- import { parseEntry, serializeEntry, entryFilename } from "../markdown.mjs"
9
- import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
10
- import { scanVectors, createTopK } from "./scan.mjs"
11
- import { readFile, stat, readdir, writeFile, mkdir } from "node:fs/promises"
12
- import { join } from "node:path"
13
- import { segmentCJK, VALID_TYPES, SCHEMA_VERSION } from "./schema.mjs"
14
-
15
- const EMBED_BATCH_SIZE = 256
16
- export const EMBED_TEXT_MAX_LEN = 2000
17
- const FTS_TOKEN_MAX = 16
18
- const DEFAULT_LIST_LIMIT = 50
19
-
20
- /**
21
- * Write a memory entry. entry: { type, title, content, tags? }
22
- * Returns the new entry id.
23
- */
24
- export async function put(memory, { type, title, content, tags = "" }) {
25
- if (!VALID_TYPES.has(type)) {
26
- throw new Error(`Invalid memory type "${type}"; expected one of: ${[...VALID_TYPES].join(", ")}`)
27
- }
28
- if (!title || !content) throw new Error("memory entry requires title and content")
29
- const now = Date.now()
30
- const stmt = memory.db.prepare(
31
- `INSERT INTO entries (type, title, content, tags, seg_title, seg_content, seg_tags, created_at, updated_at)
32
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
33
- )
34
- const info = stmt.run(type, title, content, tags, segmentCJK(title), segmentCJK(content), segmentCJK(tags), now, now)
35
- return Number(info.lastInsertRowid)
36
- }
37
-
38
- /**
39
- * Hybrid retrieval: FTS5(BM25) + vector cosine, RRF(k=60) merged ranking.
40
- * Falls back to pure FTS when no embedder. Results include layer label.
41
- * Returns [{ id, layer, type, title, content, tags, rank }]
42
- */
43
- export async function search(memory, query, { limit = 5 } = {}) {
44
- const ftsQuery = buildFtsQuery(query)
45
- const ftsList = ftsQuery ? ftsSearch(memory, ftsQuery, Math.max(limit * 4, 20)) : []
46
-
47
- if (!memory.embedder) return ftsList.slice(0, limit)
48
-
49
- // ---- vector channel ----
50
- try { await ensureEmbeddings(memory) } catch (e) {
51
- console.error(`[memory] embedding ensure failed, falling back to FTS-only: ${e.message}`)
52
- return ftsList.slice(0, limit)
53
- }
54
- let qvec
55
- try { [qvec] = await embed(memory.embedder, [query]) } catch (e) {
56
- console.error(`[memory] query embedding failed, falling back to FTS-only: ${e.message}`)
57
- return ftsList.slice(0, limit)
58
- }
59
- const vecFilter = memory.projectOrigin ? `AND (layer = 'team' OR origin = ?)` : ""
60
- const vecParams = memory.projectOrigin ? [memory.projectOrigin] : []
61
- // TUI-OOM-ROOTCAUSE(MEMORY.md §10.3):分块扫描 + 有界 top-K(原全表 .all() 物化 +
62
- // 全量排序——峰值 = 块 + K;召回语义不变)。两表各自游标扫描、共享同一 top-K。
63
- const top = createTopK(Math.max(limit * 4, 20))
64
- const onRow = (r) => top.push({ id: r.uid, score: cosine(qvec, fromBlob(r.embedding)) })
65
- scanVectors(memory.db, `SELECT rowid, 'personal:' || id AS uid, embedding FROM entries WHERE embedding IS NOT NULL`, [], { onRow })
66
- scanVectors(memory.db, `SELECT rowid, layer || ':' || COALESCE(origin, '') || ':' || path AS uid, embedding FROM files WHERE embedding IS NOT NULL ${vecFilter}`, vecParams, { onRow })
67
- const vecList = top.list()
68
-
69
- // ---- RRF merge ----
70
- const K = 60
71
- const scores = new Map()
72
- ftsList.forEach((r, i) => scores.set(r.id, (scores.get(r.id) ?? 0) + 1 / (K + i + 1)))
73
- vecList.forEach((r, i) => scores.set(r.id, (scores.get(r.id) ?? 0) + 1 / (K + i + 1)))
74
-
75
- return [...scores.entries()]
76
- .sort((a, b) => b[1] - a[1])
77
- .slice(0, limit)
78
- .map(([id, score]) => {
79
- const entry = fetchEntry(memory, id)
80
- return entry ? { ...entry, rrf: score } : null
81
- })
82
- .filter(Boolean)
83
- }
84
-
85
- /** Pure FTS search (two-table merge, sorted by bm25), used as the positional input for RRF */
86
- export function ftsSearch(memory, ftsQuery, limit) {
87
- const personal = memory.db.prepare(`
88
- SELECT e.id, e.type, e.title, e.content, e.tags, bm25(entries_fts) AS rank
89
- FROM entries_fts JOIN entries e ON e.id = entries_fts.rowid
90
- WHERE entries_fts MATCH ?
91
- ORDER BY rank LIMIT ?
92
- `).all(ftsQuery, limit).map((r) => ({ ...r, layer: "personal", id: `personal:${r.id}` }))
93
-
94
- const originFilter = memory.projectOrigin ? `AND (f.layer = 'team' OR f.origin = ?)` : ""
95
- const originParams = memory.projectOrigin ? [ftsQuery, memory.projectOrigin, limit] : [ftsQuery, limit]
96
- const files = memory.db.prepare(`
97
- SELECT f.layer, f.origin, f.path, f.type, f.title, f.content, f.tags, f.author, bm25(files_fts) AS rank
98
- FROM files_fts JOIN files f ON f.rowid = files_fts.rowid
99
- WHERE files_fts MATCH ? ${originFilter}
100
- ORDER BY rank LIMIT ?
101
- `).all(...originParams).map((r) => ({ ...r, id: `${r.layer}:${r.origin}:${r.path}` }))
102
-
103
- return [...personal, ...files].sort((a, b) => a.rank - b.rank).slice(0, limit)
104
- }
105
-
106
- /** Fetch a full entry by unified id (personal:<n> / project:<origin>:<path> / team:<origin>:<path>)
107
- * Note: since v9 the files table PK is (layer, origin, path); the same layer+path may span multiple origins.
108
- * For project layer, prefers the row matching projectOrigin; for team layer, returns any row (first match when multiple team repos share a path). */
109
- export function fetchEntry(memory, uid) {
110
- const [layer, ...rest] = uid.split(":")
111
- if (layer === "personal") {
112
- const r = memory.db.prepare(`SELECT id, type, title, content, tags FROM entries WHERE id = ?`).get(Number(rest[0]))
113
- return r ? { ...r, layer, id: uid } : null
114
- }
115
- // Files branch: origins may contain colons (Windows drive letters, e.g. project:C:\dir:file.md),
116
- // so the LAST colon is always the origin/path separator — same parsing as deleteByUid.
117
- // origin may be empty (compat with the old `project::file.md` format).
118
- const lastColon = uid.lastIndexOf(":")
119
- const origin = lastColon > layer.length ? uid.slice(layer.length + 1, lastColon) : ""
120
- const path = lastColon > layer.length ? uid.slice(lastColon + 1) : uid.slice(layer.length + 1)
121
- if (layer === "project" && memory.projectOrigin) {
122
- const r = memory.db.prepare(`SELECT type, title, content, tags, author FROM files WHERE layer = ? AND origin = ? AND path = ?`).get(layer, origin || memory.projectOrigin, path)
123
- if (r) return { ...r, layer, id: uid }
124
- }
125
- // team layer or project fallback: query by origin+path; when origin is empty, degrade to path-only (compat with old UID)
126
- if (origin) {
127
- const r = memory.db.prepare(`SELECT type, title, content, tags, author FROM files WHERE layer = ? AND origin = ? AND path = ?`).get(layer, origin, path)
128
- if (r) return { ...r, layer, id: uid }
129
- }
130
- const r = memory.db.prepare(`SELECT type, title, content, tags, author FROM files WHERE layer = ? AND path = ?`).get(layer, path || rest.join(":"))
131
- return r ? { ...r, layer, id: uid } : null
132
- }
133
-
134
- /**
135
- * Lazy embedding: batch-compute vectors for entries that don't have them yet (slow first time, zero cost thereafter).
136
- * When the embedding model changes, clear all vectors and rebuild.
137
- * Guarded by a module-level lock — concurrent fire-and-forget callers share the same promise,
138
- * so embedding API calls are never duplicated.
139
- */
140
- let _embedLock = null
141
- export function ensureEmbeddings(memory) {
142
- if (_embedLock) return _embedLock
143
- _embedLock = _runEnsureEmbeddings(memory).finally(() => { _embedLock = null })
144
- return _embedLock
145
- }
146
-
147
- async function _runEnsureEmbeddings(memory) {
148
- const modelKey = memory.embedder.model
149
- const stored = memory.db.prepare(`SELECT value FROM meta WHERE key = 'embedding_model'`).get()?.value
150
- if (stored !== modelKey) {
151
- // Invalidate all three tables + three meta keys in one go, to prevent stale vectors from dimension mismatch
152
- memory.db.prepare(`UPDATE entries SET embedding = NULL`).run()
153
- memory.db.prepare(`UPDATE files SET embedding = NULL`).run()
154
- memory.db.prepare(`UPDATE code_chunks SET embedding = NULL`).run()
155
- memory.db.prepare(`UPDATE doc_chunks SET embedding = NULL`).run()
156
- const upsert = memory.db.prepare(`INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value`)
157
- upsert.run("embedding_model", modelKey)
158
- upsert.run("code_embedding_model", modelKey)
159
- upsert.run("doc_embedding_model", modelKey)
160
- }
161
-
162
- const pendingEntries = memory.db.prepare(`SELECT id, title, content FROM entries WHERE embedding IS NULL LIMIT ${EMBED_BATCH_SIZE}`).all()
163
- const pendingFiles = memory.db.prepare(`SELECT rowid, title, content FROM files WHERE embedding IS NULL LIMIT ${EMBED_BATCH_SIZE}`).all()
164
- if (pendingEntries.length + pendingFiles.length === 0) {
165
- // No pending memory entries — also backfill code and doc chunk vectors
166
- await (await import("./code-sync.mjs")).ensureCodeEmbeddings(memory)
167
- await (await import("./docs.mjs")).ensureDocEmbeddings(memory)
168
- return
169
- }
170
-
171
- const items = [...pendingEntries, ...pendingFiles]
172
- const texts = items.map((r) => `${r.title}\n${r.content.slice(0, EMBED_TEXT_MAX_LEN)}`)
173
- const vecs = await embed(memory.embedder, texts)
174
-
175
- const updateEntry = memory.db.prepare(`UPDATE entries SET embedding = ? WHERE id = ?`)
176
- pendingEntries.forEach((r, i) => updateEntry.run(toBlob(vecs[i]), r.id))
177
- const updateFile = memory.db.prepare(`UPDATE files SET embedding = ? WHERE rowid = ?`)
178
- pendingFiles.forEach((r, i) => updateFile.run(toBlob(vecs[pendingEntries.length + i]), r.rowid))
179
-
180
- // After each batch of embeddings, also backfill code and doc chunks
181
- await (await import("./code-sync.mjs")).ensureCodeEmbeddings(memory)
182
- await (await import("./docs.mjs")).ensureDocEmbeddings(memory)
183
- }
184
-
185
- /**
186
- * Write a markdown memory entry to the specified layer directory (project/team) and index it immediately.
187
- * Writes the file only — the project layer never performs git operations on the user's project repo;
188
- * team layer commit+push is handled by gitmem.mjs.
189
- * Returns the filename.
190
- */
191
- export async function putMarkdown(memory, { layer, dir, type, title, content, tags = [], author = "unknown" }) {
192
- if (layer !== "project" && layer !== "team") throw new Error(`invalid markdown layer: ${layer}`)
193
- const filename = entryFilename(title)
194
- const markdown = serializeEntry({ type, title, tags, author }, content)
195
- await mkdir(dir, { recursive: true })
196
- await writeFile(join(dir, filename), markdown, "utf8")
197
- await indexMarkdownFile(memory, { layer, dir, filename })
198
- return filename
199
- }
200
-
201
- /**
202
- * Sync a markdown directory to the index: new/changed (by mtime) entries are re-indexed,
203
- * vanished entries are removed from the index.
204
- */
205
- export async function syncDir(memory, { layer, dir }) {
206
- let names
207
- try {
208
- names = (await readdir(dir)).filter((n) => n.endsWith(".md"))
209
- } catch {
210
- names = []
211
- }
212
-
213
- const indexed = new Map(
214
- memory.db.prepare(`SELECT path, mtime_ms FROM files WHERE layer = ? AND origin = ?`).all(layer, dir).map((r) => [r.path, r.mtime_ms]),
215
- )
216
-
217
- let added = 0, updated = 0, skipped = 0
218
- for (const filename of names) {
219
- const mtimeMs = Math.floor((await stat(join(dir, filename))).mtimeMs)
220
- const old = indexed.get(filename)
221
- const isNew = old === undefined
222
- if (!isNew && old === mtimeMs) continue
223
- try {
224
- await indexMarkdownFile(memory, { layer, dir, filename, mtimeMs })
225
- } catch (e) {
226
- console.error(`[memory] skip ${layer}/${filename}: ${e.message}`)
227
- skipped++
228
- indexed.delete(filename)
229
- continue
230
- }
231
- if (isNew) added++
232
- else updated++
233
- indexed.delete(filename)
234
- }
235
-
236
- let removed = 0
237
- for (const stale of indexed.keys()) {
238
- memory.db.prepare(`DELETE FROM files WHERE layer = ? AND origin = ? AND path = ?`).run(layer, dir, stale)
239
- removed++
240
- }
241
- return { added, updated, removed, skipped }
242
- }
243
-
244
- /** Parse a single .md and upsert into the files table */
245
- export async function indexMarkdownFile(memory, { layer, dir, filename, mtimeMs }) {
246
- const abs = join(dir, filename)
247
- const mtime = mtimeMs ?? Math.floor((await stat(abs)).mtimeMs)
248
- const { meta, content } = parseEntry(await readFile(abs, "utf8"))
249
- const tags = meta.tags.join(" ")
250
- memory.db.prepare(`
251
- INSERT INTO files (layer, origin, path, type, title, content, tags, author, mtime_ms, seg_title, seg_content, seg_tags, updated_at)
252
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
253
- ON CONFLICT(layer, origin, path) DO UPDATE SET
254
- type=excluded.type, title=excluded.title, content=excluded.content, tags=excluded.tags,
255
- author=excluded.author, mtime_ms=excluded.mtime_ms, origin=excluded.origin,
256
- seg_title=excluded.seg_title, seg_content=excluded.seg_content, seg_tags=excluded.seg_tags,
257
- updated_at=excluded.updated_at
258
- `).run(
259
- layer, dir, filename, meta.type, meta.title, content, tags, meta.author, mtime,
260
- segmentCJK(meta.title), segmentCJK(content), segmentCJK(tags), Date.now(),
261
- )
262
- }
263
-
264
- /** List entries, optionally filtered by type */
265
- export async function list(memory, { type, limit = DEFAULT_LIST_LIMIT } = {}) {
266
- if (type) {
267
- if (!VALID_TYPES.has(type)) throw new Error(`Invalid memory type "${type}"`)
268
- return memory.db
269
- .prepare(`SELECT id, type, title, content, tags, updated_at FROM entries WHERE type = ? ORDER BY updated_at DESC LIMIT ?`)
270
- .all(type, limit)
271
- }
272
- return memory.db
273
- .prepare(`SELECT id, type, title, content, tags, updated_at FROM entries ORDER BY updated_at DESC LIMIT ?`)
274
- .all(limit)
275
- }
276
-
277
- /** §6 clear action: wipe ALL personal entries (pure DB rows — files are project/team only).
278
- * FTS + embedding go with the row triggers. Returns the number of deleted rows. */
279
- export function clearPersonal(memory) {
280
- const { changes } = memory.db.prepare(`DELETE FROM entries`).run()
281
- return changes
282
- }
283
-
284
- /**
285
- * Build an FTS5 query: first split by whitespace/punctuation into tokens,
286
- * then apply CJK character segmentation to each token.
287
- * This keeps multi-character CJK words as FTS5 phrases ("分号" → "分 号" → phrase query, exact adjacency match),
288
- * while different tokens are joined with OR ("命名 规范" → "命 名" OR "规 范", each phrase requires its own adjacency).
289
- */
290
- export function buildFtsQuery(query) {
291
- const terms = query
292
- .split(/[\s,,。、;;!!??()()"`]+/)
293
- .map((t) => t.trim())
294
- .filter(Boolean)
295
- .slice(0, FTS_TOKEN_MAX)
296
- .map((t) => segmentCJK(t))
297
- if (terms.length === 0) return ""
298
- return terms.map((t) => `"${t.replaceAll('"', '""')}"`).join(" OR ")
299
- }
@@ -1,236 +0,0 @@
1
- /**
2
- * memory/delete.mjs — file-layer disk-truth row match + deletion family.
3
- *
4
- * Split out of core.mjs (2026-09-08 — core.mjs crossed the 500-line hard limit).
5
- * Owns: the shared §6 list/batch-delete row match (matchMemoryRows/diskFileRows/
6
- * likePattern — one match surface, MEMORY.md §6), single/batch deletion
7
- * (deleteByUid/deleteWhere), the legacy personal-only compat shell (remove),
8
- * file-row fetching and path containment (fetchFileEntry/assertPathInside).
9
- *
10
- * Imports CRUD/retrieval/sync primitives from core.mjs (fetchEntry/syncDir) —
11
- * one direction, no cycle.
12
- */
13
-
14
- import { parseEntry } from "../markdown.mjs"
15
- import { readFile, stat, readdir, unlink } from "node:fs/promises"
16
- import { join, resolve } from "node:path"
17
- import { fetchEntry, syncDir } from "./core.mjs"
18
-
19
- /** LIKE pattern from a keyword (wildcards escaped — literal substring match, MEMORY.md §6 keyword filter). */
20
- function likePattern(keyword) {
21
- return `%${keyword.replace(/[\\%_]/g, (c) => `\\${c}`)}%`
22
- }
23
-
24
- /**
25
- * Shared row query for the §6 list action and the §6 batch delete (one match surface —
26
- * rows carry { layer, id, type, title, ts }). Filters:
27
- * layer: "personal" | "project" | "team" | null (null = all layers)
28
- * type / keyword: optional (keyword matches title OR content, substring)
29
- * Personal rows come from the entries table; project/team rows come from a DISK scan of
30
- * the managed dir (2026-09-05 fix — disk is the truth): files present on disk but
31
- * missing from the files index (orphans: external copies / gitmem pull / an earlier
32
- * index failure) were invisible to list AND immune to batch delete — the old table-only
33
- * match surface made a layer wipe need repeated delete rounds (deleteWhere→syncDir
34
- * re-indexed the orphans one round later). Scanning disk keeps list and batch delete
35
- * consistent with what the user can see and delete. Rows from other projects'/team
36
- * repos' dirs stay out (the scan only covers the dirs this memory context manages).
37
- * Malformed files are skipped (parseEntry failure — same semantics as syncDir). Sorted
38
- * by ts (created/updated, ms) DESC.
39
- */
40
- export async function matchMemoryRows(memory, { layer = null, type = null, keyword = null, projectDir = null, teamDir = null } = {}) {
41
- const rows = []
42
- const wantLayer = (l) => !layer || layer === l
43
- if (wantLayer("personal")) {
44
- let sql = `SELECT id, type, title, created_at AS ts FROM entries`
45
- const cond = []
46
- const params = []
47
- if (type) { cond.push("type = ?"); params.push(type) }
48
- if (keyword) { cond.push("(title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\')"); const p = likePattern(keyword); params.push(p, p) }
49
- if (cond.length) sql += " WHERE " + cond.join(" AND ")
50
- sql += " ORDER BY created_at DESC"
51
- for (const r of memory.db.prepare(sql).all(...params)) {
52
- rows.push({ layer: "personal", id: `personal:${r.id}`, uid: `personal:${r.id}`, type: r.type, title: r.title, ts: r.ts })
53
- }
54
- }
55
- if (wantLayer("project") && projectDir) {
56
- for (const r of await diskFileRows(projectDir, type, keyword)) rows.push({ ...r, layer: "project", id: `project:${projectDir}:${r.path}` })
57
- }
58
- if (wantLayer("team") && teamDir) {
59
- for (const r of await diskFileRows(teamDir, type, keyword)) rows.push({ ...r, layer: "team", id: `team:${teamDir}:${r.path}` })
60
- }
61
- rows.sort((a, b) => (b.ts ?? 0) - (a.ts ?? 0))
62
- return rows
63
- }
64
-
65
- /**
66
- * Disk-truth file scan for the project/team layer (2026-09-05 fix — see matchMemoryRows):
67
- * readdir + parse every .md entry in dir, filter by type equality and keyword substring
68
- * on title OR content (case-insensitive — SQLite LIKE parity). ts = file mtime (ms).
69
- * Rows come back WITHOUT the layer field — the caller stamps layer and builds the uid.
70
- */
71
- async function diskFileRows(dir, type, keyword) {
72
- let names
73
- try {
74
- names = (await readdir(dir)).filter((n) => n.endsWith(".md"))
75
- } catch {
76
- return []
77
- }
78
- const kw = keyword ? keyword.toLowerCase() : null
79
- const out = []
80
- for (const name of names) {
81
- try {
82
- const abs = join(dir, name)
83
- const { meta, content } = parseEntry(await readFile(abs, "utf8"))
84
- if (type && meta.type !== type) continue
85
- if (kw && !(meta.title.toLowerCase().includes(kw) || content.toLowerCase().includes(kw))) continue
86
- const mtime = Math.floor((await stat(abs)).mtimeMs)
87
- out.push({ path: name, type: meta.type, title: meta.title, ts: mtime })
88
- } catch (e) {
89
- console.error(`[memory] skip ${name}: ${e.message}`)
90
- }
91
- }
92
- out.sort((a, b) => b.ts - a.ts)
93
- return out
94
- }
95
-
96
- /**
97
- * §6 batch delete (action delete + type/keyword filter, confirm handled by the tool layer):
98
- * deletes every row matchMemoryRows returns for the layer. Personal rows go straight to the
99
- * DB (FTS + embedding cleanup via row triggers); project/team rows delete the markdown file
100
- * (path containment enforced, ENOENT tolerated) then re-sync the layer dir once (index
101
- * cleanup single source). Match surface = disk scan (2026-09-05 fix — orphans on disk
102
- * with no index row are matched and deleted in the same pass; the trailing syncDir
103
- * re-indexes the survivors). Team deletion never touches git — a later gitmem pull may
104
- * resurrect the file while the remote still has it (same semantics as deleteByUid).
105
- * Returns the number of deleted rows.
106
- */
107
- export async function deleteWhere(memory, { layer, type = null, keyword = null } = {}, { dirs = {} } = {}) {
108
- const rows = await matchMemoryRows(memory, { layer, type, keyword, projectDir: dirs.project ?? null, teamDir: dirs.team ?? null })
109
- if (rows.length === 0) return 0
110
- const personalIds = []
111
- const byDir = new Map() // "layer\x00dir" → { layer, dir, paths: [] }
112
- for (const r of rows) {
113
- if (r.layer === "personal") {
114
- const id = Number(String(r.uid).split(":")[1])
115
- if (Number.isInteger(id)) personalIds.push(id)
116
- continue
117
- }
118
- const dir = r.layer === "project" ? dirs.project : dirs.team
119
- if (!dir) continue
120
- const key = `${r.layer}\x00${dir}`
121
- let group = byDir.get(key)
122
- if (!group) { group = { layer: r.layer, dir, paths: [] }; byDir.set(key, group) }
123
- group.paths.push(r.path)
124
- }
125
- const del = memory.db.prepare(`DELETE FROM entries WHERE id = ?`)
126
- for (const id of personalIds) del.run(id)
127
- for (const group of byDir.values()) {
128
- for (const path of group.paths) {
129
- assertPathInside(group.dir, path)
130
- const abs = join(group.dir, path)
131
- await unlink(abs).catch((e) => { if (e.code !== "ENOENT") throw e })
132
- }
133
- await syncDir(memory, { layer: group.layer, dir: group.dir })
134
- }
135
- return rows.length
136
- }
137
-
138
- /** Delete a memory entry by unified id. Returns the deleted entry (F3: { id, layer, type, title, content, tags }).
139
- * - personal:<n> (or bare <n>) → DELETE the entries row; FTS syncs via the entries_ad trigger and the
140
- * embedding BLOB column goes with the row.
141
- * - project:<origin>:<path> / team:<origin>:<path> → delete the markdown file at the uid's ORIGIN dir
142
- * (2026-09-08, MEMORY.md §6.2: search rows surfaced from a non-current origin — another project's
143
- * memory dir / another team clone — must be deletable where the file actually lives; the caller's
144
- * dirs[layer] is only the fallback base for origin-less legacy uids `project::file.md`/`project:file.md`).
145
- * Path containment is enforced against that base (assertPathInside — `..`/absolute variants incl.
146
- * `..\` are rejected), then syncDir clears the files row (single source of index cleanup). ENOENT on
147
- * the file is treated as already-deleted and continues; when the origin dir is missing locally,
148
- * syncDir still drops the stale (layer, origin) index rows (readdir of a missing dir yields nothing —
149
- * every row of a vanished dir is stale). Team deletion never touches git (git propagation is gitmem's
150
- * job; a later gitmem pull may resurrect the file while the remote still has it).
151
- * Throws on invalid id / missing entry (NF2) / path escaping the base dir.
152
- * 2026-09-08 (code review): the orphan-disk fallback below is confined to managed memory dirs —
153
- * an unmanaged origin must not make deleteByUid a crafted-id deletion primitive. */
154
- export async function deleteByUid(memory, uid, { dirs = {} } = {}) {
155
- const norm = /^\d+$/.test(uid) ? `personal:${uid}` : String(uid)
156
- const [layer, ...rest] = norm.split(":")
157
- if (layer === "personal") {
158
- // 畸形尾缀拒绝(2026-09-08 代码正确性批 2.2):personal:5:extra 不得静默删 id=5
159
- if (norm.split(":").length > 2) throw new Error(`invalid memory id: ${norm}`)
160
- const id = rest[0] ?? ""
161
- if (!/^\d+$/.test(id)) throw new Error(`invalid memory id: ${norm}`)
162
- const entry = fetchEntry(memory, norm)
163
- if (!entry) throw new Error(`memory ${norm} not found in layer personal`)
164
- memory.db.prepare(`DELETE FROM entries WHERE id = ?`).run(Number(id))
165
- return entry
166
- }
167
- if (layer !== "project" && layer !== "team") throw new Error(`invalid memory id: ${norm}`)
168
- // origin = between the layer prefix and the LAST colon (origins may contain colons — Windows drive
169
- // letters); path = segment after the last colon. Empty origin = legacy uid (`project::file.md` /
170
- // `project:file.md`) → dirs[layer] fallback.
171
- const lastColon = norm.lastIndexOf(":")
172
- const origin = lastColon > layer.length ? norm.slice(layer.length + 1, lastColon) : ""
173
- const path = lastColon > layer.length ? norm.slice(lastColon + 1) : norm.slice(layer.length + 1)
174
- const dir = origin || dirs[layer]
175
- if (!dir) throw new Error(`${layer} layer unavailable: no ${layer} directory configured for an origin-less id`)
176
- assertPathInside(dir, path)
177
- // Entry lookup: exact (layer, origin, path) row for origin-ful uids — the delete target is the uid's
178
- // own dir, so a path-only fallback row from another origin must not be reported as deleted.
179
- // Origin-less legacy uids keep the compat fallbacks (fetchFileEntry: current-origin then path-only).
180
- let entry = null
181
- if (origin) {
182
- const r = memory.db.prepare(`SELECT type, title, content, tags, author FROM files WHERE layer = ? AND origin = ? AND path = ?`).get(layer, origin, path)
183
- if (r) entry = { ...r, layer, id: norm }
184
- } else {
185
- entry = fetchFileEntry(memory, layer, norm, path)
186
- }
187
- const abs = join(dir, path)
188
- let fileExists = false
189
- try { await stat(abs); fileExists = true } catch { /* ENOENT — treat as already deleted */ }
190
- if (!entry && fileExists) {
191
- // Origin confinement (2026-09-08 code review): the orphan fallback deletes a file that has NO
192
- // index row — only allow it when the origin is a managed memory dir (it already has files rows,
193
- // or it is the caller's current layer dir). A uid naming an unmanaged directory must not delete
194
- // a memory-shaped .md there.
195
- if (origin && origin !== dirs[layer]) {
196
- const known = memory.db.prepare(`SELECT 1 FROM files WHERE layer = ? AND origin = ? LIMIT 1`).get(layer, origin)
197
- if (!known) throw new Error(`memory ${norm} not found in layer ${layer}: origin is not a managed ${layer} memory dir`)
198
- }
199
- try {
200
- const { meta, content } = parseEntry(await readFile(abs, "utf8"))
201
- entry = { layer, id: norm, type: meta.type, title: meta.title, content, tags: meta.tags.join(" ") }
202
- } catch { /* malformed file — keep the DB row (or null → not found below) */ }
203
- }
204
- if (!entry) throw new Error(`memory ${norm} not found in layer ${layer}`)
205
- if (fileExists) await unlink(abs).catch((e) => { if (e.code !== "ENOENT") throw e })
206
- await syncDir(memory, { layer, dir })
207
- return entry
208
- }
209
-
210
- /** Legacy personal-only delete (bare numeric id) — kept as the compat surface over deleteByUid. */
211
- export async function remove(memory, id) {
212
- const uid = /^\d+$/.test(String(id)) ? `personal:${id}` : String(id)
213
- if (!fetchEntry(memory, uid)) return false
214
- await deleteByUid(memory, uid, {})
215
- return true
216
- }
217
-
218
- /** Fetch a project/team file row for deletion: fetchEntry first, then a path-only fallback
219
- * (origins with Windows drive letters, e.g. project:C:\dir:file.md, break naive ":" splitting). */
220
- function fetchFileEntry(memory, layer, uid, path) {
221
- const entry = fetchEntry(memory, uid)
222
- if (entry) return entry
223
- const r = memory.db.prepare(`SELECT type, title, content, tags, author FROM files WHERE layer = ? AND path = ?`).get(layer, path)
224
- return r ? { ...r, layer, id: uid } : null
225
- }
226
-
227
- /** Separator-agnostic containment check: the resolved path must stay inside dir.
228
- * Both / and \ count as separators, so Windows-style traversal (..\..\\x) is caught on every platform. */
229
- function assertPathInside(dir, path) {
230
- if (!path) throw new Error(`invalid memory id: empty path`)
231
- const base = resolve(dir).replaceAll("\\", "/")
232
- const abs = resolve(dir, path.replaceAll("\\", "/")).replaceAll("\\", "/")
233
- if (abs !== base && !abs.startsWith(base + "/")) {
234
- throw new Error(`invalid memory path "${path}": must stay within ${dir}`)
235
- }
236
- }