flint-agent 1.14.0

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 (171) hide show
  1. package/.env.example +108 -0
  2. package/CHANGELOG.md +55 -0
  3. package/FEATURES.md +298 -0
  4. package/LICENSE +21 -0
  5. package/README.md +435 -0
  6. package/bin/flint.js +47 -0
  7. package/config/classifier-prompt.md +218 -0
  8. package/config/models-curated.json +4 -0
  9. package/config/providers.json +74 -0
  10. package/package.json +92 -0
  11. package/patches/ink+6.8.0.patch +78 -0
  12. package/profiles/desktop.md +65 -0
  13. package/profiles/generic.md +20 -0
  14. package/profiles/marketer.md +20 -0
  15. package/profiles/profiles.json +34 -0
  16. package/profiles/ux-reviewer.md +25 -0
  17. package/src/agent/agent.js +1743 -0
  18. package/src/agent/auto.js +346 -0
  19. package/src/agent/backoff.js +143 -0
  20. package/src/agent/compression.js +310 -0
  21. package/src/agent/content-resolver.js +180 -0
  22. package/src/agent/flow-controller.js +309 -0
  23. package/src/agent/intent-manifest.js +231 -0
  24. package/src/agent/intent-timeout.js +46 -0
  25. package/src/agent/intent.js +633 -0
  26. package/src/agent/knowledge.js +114 -0
  27. package/src/agent/learning.js +180 -0
  28. package/src/agent/modes.js +187 -0
  29. package/src/agent/outcome-ask.js +91 -0
  30. package/src/agent/project-context.js +76 -0
  31. package/src/agent/prompt-budget.js +117 -0
  32. package/src/agent/reflection-extractor.js +140 -0
  33. package/src/agent/steering.js +86 -0
  34. package/src/agent/supervisor.js +430 -0
  35. package/src/agent/swap.js +443 -0
  36. package/src/agent/system-prompt.js +446 -0
  37. package/src/agent/time-stamp.js +48 -0
  38. package/src/agent/tool-guard.js +201 -0
  39. package/src/agent/toolcall-text.js +162 -0
  40. package/src/agent/usage.js +297 -0
  41. package/src/agent/vision.js +94 -0
  42. package/src/agent/watchdog.js +139 -0
  43. package/src/agent/workspace-changes.js +177 -0
  44. package/src/api/address.js +14 -0
  45. package/src/api/client.js +280 -0
  46. package/src/api/server.js +535 -0
  47. package/src/api/stream-pipe.js +113 -0
  48. package/src/app-state.js +39 -0
  49. package/src/bootstrap.js +501 -0
  50. package/src/bus/drain-loop.js +497 -0
  51. package/src/bus/index.js +270 -0
  52. package/src/bus/plugins.js +65 -0
  53. package/src/child-idle.js +14 -0
  54. package/src/cli.js +118 -0
  55. package/src/commands/commands.js +1297 -0
  56. package/src/commands/registry.js +132 -0
  57. package/src/components/App.js +491 -0
  58. package/src/components/CarefulMenu.js +145 -0
  59. package/src/components/HistoryWriter.js +86 -0
  60. package/src/components/LineInput.js +69 -0
  61. package/src/components/LiveZone.js +294 -0
  62. package/src/components/OverlayMenu.js +179 -0
  63. package/src/components/SystemPanel.js +156 -0
  64. package/src/components/Table.js +54 -0
  65. package/src/config.js +249 -0
  66. package/src/free-models.js +230 -0
  67. package/src/index.js +1111 -0
  68. package/src/input-handler.js +13 -0
  69. package/src/input-text.js +123 -0
  70. package/src/launcher.js +129 -0
  71. package/src/logging/api-log.js +95 -0
  72. package/src/logging/chat-log-follower.js +113 -0
  73. package/src/logging/chat-log.js +15 -0
  74. package/src/logging/log-collector.js +182 -0
  75. package/src/logging/logger.js +112 -0
  76. package/src/logging/tool-log.js +20 -0
  77. package/src/mcp-client.js +314 -0
  78. package/src/memory/conversation-digest.js +113 -0
  79. package/src/memory/extract-facts.js +98 -0
  80. package/src/memory/facts.js +181 -0
  81. package/src/memory/inbox.js +63 -0
  82. package/src/memory/markdown.js +38 -0
  83. package/src/memory/patterns.js +185 -0
  84. package/src/memory/project.js +66 -0
  85. package/src/memory/reflections.js +74 -0
  86. package/src/memory/retrieval.js +84 -0
  87. package/src/memory/rules.js +105 -0
  88. package/src/memory/session-facts.js +125 -0
  89. package/src/memory/skills.js +191 -0
  90. package/src/memory/sqlite-store.js +653 -0
  91. package/src/memory/store.js +208 -0
  92. package/src/memory/tools.js +196 -0
  93. package/src/memory/user-model.js +86 -0
  94. package/src/message-handler.js +775 -0
  95. package/src/model-check.js +218 -0
  96. package/src/plugins/loader.js +120 -0
  97. package/src/plugins/manager.js +88 -0
  98. package/src/production-env.js +22 -0
  99. package/src/profiles.js +42 -0
  100. package/src/providers/adapters/anthropic.js +270 -0
  101. package/src/providers/adapters/openai.js +120 -0
  102. package/src/providers/keys-dpapi.js +41 -0
  103. package/src/providers/keys-fallback.js +31 -0
  104. package/src/providers/keys.js +132 -0
  105. package/src/providers/models.js +154 -0
  106. package/src/providers/registry.js +56 -0
  107. package/src/providers/state.js +56 -0
  108. package/src/registry.js +96 -0
  109. package/src/restart.js +29 -0
  110. package/src/sandbox/backend.js +130 -0
  111. package/src/security/api-auth.js +132 -0
  112. package/src/security/audit.js +98 -0
  113. package/src/security/child-policy.js +41 -0
  114. package/src/security/command-guard.js +173 -0
  115. package/src/security/content-fence.js +250 -0
  116. package/src/security/content-validator.js +132 -0
  117. package/src/security/index.js +143 -0
  118. package/src/security/network-guard.js +126 -0
  119. package/src/security/pairing.js +180 -0
  120. package/src/security/path-guard.js +140 -0
  121. package/src/security/persona-guard.js +67 -0
  122. package/src/security/policies.js +452 -0
  123. package/src/security/safety-constants.js +34 -0
  124. package/src/security/watchdog.js +107 -0
  125. package/src/sessions.js +130 -0
  126. package/src/spend.js +97 -0
  127. package/src/startup-watchdog.js +59 -0
  128. package/src/stdio/args.js +71 -0
  129. package/src/stdio/guard.js +59 -0
  130. package/src/stdio/protocol.js +167 -0
  131. package/src/stdio/run.js +106 -0
  132. package/src/stdio/session.js +180 -0
  133. package/src/store/agent-slice.js +306 -0
  134. package/src/store/dataset-slice.js +73 -0
  135. package/src/store/index.js +22 -0
  136. package/src/store/process-slice.js +135 -0
  137. package/src/store/session-slice.js +191 -0
  138. package/src/store/ui-slice.js +119 -0
  139. package/src/tasks/db.js +184 -0
  140. package/src/tasks/queries.js +589 -0
  141. package/src/tools/agent-tools.js +473 -0
  142. package/src/tools/checkpoint.js +152 -0
  143. package/src/tools/command-approvals.js +180 -0
  144. package/src/tools/dataset.js +50 -0
  145. package/src/tools/filesystem.js +682 -0
  146. package/src/tools/inbox-tools.js +48 -0
  147. package/src/tools/mesh.js +135 -0
  148. package/src/tools/own-env.js +136 -0
  149. package/src/tools/permissions.js +681 -0
  150. package/src/tools/plugin-tools.js +123 -0
  151. package/src/tools/process-tools.js +595 -0
  152. package/src/tools/registry.js +307 -0
  153. package/src/tools/swap-tools.js +72 -0
  154. package/src/tools/system.js +662 -0
  155. package/src/tools/tasks.js +532 -0
  156. package/src/tools/tool-search.js +171 -0
  157. package/src/ui/header.js +140 -0
  158. package/src/ui/input-cursor.js +23 -0
  159. package/src/ui/last-line.js +25 -0
  160. package/src/ui/line-edit.js +135 -0
  161. package/src/ui/output.js +399 -0
  162. package/src/ui/paste-tokens.js +131 -0
  163. package/src/ui/prompt-attention.js +134 -0
  164. package/src/ui/render-options.js +13 -0
  165. package/src/ui/replay.js +94 -0
  166. package/src/ui/splash.js +49 -0
  167. package/src/ui/status-level.js +36 -0
  168. package/src/ui/tool-ledger.js +203 -0
  169. package/src/ui/window-title.js +150 -0
  170. package/src/update.js +205 -0
  171. package/system.md +63 -0
@@ -0,0 +1,653 @@
1
+ // Unified SQLite memory store — all memory layers in one file.
2
+ //
3
+ // Layers: reflections (L1), patterns (L2), skills (L3), facts (L4), user_model (L5).
4
+ // Migration: reads existing JSONL on first run, inserts into tables, renames JSONL to .migrated.
5
+ //
6
+
7
+ import Database from "better-sqlite3";
8
+ import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import { createRequire } from "node:module";
12
+ const require = createRequire(import.meta.url);
13
+
14
+ // FLINT_DATA_DIR moves the memory database off the shared default, so a bench
15
+ // subject does not read and write the operator's own memory. On 2026-09-21 the
16
+ // readiness subject and the live agent shared this file, and so did the unit
17
+ // test that clears the facts table in beforeEach.
18
+ const DIR = process.env.FLINT_DATA_DIR
19
+ ? join(resolve(process.env.FLINT_DATA_DIR), "memory")
20
+ : join(homedir(), ".flint", "memory");
21
+ const DB_PATH = join(DIR, "index.sqlite");
22
+
23
+ let _db = null;
24
+ let _vecEnabled = false;
25
+ const EMBEDDING_PROVIDER = process.env.AGENT_MEMORY_EMBEDDING || "none";
26
+ const EMBEDDING_DIM = 384; // nomic-embed default; OpenAI ada = 1536
27
+
28
+ function ensureDir() {
29
+ if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true });
30
+ }
31
+
32
+ /** Where this instance keeps its memory. Answers "which database am I on?". */
33
+ export function memoryDbPath() {
34
+ return DB_PATH;
35
+ }
36
+
37
+ export function getDb() {
38
+ if (_db) return _db;
39
+ // Guard: under a test runner, refuse to touch the real home database.
40
+ // FLINT_DATA_DIR must be set to a temp directory by the test config.
41
+ if (process.env.VITEST === "true" && !process.env.FLINT_DATA_DIR) {
42
+ throw new Error(
43
+ "Refusing to open real ~/.flint database under test runner. " +
44
+ "Set FLINT_DATA_DIR to a temp directory in your vitest config."
45
+ );
46
+ }
47
+ ensureDir();
48
+ _db = new Database(DB_PATH);
49
+ _db.pragma("journal_mode = WAL");
50
+ _db.pragma("foreign_keys = ON");
51
+
52
+ // Try loading sqlite-vec extension for vector search
53
+ if (EMBEDDING_PROVIDER !== "none") {
54
+ try {
55
+ const { load } = require("sqlite-vec");
56
+ load(_db);
57
+ _vecEnabled = true;
58
+ } catch {
59
+ // sqlite-vec not available — fall back to FTS5 only
60
+ }
61
+ }
62
+
63
+ initSchema(_db);
64
+ return _db;
65
+ }
66
+
67
+ function initSchema(db) {
68
+ db.exec(`
69
+ CREATE TABLE IF NOT EXISTS reflections (
70
+ id TEXT PRIMARY KEY,
71
+ ts TEXT NOT NULL,
72
+ session_id TEXT DEFAULT '',
73
+ did TEXT DEFAULT '',
74
+ wrong TEXT DEFAULT '[]',
75
+ better TEXT DEFAULT '[]',
76
+ tags TEXT DEFAULT '[]',
77
+ created_at TEXT DEFAULT (datetime('now'))
78
+ );
79
+
80
+ CREATE TABLE IF NOT EXISTS patterns (
81
+ id TEXT PRIMARY KEY,
82
+ ts TEXT NOT NULL,
83
+ session_id TEXT DEFAULT '',
84
+ request TEXT DEFAULT '',
85
+ tokens TEXT DEFAULT '[]',
86
+ first_tool TEXT DEFAULT '',
87
+ all_tools TEXT DEFAULT '[]',
88
+ ok INTEGER DEFAULT 1,
89
+ created_at TEXT DEFAULT (datetime('now'))
90
+ );
91
+
92
+ CREATE TABLE IF NOT EXISTS skills (
93
+ id TEXT PRIMARY KEY,
94
+ name TEXT NOT NULL,
95
+ content TEXT DEFAULT '',
96
+ source_file TEXT DEFAULT '',
97
+ tags TEXT DEFAULT '[]',
98
+ created_at TEXT DEFAULT (datetime('now')),
99
+ updated_at TEXT DEFAULT (datetime('now'))
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS facts (
103
+ id TEXT PRIMARY KEY,
104
+ category TEXT DEFAULT 'general',
105
+ content TEXT NOT NULL,
106
+ source TEXT DEFAULT 'agent',
107
+ confidence REAL DEFAULT 1.0,
108
+ created_at TEXT DEFAULT (datetime('now'))
109
+ );
110
+
111
+ CREATE TABLE IF NOT EXISTS user_model (
112
+ id TEXT PRIMARY KEY,
113
+ trait TEXT NOT NULL,
114
+ value TEXT DEFAULT '',
115
+ evidence TEXT DEFAULT '',
116
+ confidence REAL DEFAULT 0.5,
117
+ updated_at TEXT DEFAULT (datetime('now'))
118
+ );
119
+
120
+ -- Migration: add success tracking columns to patterns (Phase 2)
121
+ -- ALTER TABLE is idempotent via pragma check below.
122
+
123
+ -- FTS5 full-text search across all text-heavy layers.
124
+ -- Stores own copy of text for direct retrieval without JOINs.
125
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
126
+ layer,
127
+ entry_id,
128
+ text,
129
+ tokenize='unicode61 remove_diacritics 2'
130
+ );
131
+ `);
132
+
133
+ // Migration: add success tracking to patterns
134
+ const cols = db.pragma("table_info(patterns)").map(c => c.name);
135
+ if (!cols.includes("success_count")) {
136
+ db.exec(`
137
+ ALTER TABLE patterns ADD COLUMN success_count INTEGER DEFAULT 1;
138
+ ALTER TABLE patterns ADD COLUMN fail_count INTEGER DEFAULT 0;
139
+ ALTER TABLE patterns ADD COLUMN version INTEGER DEFAULT 1;
140
+ `);
141
+ }
142
+
143
+ // Migration: add project scope to facts
144
+ // NULL project = global (about the user). Set project = "flint" etc for scoped.
145
+ const factCols = db.pragma("table_info(facts)").map(c => c.name);
146
+ if (!factCols.includes("project")) {
147
+ db.exec(`ALTER TABLE facts ADD COLUMN project TEXT`);
148
+ }
149
+
150
+ // Migration: add pinned flag to facts (2026-04-20).
151
+ // pinned=1 means the fact is never LRU-evicted. Defends curated knowledge
152
+ // (user preferences, project metadata) from being pushed out by transient
153
+ // session artifacts during heavy benchmark/workflow runs.
154
+ if (!factCols.includes("pinned")) {
155
+ db.exec(`ALTER TABLE facts ADD COLUMN pinned INTEGER DEFAULT 0`);
156
+ }
157
+ }
158
+
159
+ // -- Reflections (L1) --
160
+
161
+ export function insertReflection(r) {
162
+ const db = getDb();
163
+ db.prepare(`
164
+ INSERT OR REPLACE INTO reflections (id, ts, session_id, did, wrong, better, tags)
165
+ VALUES (?, ?, ?, ?, ?, ?, ?)
166
+ `).run(
167
+ r.id, r.ts, r.session_id || "",
168
+ r.did || "",
169
+ JSON.stringify(r.wrong || []),
170
+ JSON.stringify(r.better || []),
171
+ JSON.stringify(r.tags || [])
172
+ );
173
+ // Auto-index for FTS5 search
174
+ const ftsText = [r.did, ...(r.wrong || []), ...(r.better || [])].filter(Boolean).join(" ");
175
+ indexEntry("reflections", r.id, ftsText);
176
+ return r.id;
177
+ }
178
+
179
+ export function getRecentReflections(n = 5) {
180
+ const db = getDb();
181
+ return db.prepare("SELECT * FROM reflections ORDER BY ts DESC, rowid DESC LIMIT ?").all(n).map(deserializeReflection);
182
+ }
183
+
184
+ export function getAllReflections() {
185
+ return getDb().prepare("SELECT * FROM reflections ORDER BY ts ASC").all().map(deserializeReflection);
186
+ }
187
+
188
+ export function countReflections() {
189
+ return getDb().prepare("SELECT COUNT(*) as c FROM reflections").get().c;
190
+ }
191
+
192
+ function deserializeReflection(row) {
193
+ return {
194
+ id: row.id, ts: row.ts, session_id: row.session_id,
195
+ did: row.did,
196
+ wrong: safeParse(row.wrong, []),
197
+ better: safeParse(row.better, []),
198
+ tags: safeParse(row.tags, []),
199
+ };
200
+ }
201
+
202
+ // -- Patterns (L2) --
203
+
204
+ export function insertPattern(p) {
205
+ const db = getDb();
206
+ db.prepare(`
207
+ INSERT OR REPLACE INTO patterns (id, ts, session_id, request, tokens, first_tool, all_tools, ok, success_count, fail_count, version)
208
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
209
+ `).run(
210
+ p.id, p.ts, p.session_id || "",
211
+ p.request || "",
212
+ JSON.stringify(p.tokens || []),
213
+ p.first_tool || "",
214
+ JSON.stringify(p.all_tools || []),
215
+ p.ok !== false ? 1 : 0,
216
+ p.success_count ?? 1,
217
+ p.fail_count ?? 0,
218
+ p.version ?? 1
219
+ );
220
+ indexEntry("patterns", p.id, [p.request || "", p.first_tool || ""].join(" "));
221
+ }
222
+
223
+ /**
224
+ * Increment success_count for an existing pattern.
225
+ */
226
+ export function incrementPatternSuccess(id) {
227
+ getDb().prepare("UPDATE patterns SET success_count = success_count + 1, ts = datetime('now') WHERE id = ?").run(id);
228
+ }
229
+
230
+ /**
231
+ * Increment fail_count for an existing pattern.
232
+ */
233
+ export function incrementPatternFail(id) {
234
+ getDb().prepare("UPDATE patterns SET fail_count = fail_count + 1 WHERE id = ?").run(id);
235
+ }
236
+
237
+ /**
238
+ * Find a pattern with overlapping tokens and same first_tool (for dedup/versioning).
239
+ * Returns the most recent match or null.
240
+ */
241
+ export function findSimilarPattern(tokens, firstTool) {
242
+ if (!tokens?.length) return null;
243
+ const db = getDb();
244
+ const all = db.prepare("SELECT * FROM patterns ORDER BY ts DESC").all();
245
+ for (const row of all) {
246
+ const rowTokens = safeParse(row.tokens, []);
247
+ if (row.first_tool !== firstTool) continue;
248
+ // Jaccard similarity >= 0.5
249
+ const intersection = tokens.filter(t => rowTokens.includes(t)).length;
250
+ const union = new Set([...tokens, ...rowTokens]).size;
251
+ if (union > 0 && intersection / union >= 0.5) {
252
+ return deserializePattern(row);
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+
258
+ /**
259
+ * Replace a pattern's tool choice (versioning on correction).
260
+ */
261
+ export function replacePatternTool(id, newTool, newAllTools) {
262
+ getDb().prepare(`
263
+ UPDATE patterns SET first_tool = ?, all_tools = ?, version = version + 1, fail_count = fail_count + 1, ts = datetime('now')
264
+ WHERE id = ?
265
+ `).run(newTool, JSON.stringify(newAllTools || [newTool]), id);
266
+ }
267
+
268
+ export function getAllPatterns() {
269
+ return getDb().prepare("SELECT * FROM patterns ORDER BY ts ASC").all().map(deserializePattern);
270
+ }
271
+
272
+ /**
273
+ * Get top patterns by success rate, for session-stable prompt injection.
274
+ */
275
+ export function getTopPatterns(limit = 20) {
276
+ return getDb().prepare(`
277
+ SELECT *, CAST(success_count AS REAL) / MAX(success_count + fail_count, 1) AS success_rate
278
+ FROM patterns
279
+ WHERE success_count > 0
280
+ ORDER BY success_rate DESC, success_count DESC
281
+ LIMIT ?
282
+ `).all(limit).map(deserializePattern);
283
+ }
284
+
285
+ export function countPatterns() {
286
+ return getDb().prepare("SELECT COUNT(*) as c FROM patterns").get().c;
287
+ }
288
+
289
+ function deserializePattern(row) {
290
+ return {
291
+ id: row.id, ts: row.ts, session_id: row.session_id,
292
+ request: row.request,
293
+ tokens: safeParse(row.tokens, []),
294
+ first_tool: row.first_tool,
295
+ all_tools: safeParse(row.all_tools, []),
296
+ ok: row.ok !== 0,
297
+ success_count: row.success_count ?? 1,
298
+ fail_count: row.fail_count ?? 0,
299
+ version: row.version ?? 1,
300
+ };
301
+ }
302
+
303
+ // -- Skills (L3) --
304
+
305
+ export function insertSkill(s) {
306
+ getDb().prepare(`
307
+ INSERT OR REPLACE INTO skills (id, name, content, source_file, tags, updated_at)
308
+ VALUES (?, ?, ?, ?, ?, datetime('now'))
309
+ `).run(s.id, s.name, s.content || "", s.source_file || "", JSON.stringify(s.tags || []));
310
+ }
311
+
312
+ export function getAllSkills() {
313
+ return getDb().prepare("SELECT * FROM skills ORDER BY name").all().map(r => ({
314
+ ...r, tags: safeParse(r.tags, [])
315
+ }));
316
+ }
317
+
318
+ export function getSkill(id) {
319
+ const row = getDb().prepare("SELECT * FROM skills WHERE id = ?").get(id);
320
+ return row ? { ...row, tags: safeParse(row.tags, []) } : null;
321
+ }
322
+
323
+ export function deleteSkill(id) {
324
+ getDb().prepare("DELETE FROM skills WHERE id = ?").run(id);
325
+ }
326
+
327
+ // -- Facts (L4) --
328
+
329
+ export function insertFact(f) {
330
+ getDb().prepare(`
331
+ INSERT OR REPLACE INTO facts (id, category, content, source, confidence, project, pinned)
332
+ VALUES (?, ?, ?, ?, ?, ?, ?)
333
+ `).run(f.id, f.category || "general", f.content, f.source || "agent", f.confidence ?? 1.0, f.project || null, f.pinned ? 1 : 0);
334
+ }
335
+
336
+ /**
337
+ * Mark fact as pinned (never LRU-evicted). Used for curated knowledge.
338
+ */
339
+ export function setFactPinned(id, pinned) {
340
+ getDb().prepare("UPDATE facts SET pinned = ? WHERE id = ?").run(pinned ? 1 : 0, id);
341
+ }
342
+
343
+ /**
344
+ * Get all facts, optionally filtered by project.
345
+ *
346
+ * @param {object} [opts]
347
+ * @param {string|null} [opts.project] — project name; NULL or undefined = all scopes;
348
+ * empty string "" = global only; "flint" = flint + global (implicit union).
349
+ */
350
+ export function getAllFacts(opts = {}) {
351
+ const db = getDb();
352
+ const p = opts.project;
353
+ if (p === undefined || p === null) {
354
+ // All facts (no scope filter)
355
+ return db.prepare("SELECT * FROM facts ORDER BY created_at DESC").all();
356
+ }
357
+ if (p === "") {
358
+ // Global only — project IS NULL
359
+ return db.prepare("SELECT * FROM facts WHERE project IS NULL ORDER BY created_at DESC").all();
360
+ }
361
+ // Named project — include scoped + global (two-bucket union)
362
+ return db.prepare("SELECT * FROM facts WHERE project = ? OR project IS NULL ORDER BY created_at DESC").all(p);
363
+ }
364
+
365
+ export function deleteFact(id) {
366
+ getDb().prepare("DELETE FROM facts WHERE id = ?").run(id);
367
+ }
368
+
369
+ // -- User Model (L5) --
370
+
371
+ export function upsertTrait(trait, value, evidence, confidence) {
372
+ const id = "trait-" + trait.toLowerCase().replace(/\s+/g, "-").slice(0, 50);
373
+ getDb().prepare(`
374
+ INSERT INTO user_model (id, trait, value, evidence, confidence, updated_at)
375
+ VALUES (?, ?, ?, ?, ?, datetime('now'))
376
+ ON CONFLICT(id) DO UPDATE SET value=?, evidence=?, confidence=?, updated_at=datetime('now')
377
+ `).run(id, trait, value, evidence, confidence, value, evidence, confidence);
378
+ }
379
+
380
+ export function getAllTraits() {
381
+ return getDb().prepare("SELECT * FROM user_model ORDER BY confidence DESC").all();
382
+ }
383
+
384
+ // -- Migration from JSONL --
385
+
386
+ export function migrateFromJsonl() {
387
+ const db = getDb();
388
+ let migrated = { reflections: 0, patterns: 0 };
389
+
390
+ const reflFile = join(DIR, "reflections.jsonl");
391
+ if (existsSync(reflFile) && countReflections() === 0) {
392
+ const lines = readFileSync(reflFile, "utf-8").split("\n").filter(l => l.trim());
393
+ const insert = db.prepare(`
394
+ INSERT OR IGNORE INTO reflections (id, ts, session_id, did, wrong, better, tags)
395
+ VALUES (?, ?, ?, ?, ?, ?, ?)
396
+ `);
397
+ db.transaction(() => {
398
+ for (const line of lines) {
399
+ try {
400
+ const r = JSON.parse(line);
401
+ insert.run(r.id, r.ts, r.session_id || "", r.did || "",
402
+ JSON.stringify(r.wrong || []), JSON.stringify(r.better || []), JSON.stringify(r.tags || []));
403
+ migrated.reflections++;
404
+ } catch {}
405
+ }
406
+ })();
407
+ renameSync(reflFile, reflFile + ".migrated");
408
+ }
409
+
410
+ const patFile = join(DIR, "patterns.jsonl");
411
+ if (existsSync(patFile) && countPatterns() === 0) {
412
+ const lines = readFileSync(patFile, "utf-8").split("\n").filter(l => l.trim());
413
+ const insert = db.prepare(`
414
+ INSERT OR IGNORE INTO patterns (id, ts, session_id, request, tokens, first_tool, all_tools, ok)
415
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
416
+ `);
417
+ db.transaction(() => {
418
+ for (const line of lines) {
419
+ try {
420
+ const p = JSON.parse(line);
421
+ insert.run(p.id, p.ts, p.session_id || "", p.request || "",
422
+ JSON.stringify(p.tokens || []), p.first_tool || "", JSON.stringify(p.all_tools || []),
423
+ p.ok !== false ? 1 : 0);
424
+ migrated.patterns++;
425
+ } catch {}
426
+ }
427
+ })();
428
+ renameSync(patFile, patFile + ".migrated");
429
+ }
430
+
431
+ return migrated;
432
+ }
433
+
434
+ // -- FTS5 Full-Text Search --
435
+
436
+ /**
437
+ * Index a memory entry into FTS5 for keyword search.
438
+ * Call after every insert/update to keep index fresh.
439
+ * @param {string} layer — "reflections"|"patterns"|"skills"|"facts"|"user_model"
440
+ * @param {string} entryId — row id in the source table
441
+ * @param {string} text — searchable text content
442
+ */
443
+ export function indexEntry(layer, entryId, text) {
444
+ const db = getDb();
445
+ // Remove old entry first (external-content FTS needs manual sync)
446
+ db.prepare("DELETE FROM memory_fts WHERE entry_id = ? AND layer = ?").run(entryId, layer);
447
+ if (text && text.trim()) {
448
+ db.prepare("INSERT INTO memory_fts (layer, entry_id, text) VALUES (?, ?, ?)").run(layer, entryId, text);
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Search across all memory layers via FTS5.
454
+ * Returns matching entries ranked by BM25 relevance.
455
+ * @param {string} query — user search terms
456
+ * @param {object} opts
457
+ * @param {number} opts.limit — max results (default 10)
458
+ * @param {string} opts.layer — filter to specific layer (optional)
459
+ * @returns {Array<{layer, entry_id, text, rank}>}
460
+ */
461
+ export function searchFts(query, opts = {}) {
462
+ const db = getDb();
463
+ const limit = opts.limit || 10;
464
+ if (!query || !query.trim()) return [];
465
+
466
+ // Escape FTS5 special chars and build query
467
+ const cleaned = query.replace(/['"(){}[\]*:^~!@#$%&]/g, " ").trim();
468
+ if (!cleaned) return [];
469
+
470
+ try {
471
+ if (opts.layer) {
472
+ return db.prepare(`
473
+ SELECT layer, entry_id, text, rank
474
+ FROM memory_fts
475
+ WHERE memory_fts MATCH ? AND layer = ?
476
+ ORDER BY rank
477
+ LIMIT ?
478
+ `).all(cleaned, opts.layer, limit);
479
+ }
480
+ return db.prepare(`
481
+ SELECT layer, entry_id, text, rank
482
+ FROM memory_fts
483
+ WHERE memory_fts MATCH ?
484
+ ORDER BY rank
485
+ LIMIT ?
486
+ `).all(cleaned, limit);
487
+ } catch {
488
+ return [];
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Rebuild the FTS5 index from all source tables.
494
+ * Call after migration or if index seems corrupt.
495
+ */
496
+ export function rebuildFtsIndex() {
497
+ const db = getDb();
498
+ // Clear existing index
499
+ db.prepare("DELETE FROM memory_fts").run();
500
+
501
+ // Index reflections: combine did + wrong + better
502
+ const reflections = db.prepare("SELECT id, did, wrong, better FROM reflections").all();
503
+ const insertFts = db.prepare("INSERT INTO memory_fts (layer, entry_id, text) VALUES (?, ?, ?)");
504
+ db.transaction(() => {
505
+ for (const r of reflections) {
506
+ const parts = [r.did, ...safeParse(r.wrong, []), ...safeParse(r.better, [])].filter(Boolean);
507
+ if (parts.length) insertFts.run("reflections", r.id, parts.join(" "));
508
+ }
509
+ // Index patterns: request text
510
+ const patterns = db.prepare("SELECT id, request FROM patterns").all();
511
+ for (const p of patterns) {
512
+ if (p.request) insertFts.run("patterns", p.id, p.request);
513
+ }
514
+ // Index skills: name + content
515
+ const skills = db.prepare("SELECT id, name, content FROM skills").all();
516
+ for (const s of skills) {
517
+ insertFts.run("skills", s.id, [s.name, s.content].filter(Boolean).join(" "));
518
+ }
519
+ // Index facts: content
520
+ const facts = db.prepare("SELECT id, content FROM facts").all();
521
+ for (const f of facts) {
522
+ if (f.content) insertFts.run("facts", f.id, f.content);
523
+ }
524
+ // Index user_model: trait + value
525
+ const traits = db.prepare("SELECT id, trait, value FROM user_model").all();
526
+ for (const t of traits) {
527
+ insertFts.run("user_model", t.id, [t.trait, t.value].filter(Boolean).join(" "));
528
+ }
529
+ })();
530
+ return { indexed: reflections.length + db.prepare("SELECT COUNT(*) as c FROM patterns").get().c };
531
+ }
532
+
533
+ // -- Helpers --
534
+
535
+ function safeParse(str, fallback) {
536
+ try { return JSON.parse(str); } catch { return fallback; }
537
+ }
538
+
539
+ // -- Vector Search (sqlite-vec) --
540
+
541
+ /**
542
+ * Check if vector search is available.
543
+ */
544
+ export function isVecEnabled() {
545
+ return _vecEnabled;
546
+ }
547
+
548
+ /**
549
+ * Store a vector embedding for a memory entry.
550
+ * @param {string} layer
551
+ * @param {string} entryId
552
+ * @param {Float32Array|number[]} embedding
553
+ */
554
+ export function storeEmbedding(layer, entryId, embedding) {
555
+ if (!_vecEnabled) return;
556
+ const db = getDb();
557
+ try {
558
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(
559
+ layer TEXT,
560
+ entry_id TEXT,
561
+ embedding float[${EMBEDDING_DIM}]
562
+ )`);
563
+ } catch {} // table may already exist
564
+ // Remove old embedding
565
+ try { db.prepare("DELETE FROM memory_vec WHERE entry_id = ? AND layer = ?").run(entryId, layer); } catch {}
566
+ try {
567
+ const buf = Buffer.from(new Float32Array(embedding).buffer);
568
+ db.prepare("INSERT INTO memory_vec (layer, entry_id, embedding) VALUES (?, ?, ?)").run(layer, entryId, buf);
569
+ } catch {}
570
+ }
571
+
572
+ /**
573
+ * Semantic vector search across memory.
574
+ * @param {Float32Array|number[]} queryEmbedding
575
+ * @param {object} opts — { limit, layer }
576
+ * @returns {Array<{layer, entry_id, distance}>}
577
+ */
578
+ export function searchVec(queryEmbedding, opts = {}) {
579
+ if (!_vecEnabled) return [];
580
+ const db = getDb();
581
+ const limit = opts.limit || 10;
582
+ const buf = Buffer.from(new Float32Array(queryEmbedding).buffer);
583
+ try {
584
+ if (opts.layer) {
585
+ return db.prepare(`
586
+ SELECT layer, entry_id, distance
587
+ FROM memory_vec
588
+ WHERE embedding MATCH ? AND layer = ?
589
+ ORDER BY distance
590
+ LIMIT ?
591
+ `).all(buf, opts.layer, limit);
592
+ }
593
+ return db.prepare(`
594
+ SELECT layer, entry_id, distance
595
+ FROM memory_vec
596
+ WHERE embedding MATCH ?
597
+ ORDER BY distance
598
+ LIMIT ?
599
+ `).all(buf, limit);
600
+ } catch {
601
+ return [];
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Hybrid search: combine FTS5 (BM25) + vector scores.
607
+ * Returns unified results ranked by combined score.
608
+ * @param {string} query — text query for FTS
609
+ * @param {Float32Array|number[]} queryEmbedding — vector for semantic search
610
+ * @param {object} opts
611
+ * @returns {Array<{layer, entry_id, text, score}>}
612
+ */
613
+ export function searchHybrid(query, queryEmbedding, opts = {}) {
614
+ const limit = opts.limit || 10;
615
+ const ftsResults = searchFts(query, { limit: limit * 2, layer: opts.layer });
616
+ const vecResults = _vecEnabled && queryEmbedding
617
+ ? searchVec(queryEmbedding, { limit: limit * 2, layer: opts.layer })
618
+ : [];
619
+
620
+ // Merge by entry_id, combine scores
621
+ const scores = new Map();
622
+ for (const r of ftsResults) {
623
+ const key = `${r.layer}:${r.entry_id}`;
624
+ scores.set(key, { ...r, ftsRank: Math.abs(r.rank || 0), vecDist: Infinity, score: 0 });
625
+ }
626
+ for (const r of vecResults) {
627
+ const key = `${r.layer}:${r.entry_id}`;
628
+ if (scores.has(key)) {
629
+ scores.get(key).vecDist = r.distance;
630
+ } else {
631
+ scores.set(key, { layer: r.layer, entry_id: r.entry_id, text: "", ftsRank: 0, vecDist: r.distance, score: 0 });
632
+ }
633
+ }
634
+
635
+ // Score: normalize and combine (0.6 FTS + 0.4 vector)
636
+ const maxFts = Math.max(...[...scores.values()].map(s => s.ftsRank), 1);
637
+ const maxVec = Math.max(...[...scores.values()].filter(s => s.vecDist < Infinity).map(s => s.vecDist), 1);
638
+ for (const s of scores.values()) {
639
+ const ftsScore = s.ftsRank / maxFts;
640
+ const vecScore = s.vecDist < Infinity ? (1 - s.vecDist / maxVec) : 0;
641
+ s.score = 0.6 * ftsScore + 0.4 * vecScore;
642
+ }
643
+
644
+ return [...scores.values()]
645
+ .sort((a, b) => b.score - a.score)
646
+ .slice(0, limit);
647
+ }
648
+
649
+ export function closeDb() {
650
+ if (_db) { _db.close(); _db = null; _vecEnabled = false; }
651
+ }
652
+
653
+ export const __internal = { DIR, DB_PATH, EMBEDDING_DIM, isVecEnabled: () => _vecEnabled };