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.
- package/.env.example +108 -0
- package/CHANGELOG.md +55 -0
- package/FEATURES.md +298 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/bin/flint.js +47 -0
- package/config/classifier-prompt.md +218 -0
- package/config/models-curated.json +4 -0
- package/config/providers.json +74 -0
- package/package.json +92 -0
- package/patches/ink+6.8.0.patch +78 -0
- package/profiles/desktop.md +65 -0
- package/profiles/generic.md +20 -0
- package/profiles/marketer.md +20 -0
- package/profiles/profiles.json +34 -0
- package/profiles/ux-reviewer.md +25 -0
- package/src/agent/agent.js +1743 -0
- package/src/agent/auto.js +346 -0
- package/src/agent/backoff.js +143 -0
- package/src/agent/compression.js +310 -0
- package/src/agent/content-resolver.js +180 -0
- package/src/agent/flow-controller.js +309 -0
- package/src/agent/intent-manifest.js +231 -0
- package/src/agent/intent-timeout.js +46 -0
- package/src/agent/intent.js +633 -0
- package/src/agent/knowledge.js +114 -0
- package/src/agent/learning.js +180 -0
- package/src/agent/modes.js +187 -0
- package/src/agent/outcome-ask.js +91 -0
- package/src/agent/project-context.js +76 -0
- package/src/agent/prompt-budget.js +117 -0
- package/src/agent/reflection-extractor.js +140 -0
- package/src/agent/steering.js +86 -0
- package/src/agent/supervisor.js +430 -0
- package/src/agent/swap.js +443 -0
- package/src/agent/system-prompt.js +446 -0
- package/src/agent/time-stamp.js +48 -0
- package/src/agent/tool-guard.js +201 -0
- package/src/agent/toolcall-text.js +162 -0
- package/src/agent/usage.js +297 -0
- package/src/agent/vision.js +94 -0
- package/src/agent/watchdog.js +139 -0
- package/src/agent/workspace-changes.js +177 -0
- package/src/api/address.js +14 -0
- package/src/api/client.js +280 -0
- package/src/api/server.js +535 -0
- package/src/api/stream-pipe.js +113 -0
- package/src/app-state.js +39 -0
- package/src/bootstrap.js +501 -0
- package/src/bus/drain-loop.js +497 -0
- package/src/bus/index.js +270 -0
- package/src/bus/plugins.js +65 -0
- package/src/child-idle.js +14 -0
- package/src/cli.js +118 -0
- package/src/commands/commands.js +1297 -0
- package/src/commands/registry.js +132 -0
- package/src/components/App.js +491 -0
- package/src/components/CarefulMenu.js +145 -0
- package/src/components/HistoryWriter.js +86 -0
- package/src/components/LineInput.js +69 -0
- package/src/components/LiveZone.js +294 -0
- package/src/components/OverlayMenu.js +179 -0
- package/src/components/SystemPanel.js +156 -0
- package/src/components/Table.js +54 -0
- package/src/config.js +249 -0
- package/src/free-models.js +230 -0
- package/src/index.js +1111 -0
- package/src/input-handler.js +13 -0
- package/src/input-text.js +123 -0
- package/src/launcher.js +129 -0
- package/src/logging/api-log.js +95 -0
- package/src/logging/chat-log-follower.js +113 -0
- package/src/logging/chat-log.js +15 -0
- package/src/logging/log-collector.js +182 -0
- package/src/logging/logger.js +112 -0
- package/src/logging/tool-log.js +20 -0
- package/src/mcp-client.js +314 -0
- package/src/memory/conversation-digest.js +113 -0
- package/src/memory/extract-facts.js +98 -0
- package/src/memory/facts.js +181 -0
- package/src/memory/inbox.js +63 -0
- package/src/memory/markdown.js +38 -0
- package/src/memory/patterns.js +185 -0
- package/src/memory/project.js +66 -0
- package/src/memory/reflections.js +74 -0
- package/src/memory/retrieval.js +84 -0
- package/src/memory/rules.js +105 -0
- package/src/memory/session-facts.js +125 -0
- package/src/memory/skills.js +191 -0
- package/src/memory/sqlite-store.js +653 -0
- package/src/memory/store.js +208 -0
- package/src/memory/tools.js +196 -0
- package/src/memory/user-model.js +86 -0
- package/src/message-handler.js +775 -0
- package/src/model-check.js +218 -0
- package/src/plugins/loader.js +120 -0
- package/src/plugins/manager.js +88 -0
- package/src/production-env.js +22 -0
- package/src/profiles.js +42 -0
- package/src/providers/adapters/anthropic.js +270 -0
- package/src/providers/adapters/openai.js +120 -0
- package/src/providers/keys-dpapi.js +41 -0
- package/src/providers/keys-fallback.js +31 -0
- package/src/providers/keys.js +132 -0
- package/src/providers/models.js +154 -0
- package/src/providers/registry.js +56 -0
- package/src/providers/state.js +56 -0
- package/src/registry.js +96 -0
- package/src/restart.js +29 -0
- package/src/sandbox/backend.js +130 -0
- package/src/security/api-auth.js +132 -0
- package/src/security/audit.js +98 -0
- package/src/security/child-policy.js +41 -0
- package/src/security/command-guard.js +173 -0
- package/src/security/content-fence.js +250 -0
- package/src/security/content-validator.js +132 -0
- package/src/security/index.js +143 -0
- package/src/security/network-guard.js +126 -0
- package/src/security/pairing.js +180 -0
- package/src/security/path-guard.js +140 -0
- package/src/security/persona-guard.js +67 -0
- package/src/security/policies.js +452 -0
- package/src/security/safety-constants.js +34 -0
- package/src/security/watchdog.js +107 -0
- package/src/sessions.js +130 -0
- package/src/spend.js +97 -0
- package/src/startup-watchdog.js +59 -0
- package/src/stdio/args.js +71 -0
- package/src/stdio/guard.js +59 -0
- package/src/stdio/protocol.js +167 -0
- package/src/stdio/run.js +106 -0
- package/src/stdio/session.js +180 -0
- package/src/store/agent-slice.js +306 -0
- package/src/store/dataset-slice.js +73 -0
- package/src/store/index.js +22 -0
- package/src/store/process-slice.js +135 -0
- package/src/store/session-slice.js +191 -0
- package/src/store/ui-slice.js +119 -0
- package/src/tasks/db.js +184 -0
- package/src/tasks/queries.js +589 -0
- package/src/tools/agent-tools.js +473 -0
- package/src/tools/checkpoint.js +152 -0
- package/src/tools/command-approvals.js +180 -0
- package/src/tools/dataset.js +50 -0
- package/src/tools/filesystem.js +682 -0
- package/src/tools/inbox-tools.js +48 -0
- package/src/tools/mesh.js +135 -0
- package/src/tools/own-env.js +136 -0
- package/src/tools/permissions.js +681 -0
- package/src/tools/plugin-tools.js +123 -0
- package/src/tools/process-tools.js +595 -0
- package/src/tools/registry.js +307 -0
- package/src/tools/swap-tools.js +72 -0
- package/src/tools/system.js +662 -0
- package/src/tools/tasks.js +532 -0
- package/src/tools/tool-search.js +171 -0
- package/src/ui/header.js +140 -0
- package/src/ui/input-cursor.js +23 -0
- package/src/ui/last-line.js +25 -0
- package/src/ui/line-edit.js +135 -0
- package/src/ui/output.js +399 -0
- package/src/ui/paste-tokens.js +131 -0
- package/src/ui/prompt-attention.js +134 -0
- package/src/ui/render-options.js +13 -0
- package/src/ui/replay.js +94 -0
- package/src/ui/splash.js +49 -0
- package/src/ui/status-level.js +36 -0
- package/src/ui/tool-ledger.js +203 -0
- package/src/ui/window-title.js +150 -0
- package/src/update.js +205 -0
- 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 };
|