@clauderecallhq/cli 0.93.0 → 0.95.4
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/README.md +6 -6
- package/dist/cli.js +438 -411
- package/dist/daemon/entrypoint.js +353 -304
- package/dist/daemon/query-worker.js +758 -0
- package/dist/mcp-server.js +134 -115
- package/dist/web/assets/{dist-CZGPayHg.js → dist-C3F6ixrp.js} +3 -3
- package/dist/web/assets/index-Uw_Mu1-d.css +1 -0
- package/dist/web/assets/{index-B6JmLj7M.js → index-emVWWys3.js} +15 -15
- package/dist/web/index.html +2 -2
- package/package.json +5 -4
- package/scripts/postinstall.mjs +1 -1
- package/dist/web/assets/index-Bk28z8Va.css +0 -1
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
/* Claude Recall (proprietary). See LICENSE for terms. */
|
|
2
|
+
var _e=Object.defineProperty;var B=(e,s)=>{for(var t in s)_e(e,t,{get:s[t],enumerable:!0})};import{parentPort as m}from"node:worker_threads";import{createRequire as Ne}from"node:module";{let e=process.emit.bind(process);process.emit=function(s,...t){let r=t[0];return s==="warning"&&r instanceof Error&&r.name==="ExperimentalWarning"&&/SQLite/i.test(r.message)?!1:e(s,...t)}}var pe=Ne(import.meta.url),he=["node","sqlite"].join(":"),Le=pe(he),X=class{inner;constructor(s){this.inner=s}get(...s){return s.length===0?this.inner.get():this.inner.get(...s)}all(...s){return s.length===0?this.inner.all():this.inner.all(...s)}run(...s){let t=s.length===0?this.inner.run():this.inner.run(...s);return{changes:t.changes,lastInsertRowid:t.lastInsertRowid}}iterate(...s){return s.length===0?this.inner.iterate():this.inner.iterate(...s)}},v=class{inner;extensionLoadingEnabled=!1;txDepth=0;constructor(s,t={}){this.inner=new Le.DatabaseSync(s,{readOnly:t.readonly??!1,allowExtension:!0})}prepare(s){return new X(this.inner.prepare(s))}exec(s){this.inner.exec(s)}close(){this.inner.close()}pragma(s,t={}){if(s.includes("=")){this.inner.exec(`PRAGMA ${s}`);return}if(t.simple){let r=this.inner.prepare(`PRAGMA ${s}`).get();return r&&typeof r=="object"?Object.values(r)[0]:void 0}return this.inner.prepare(`PRAGMA ${s}`).all()}transaction(s){return((...r)=>{this.txDepth===0?this.inner.exec("BEGIN"):this.inner.exec(`SAVEPOINT sp_${this.txDepth}`),this.txDepth+=1;try{let o=s(...r);return this.txDepth-=1,this.txDepth===0?this.inner.exec("COMMIT"):this.inner.exec(`RELEASE sp_${this.txDepth}`),o}catch(o){this.txDepth-=1;try{this.txDepth===0?this.inner.exec("ROLLBACK"):(this.inner.exec(`ROLLBACK TO sp_${this.txDepth}`),this.inner.exec(`RELEASE sp_${this.txDepth}`))}catch{}throw o}})}loadExtension(s,t){this.extensionLoadingEnabled||(this.inner.enableLoadExtension(!0),this.extensionLoadingEnabled=!0),t===void 0?this.inner.loadExtension(s):this.inner.loadExtension(s,t)}},Y=v;import{homedir as q}from"node:os";import{join as U,basename as ns}from"node:path";import{existsSync as ge,mkdirSync as Ie,chmodSync as fe,readdirSync as is,statSync as as}from"node:fs";var ds=process.env.CLAUDE_PROJECTS_DIR?process.env.CLAUDE_PROJECTS_DIR:U(q(),".claude","projects"),L=process.env.RECALL_HOME?process.env.RECALL_HOME:U(q(),".recall"),K=U(L,"db.sqlite");function H(){ge(L)||Ie(L,{recursive:!0,mode:448}),process.platform!=="win32"&&fe(L,448)}var W=`
|
|
3
|
+
PRAGMA journal_mode = WAL;
|
|
4
|
+
PRAGMA synchronous = NORMAL;
|
|
5
|
+
PRAGMA foreign_keys = ON;
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
8
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9
|
+
encoded_path TEXT UNIQUE NOT NULL,
|
|
10
|
+
decoded_path TEXT NOT NULL,
|
|
11
|
+
name TEXT NOT NULL,
|
|
12
|
+
repo_root TEXT,
|
|
13
|
+
main_repo TEXT,
|
|
14
|
+
is_repo INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
is_worktree INTEGER NOT NULL DEFAULT 0
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
21
|
+
file_path TEXT NOT NULL,
|
|
22
|
+
file_mtime REAL NOT NULL,
|
|
23
|
+
started_at TEXT,
|
|
24
|
+
ended_at TEXT,
|
|
25
|
+
message_count INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
user_message_count INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
assistant_message_count INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
first_user_message TEXT,
|
|
29
|
+
cwd TEXT,
|
|
30
|
+
git_branch TEXT,
|
|
31
|
+
version TEXT,
|
|
32
|
+
indexed_at TEXT NOT NULL
|
|
33
|
+
);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
38
|
+
rowid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
uuid TEXT UNIQUE NOT NULL,
|
|
40
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
41
|
+
parent_uuid TEXT,
|
|
42
|
+
type TEXT NOT NULL,
|
|
43
|
+
role TEXT,
|
|
44
|
+
timestamp TEXT,
|
|
45
|
+
is_sidechain INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
content_text TEXT,
|
|
47
|
+
tool_names TEXT,
|
|
48
|
+
raw_json TEXT NOT NULL
|
|
49
|
+
);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
|
51
|
+
|
|
52
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
53
|
+
content_text,
|
|
54
|
+
tool_names,
|
|
55
|
+
content='messages',
|
|
56
|
+
content_rowid='rowid',
|
|
57
|
+
tokenize = 'porter unicode61'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
|
|
61
|
+
INSERT INTO messages_fts(rowid, content_text, tool_names)
|
|
62
|
+
VALUES (new.rowid, new.content_text, new.tool_names);
|
|
63
|
+
END;
|
|
64
|
+
|
|
65
|
+
CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
|
|
66
|
+
INSERT INTO messages_fts(messages_fts, rowid, content_text, tool_names)
|
|
67
|
+
VALUES ('delete', old.rowid, old.content_text, old.tool_names);
|
|
68
|
+
END;
|
|
69
|
+
|
|
70
|
+
CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
|
|
71
|
+
INSERT INTO messages_fts(messages_fts, rowid, content_text, tool_names)
|
|
72
|
+
VALUES ('delete', old.rowid, old.content_text, old.tool_names);
|
|
73
|
+
INSERT INTO messages_fts(rowid, content_text, tool_names)
|
|
74
|
+
VALUES (new.rowid, new.content_text, new.tool_names);
|
|
75
|
+
END;
|
|
76
|
+
|
|
77
|
+
-- v0.4.1 \u2014 session aliases. The UUID stays the primary key forever; aliases
|
|
78
|
+
-- are a display layer on top. Every edit archives the prior value into
|
|
79
|
+
-- previous_aliases (JSON array) \u2014 we never destroy history.
|
|
80
|
+
CREATE TABLE IF NOT EXISTS session_aliases (
|
|
81
|
+
session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
|
|
82
|
+
alias TEXT NOT NULL,
|
|
83
|
+
updated_at TEXT NOT NULL,
|
|
84
|
+
previous_aliases TEXT NOT NULL DEFAULT '[]'
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
-- v0.4.2 \u2014 per-session markdown notes.
|
|
88
|
+
-- Every save archives the prior content into previous_versions and also mirrors
|
|
89
|
+
-- the full note out to ~/.recall/notes/<session_id>.md on disk. Nothing is ever
|
|
90
|
+
-- destroyed: an empty string means "cleared" with full history preserved.
|
|
91
|
+
CREATE TABLE IF NOT EXISTS session_notes (
|
|
92
|
+
session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
|
|
93
|
+
content TEXT NOT NULL DEFAULT '',
|
|
94
|
+
updated_at TEXT NOT NULL,
|
|
95
|
+
previous_versions TEXT NOT NULL DEFAULT '[]'
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
-- v0.4.3 \u2014 session tags. Many-to-many. Never destructively delete:
|
|
99
|
+
-- tag_events is an append-only log of every add and remove across all time,
|
|
100
|
+
-- so the full history of what was tagged when and why is always recoverable.
|
|
101
|
+
CREATE TABLE IF NOT EXISTS session_tags (
|
|
102
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
103
|
+
tag TEXT NOT NULL,
|
|
104
|
+
created_at TEXT NOT NULL,
|
|
105
|
+
PRIMARY KEY (session_id, tag)
|
|
106
|
+
);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
|
|
108
|
+
|
|
109
|
+
CREATE TABLE IF NOT EXISTS tag_events (
|
|
110
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
111
|
+
session_id TEXT NOT NULL,
|
|
112
|
+
tag TEXT NOT NULL,
|
|
113
|
+
action TEXT NOT NULL, -- 'add' | 'remove'
|
|
114
|
+
at TEXT NOT NULL
|
|
115
|
+
);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_tag_events_session ON tag_events(session_id, at DESC);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_tag_events_tag ON tag_events(tag, at DESC);
|
|
118
|
+
|
|
119
|
+
-- v0.4.5 \u2014 opt-in clipboard archive. This is the ONE table where a user can
|
|
120
|
+
-- truly purge a row on demand (not soft-delete), because it may contain
|
|
121
|
+
-- accidentally-archived secrets. This carve-out is documented as a deliberate
|
|
122
|
+
-- exception to the "never delete" rule specifically for this data class.
|
|
123
|
+
CREATE TABLE IF NOT EXISTS paste_archives (
|
|
124
|
+
id TEXT PRIMARY KEY,
|
|
125
|
+
created_at TEXT NOT NULL,
|
|
126
|
+
content TEXT NOT NULL,
|
|
127
|
+
size_bytes INTEGER NOT NULL,
|
|
128
|
+
source TEXT NOT NULL DEFAULT 'cli', -- 'cli' | 'cli-piped' | 'ui' | \u2026
|
|
129
|
+
label TEXT -- optional user-supplied short description
|
|
130
|
+
);
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_paste_archives_created ON paste_archives(created_at DESC);
|
|
132
|
+
|
|
133
|
+
-- v0.8 \u2014 collections. User-curated hand-picked groupings of sessions that
|
|
134
|
+
-- cut across the coarse-grained cwd-based "projects". A collection can nest
|
|
135
|
+
-- as a tree (parent_id \u2192 null means root). Soft-deletion only: archived_at
|
|
136
|
+
-- hides rows from default views, the row itself stays forever.
|
|
137
|
+
--
|
|
138
|
+
-- Durability: SQLite (this table + collection_events append-only log) plus a
|
|
139
|
+
-- plain-text mirror at ~/.recall/collections.json rewritten on every mutation.
|
|
140
|
+
CREATE TABLE IF NOT EXISTS collections (
|
|
141
|
+
id TEXT PRIMARY KEY,
|
|
142
|
+
name TEXT NOT NULL,
|
|
143
|
+
description TEXT,
|
|
144
|
+
icon TEXT,
|
|
145
|
+
color TEXT,
|
|
146
|
+
parent_id TEXT REFERENCES collections(id) ON DELETE SET NULL,
|
|
147
|
+
sort_key TEXT NOT NULL DEFAULT '',
|
|
148
|
+
created_at TEXT NOT NULL,
|
|
149
|
+
updated_at TEXT NOT NULL,
|
|
150
|
+
archived_at TEXT
|
|
151
|
+
);
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_collections_parent ON collections(parent_id);
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_collections_archived ON collections(archived_at);
|
|
154
|
+
|
|
155
|
+
CREATE TABLE IF NOT EXISTS collection_sessions (
|
|
156
|
+
collection_id TEXT NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
|
157
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
158
|
+
added_at TEXT NOT NULL,
|
|
159
|
+
note TEXT,
|
|
160
|
+
PRIMARY KEY (collection_id, session_id)
|
|
161
|
+
);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_collection_sessions_session ON collection_sessions(session_id);
|
|
163
|
+
|
|
164
|
+
CREATE TABLE IF NOT EXISTS collection_events (
|
|
165
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
166
|
+
collection_id TEXT NOT NULL,
|
|
167
|
+
session_id TEXT,
|
|
168
|
+
action TEXT NOT NULL, -- 'create' | 'rename' | 'describe' | 'recolor' | 'add' | 'remove' | 'archive' | 'restore' | 'move' | 'reorder'
|
|
169
|
+
payload TEXT, -- JSON context (old_name, new_parent, etc.)
|
|
170
|
+
at TEXT NOT NULL
|
|
171
|
+
);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_collection_events_at ON collection_events(at DESC);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_collection_events_collection ON collection_events(collection_id, at DESC);
|
|
174
|
+
|
|
175
|
+
-- v0.11 \u2014 semantic search (Tier 0: shells out to the user's local claude CLI to
|
|
176
|
+
-- summarize each session into 3-sentence prose + a keyword set). Both columns
|
|
177
|
+
-- are TEXT and feed sessions_fts so a conceptual query can hit this index in
|
|
178
|
+
-- addition to the per-message FTS5 index. Pipeline is OFF by default; users opt
|
|
179
|
+
-- in via "recall semantic on". Plain-text mirror at ~/.recall/semantic/<id>.json.
|
|
180
|
+
CREATE TABLE IF NOT EXISTS session_semantic (
|
|
181
|
+
session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
|
|
182
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
183
|
+
keywords TEXT NOT NULL DEFAULT '', -- comma-separated
|
|
184
|
+
model TEXT,
|
|
185
|
+
source_message_count INTEGER NOT NULL DEFAULT 0,
|
|
186
|
+
generated_at TEXT NOT NULL
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
|
|
190
|
+
summary,
|
|
191
|
+
keywords,
|
|
192
|
+
content='session_semantic',
|
|
193
|
+
content_rowid='rowid',
|
|
194
|
+
tokenize = 'porter unicode61'
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
CREATE TRIGGER IF NOT EXISTS session_semantic_ai AFTER INSERT ON session_semantic BEGIN
|
|
198
|
+
INSERT INTO sessions_fts(rowid, summary, keywords)
|
|
199
|
+
VALUES (new.rowid, new.summary, new.keywords);
|
|
200
|
+
END;
|
|
201
|
+
|
|
202
|
+
CREATE TRIGGER IF NOT EXISTS session_semantic_ad AFTER DELETE ON session_semantic BEGIN
|
|
203
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, summary, keywords)
|
|
204
|
+
VALUES ('delete', old.rowid, old.summary, old.keywords);
|
|
205
|
+
END;
|
|
206
|
+
|
|
207
|
+
CREATE TRIGGER IF NOT EXISTS session_semantic_au AFTER UPDATE ON session_semantic BEGIN
|
|
208
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, summary, keywords)
|
|
209
|
+
VALUES ('delete', old.rowid, old.summary, old.keywords);
|
|
210
|
+
INSERT INTO sessions_fts(rowid, summary, keywords)
|
|
211
|
+
VALUES (new.rowid, new.summary, new.keywords);
|
|
212
|
+
END;
|
|
213
|
+
|
|
214
|
+
-- v0.7 \u2014 vector search tier (Pro-only). Chunks are per-conversational-turn
|
|
215
|
+
-- segments of messages, embedded via local bge-base-en-v1.5 (768d). The vector
|
|
216
|
+
-- data is a derived cache; if lost, it is recomputable from messages.
|
|
217
|
+
-- Tables stay empty on Free tier (no model downloaded, no worker started).
|
|
218
|
+
--
|
|
219
|
+
-- NOTE: the vec_chunks vec0 virtual table is intentionally NOT created here.
|
|
220
|
+
-- It lives in src/db/vecLoader.ts (ensureVecChunksTable), loaded lazily so a
|
|
221
|
+
-- lean MCP child that only does FTS/metadata never instantiates the native
|
|
222
|
+
-- vec0 module. The daemon/worker/CLI call ensureVecChunksTable at open.
|
|
223
|
+
|
|
224
|
+
CREATE TABLE IF NOT EXISTS chunk_meta (
|
|
225
|
+
rowid INTEGER PRIMARY KEY,
|
|
226
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
227
|
+
message_uuids TEXT NOT NULL DEFAULT '[]',
|
|
228
|
+
text TEXT NOT NULL DEFAULT '',
|
|
229
|
+
embedding_model_id TEXT NOT NULL DEFAULT 'bge-base-en-v1.5',
|
|
230
|
+
embedding_dim INTEGER NOT NULL DEFAULT 768,
|
|
231
|
+
stale INTEGER NOT NULL DEFAULT 0,
|
|
232
|
+
generated_at TEXT NOT NULL DEFAULT ''
|
|
233
|
+
);
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_chunk_meta_session ON chunk_meta(session_id);
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_chunk_meta_stale ON chunk_meta(stale) WHERE stale = 1;
|
|
236
|
+
|
|
237
|
+
-- chunk_queue action CHECK constraint expanded in v0.78 to include
|
|
238
|
+
-- 'pending_post_migration'. New databases accept this value; existing
|
|
239
|
+
-- databases retain the older 3-action check (SQLite cannot ALTER a
|
|
240
|
+
-- CHECK constraint in place). The Phase 2 migrate command (src/semantic/
|
|
241
|
+
-- migrate.ts, TBD) pauses the embedder worker before enqueueing any
|
|
242
|
+
-- 'pending_post_migration' rows; on an older DB the watcher (D1) catches
|
|
243
|
+
-- the resulting CHECK violation and falls back to 'embed' \u2014 safe because
|
|
244
|
+
-- the worker is paused on the migration lock during the window.
|
|
245
|
+
CREATE TABLE IF NOT EXISTS chunk_queue (
|
|
246
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
247
|
+
session_id TEXT NOT NULL,
|
|
248
|
+
message_uuid TEXT,
|
|
249
|
+
action TEXT NOT NULL CHECK (action IN ('embed', 'reembed', 'delete', 'pending_post_migration')),
|
|
250
|
+
enqueued_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
251
|
+
);
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_chunk_queue_session ON chunk_queue(session_id);
|
|
253
|
+
|
|
254
|
+
-- v0.78 \u2014 semantic migration state (Phase 2 of Tier 2 modernization).
|
|
255
|
+
-- Tracks the lifecycle of a \`recall semantic migrate\` run: in-progress with
|
|
256
|
+
-- a cursor, completed (vec_chunks now holds the new-backend vectors and
|
|
257
|
+
-- vec_chunks_v1_backup retains the prior set for 30 days), failed (the
|
|
258
|
+
-- partial vec_chunks_v2 is left in place for forensic inspection then
|
|
259
|
+
-- discarded by a future migrate retry), or rolled_back (the backup was
|
|
260
|
+
-- swapped back in via \`recall semantic rollback-migration\`).
|
|
261
|
+
--
|
|
262
|
+
-- schema_version lets a future CLI version refuse to resume a cursor it
|
|
263
|
+
-- cannot interpret rather than corrupt the migration state silently.
|
|
264
|
+
CREATE TABLE IF NOT EXISTS migration_state (
|
|
265
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
266
|
+
schema_version INTEGER NOT NULL,
|
|
267
|
+
status TEXT NOT NULL CHECK (status IN (
|
|
268
|
+
'in_progress',
|
|
269
|
+
'paused',
|
|
270
|
+
'completed',
|
|
271
|
+
'rolled_back',
|
|
272
|
+
'failed'
|
|
273
|
+
)),
|
|
274
|
+
started_at TEXT NOT NULL,
|
|
275
|
+
completed_at TEXT,
|
|
276
|
+
lock_taken_at TEXT,
|
|
277
|
+
lock_taken_by_pid INTEGER,
|
|
278
|
+
cursor_session_id TEXT,
|
|
279
|
+
cursor_chunk_id INTEGER,
|
|
280
|
+
model_id_old TEXT NOT NULL,
|
|
281
|
+
model_id_new TEXT NOT NULL
|
|
282
|
+
);
|
|
283
|
+
CREATE INDEX IF NOT EXISTS idx_migration_state_status ON migration_state(status);
|
|
284
|
+
|
|
285
|
+
-- Belt-and-suspenders: at most one active (in-progress or paused) row.
|
|
286
|
+
-- Partial UNIQUE INDEX on schema_version with predicate status IN ('in_progress','paused')
|
|
287
|
+
-- means "at most one row per schema_version that is active." Since schema_version
|
|
288
|
+
-- is monotonic, this effectively caps active migrations at one regardless of
|
|
289
|
+
-- application-layer race conditions.
|
|
290
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_migration_state_single_active
|
|
291
|
+
ON migration_state(schema_version) WHERE status IN ('in_progress', 'paused');
|
|
292
|
+
|
|
293
|
+
-- v0.67.0 -- retention/archive (P6). Power-user durability: sessions older
|
|
294
|
+
-- than the user chosen window can be moved to messages_archive so the hot
|
|
295
|
+
-- DB stays small without dropping data. Sessions metadata stays hot for
|
|
296
|
+
-- list/search by title; only the message bodies move. The "recall archive
|
|
297
|
+
-- restore <id>" command reverses the move. Source JSONLs are still the
|
|
298
|
+
-- immutable ground truth -- the archive table is a fast-path; in extremis
|
|
299
|
+
-- a session can be reindexed from its file_path on disk.
|
|
300
|
+
--
|
|
301
|
+
-- archive_status on the sessions row drives the UI: archived sessions show
|
|
302
|
+
-- a badge and a "restore to load full transcript" affordance.
|
|
303
|
+
CREATE TABLE IF NOT EXISTS messages_archive (
|
|
304
|
+
uuid TEXT PRIMARY KEY,
|
|
305
|
+
session_id TEXT NOT NULL,
|
|
306
|
+
parent_uuid TEXT,
|
|
307
|
+
type TEXT,
|
|
308
|
+
role TEXT,
|
|
309
|
+
timestamp TEXT,
|
|
310
|
+
is_sidechain INTEGER NOT NULL DEFAULT 0,
|
|
311
|
+
content_text TEXT,
|
|
312
|
+
tool_names TEXT,
|
|
313
|
+
raw_json TEXT,
|
|
314
|
+
archived_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
315
|
+
);
|
|
316
|
+
CREATE INDEX IF NOT EXISTS idx_messages_archive_session ON messages_archive(session_id);
|
|
317
|
+
|
|
318
|
+
-- v0.67.0 \u2014 semantic enqueue gate. Without this, every INSERT/DELETE/UPDATE
|
|
319
|
+
-- on messages enqueued a chunk_queue row, regardless of whether the embedder
|
|
320
|
+
-- worker was running. Power users with semantic disabled (the default)
|
|
321
|
+
-- accumulated 50M+ orphan rows in days, growing the DB by ~1 GB/day.
|
|
322
|
+
-- The gate is a single key in app_settings, kept in sync with
|
|
323
|
+
-- ~/.recall/config.json:semantic.enabled by the daemon at boot and on every
|
|
324
|
+
-- semantic-config write.
|
|
325
|
+
CREATE TABLE IF NOT EXISTS app_settings (
|
|
326
|
+
key TEXT PRIMARY KEY,
|
|
327
|
+
value TEXT NOT NULL
|
|
328
|
+
);
|
|
329
|
+
INSERT OR IGNORE INTO app_settings(key, value) VALUES ('semantic_enabled', '0');
|
|
330
|
+
|
|
331
|
+
-- Drop unconditional v0.10/v0.11-era triggers; replaced below with the gated
|
|
332
|
+
-- versions. Idempotent: SQLite no-ops when the trigger does not exist, so
|
|
333
|
+
-- this is safe to run on every boot and on fresh databases.
|
|
334
|
+
--
|
|
335
|
+
-- 2026-05-23 \u2014 added IF NOT EXISTS guards on the CREATE side too. Without
|
|
336
|
+
-- them, any failure between the DROPs and the CREATEs (e.g. a partially
|
|
337
|
+
-- applied prior boot, or a future statement inserted between them) left
|
|
338
|
+
-- the live DB with the gated triggers but the next SCHEMA_SQL re-exec
|
|
339
|
+
-- still threw "trigger messages_vec_ai already exists". That threw all the
|
|
340
|
+
-- way out of getDb(), which caused syncSemanticEnabledToDb to silently
|
|
341
|
+
-- skip the gate flip \u2014 and the live (still-present) triggers fired with
|
|
342
|
+
-- the prior semantic_enabled='1' value, causing spurious chunk_queue
|
|
343
|
+
-- inserts even when semantic search was disabled. Belt and suspenders:
|
|
344
|
+
-- drop-then-create-if-not-exists is fully idempotent.
|
|
345
|
+
DROP TRIGGER IF EXISTS messages_vec_ai;
|
|
346
|
+
DROP TRIGGER IF EXISTS messages_vec_ad;
|
|
347
|
+
DROP TRIGGER IF EXISTS messages_vec_au;
|
|
348
|
+
|
|
349
|
+
CREATE TRIGGER IF NOT EXISTS messages_vec_ai AFTER INSERT ON messages
|
|
350
|
+
WHEN new.is_sidechain = 0
|
|
351
|
+
AND new.content_text IS NOT NULL
|
|
352
|
+
AND (SELECT value FROM app_settings WHERE key = 'semantic_enabled') = '1'
|
|
353
|
+
BEGIN
|
|
354
|
+
INSERT INTO chunk_queue(session_id, message_uuid, action, enqueued_at)
|
|
355
|
+
VALUES (new.session_id, new.uuid, 'embed', datetime('now'));
|
|
356
|
+
END;
|
|
357
|
+
|
|
358
|
+
CREATE TRIGGER IF NOT EXISTS messages_vec_ad AFTER DELETE ON messages
|
|
359
|
+
WHEN (SELECT value FROM app_settings WHERE key = 'semantic_enabled') = '1'
|
|
360
|
+
BEGIN
|
|
361
|
+
INSERT INTO chunk_queue(session_id, message_uuid, action, enqueued_at)
|
|
362
|
+
VALUES (old.session_id, old.uuid, 'delete', datetime('now'));
|
|
363
|
+
END;
|
|
364
|
+
|
|
365
|
+
CREATE TRIGGER IF NOT EXISTS messages_vec_au AFTER UPDATE OF content_text ON messages
|
|
366
|
+
WHEN new.is_sidechain = 0
|
|
367
|
+
AND (SELECT value FROM app_settings WHERE key = 'semantic_enabled') = '1'
|
|
368
|
+
BEGIN
|
|
369
|
+
INSERT INTO chunk_queue(session_id, message_uuid, action, enqueued_at)
|
|
370
|
+
VALUES (new.session_id, new.uuid, 'reembed', datetime('now'));
|
|
371
|
+
END;
|
|
372
|
+
|
|
373
|
+
-- v0.10a \u2014 cost / token analytics. Claude Code already writes per-assistant-
|
|
374
|
+
-- message usage + model into the source JSONLs; we persist the raw counts
|
|
375
|
+
-- here and derive dollar amounts at render time (pricing changes; token
|
|
376
|
+
-- counts don't). message_usage is 1:1 with messages (keyed by message_uuid)
|
|
377
|
+
-- so a reindex rebuilds it cleanly; rollup columns on sessions are a
|
|
378
|
+
-- cache for fast list rendering.
|
|
379
|
+
CREATE TABLE IF NOT EXISTS message_usage (
|
|
380
|
+
message_uuid TEXT PRIMARY KEY REFERENCES messages(uuid) ON DELETE CASCADE,
|
|
381
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
382
|
+
model TEXT,
|
|
383
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
384
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
385
|
+
cache_create_tokens INTEGER NOT NULL DEFAULT 0,
|
|
386
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
387
|
+
timestamp TEXT
|
|
388
|
+
);
|
|
389
|
+
CREATE INDEX IF NOT EXISTS idx_message_usage_session ON message_usage(session_id);
|
|
390
|
+
CREATE INDEX IF NOT EXISTS idx_message_usage_model ON message_usage(model);
|
|
391
|
+
-- Windowed stats queries (7d / 30d) filter on mu.timestamp >= since.
|
|
392
|
+
-- Without this index every refresh full-scans message_usage.
|
|
393
|
+
CREATE INDEX IF NOT EXISTS idx_message_usage_timestamp
|
|
394
|
+
ON message_usage(timestamp);
|
|
395
|
+
-- Daily-bucket aggregation groups by substr(timestamp, 1, 10) and joins
|
|
396
|
+
-- on session_id. The composite covers the common "by session in window"
|
|
397
|
+
-- access pattern and lets the engine skip the row lookup for many plans.
|
|
398
|
+
CREATE INDEX IF NOT EXISTS idx_message_usage_session_ts
|
|
399
|
+
ON message_usage(session_id, timestamp);
|
|
400
|
+
|
|
401
|
+
-- v0.10b \u2014 git correlation. For any session whose cwd is a git worktree we
|
|
402
|
+
-- run a read-only \`git log\` scoped to that cwd for the [started_at, ended_at]
|
|
403
|
+
-- window and record every resulting commit. Composite PK (session_id,
|
|
404
|
+
-- commit_sha) lets a single commit belong to multiple sessions (rare, but
|
|
405
|
+
-- happens when a long session ends right as another starts). The reverse
|
|
406
|
+
-- index on commit_sha powers \`recall blame <sha>\`.
|
|
407
|
+
--
|
|
408
|
+
-- correlated_at lets the correlator skip sessions it has already processed
|
|
409
|
+
-- recently; cwd_snapshot captures the directory we ran git in so stale rows
|
|
410
|
+
-- can be identified if the user later points the session elsewhere.
|
|
411
|
+
CREATE TABLE IF NOT EXISTS session_commits (
|
|
412
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
413
|
+
commit_sha TEXT NOT NULL,
|
|
414
|
+
committed_at TEXT,
|
|
415
|
+
subject TEXT,
|
|
416
|
+
cwd_snapshot TEXT,
|
|
417
|
+
correlated_at TEXT NOT NULL,
|
|
418
|
+
PRIMARY KEY (session_id, commit_sha)
|
|
419
|
+
);
|
|
420
|
+
CREATE INDEX IF NOT EXISTS idx_session_commits_sha ON session_commits(commit_sha);
|
|
421
|
+
CREATE INDEX IF NOT EXISTS idx_session_commits_session ON session_commits(session_id);
|
|
422
|
+
-- v0.13 \u2014 MCP write audit log. Every write tool invocation appends one row,
|
|
423
|
+
-- regardless of outcome (ok / error / rate_limited). Tags and collections
|
|
424
|
+
-- already have their own append-only event tables (tag_events,
|
|
425
|
+
-- collection_events) \u2014 this table covers note/alias writes and provides a
|
|
426
|
+
-- single chronological feed across all MCP write activity for review.
|
|
427
|
+
CREATE TABLE IF NOT EXISTS mcp_audit_events (
|
|
428
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
429
|
+
tool TEXT NOT NULL,
|
|
430
|
+
args_json TEXT NOT NULL,
|
|
431
|
+
result TEXT NOT NULL, -- 'ok' | 'error' | 'rate_limited'
|
|
432
|
+
error_message TEXT,
|
|
433
|
+
caller TEXT,
|
|
434
|
+
at TEXT NOT NULL
|
|
435
|
+
);
|
|
436
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_audit_events_at ON mcp_audit_events(at DESC);
|
|
437
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_audit_events_tool ON mcp_audit_events(tool, at DESC);
|
|
438
|
+
|
|
439
|
+
-- v0.15 T6 \u2014 auto-collections. Rules match sessions by cwd prefix, project
|
|
440
|
+
-- id, tag, plan-file reference, or git branch prefix, and insert auto
|
|
441
|
+
-- memberships into collection_sessions (tagged with source='auto' + the
|
|
442
|
+
-- originating rule_id). Manual memberships (source='manual') are never
|
|
443
|
+
-- touched by the rule engine \u2014 so a user's hand-curated pick always wins.
|
|
444
|
+
--
|
|
445
|
+
-- Suggestions are the discovery half: the daemon periodically surveys the
|
|
446
|
+
-- corpus, detects clusters that *would* make good auto-collections, and
|
|
447
|
+
-- stashes them here for the user to accept or dismiss. UNIQUE(type,pattern)
|
|
448
|
+
-- means the same cluster won't be re-suggested every scan.
|
|
449
|
+
--
|
|
450
|
+
-- Durability mirror: ~/.recall/auto-rules/{rules.json,suggestions.json}
|
|
451
|
+
-- rewritten on every mutation. Deleting a rule removes ONLY its auto
|
|
452
|
+
-- memberships (matched via rule_id); the target collection stays so the
|
|
453
|
+
-- user can keep whatever they manually added into it.
|
|
454
|
+
CREATE TABLE IF NOT EXISTS auto_collection_rules (
|
|
455
|
+
id TEXT PRIMARY KEY,
|
|
456
|
+
name TEXT NOT NULL,
|
|
457
|
+
type TEXT NOT NULL CHECK (type IN ('cwd-prefix','project-id','tag','plan-file','git-branch-prefix')),
|
|
458
|
+
pattern TEXT NOT NULL,
|
|
459
|
+
collection_id TEXT NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
|
460
|
+
priority INTEGER NOT NULL DEFAULT 100,
|
|
461
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
462
|
+
created_at TEXT NOT NULL,
|
|
463
|
+
created_by TEXT NOT NULL DEFAULT 'user' -- 'user' | 'suggestion-accepted' | 'seed'
|
|
464
|
+
);
|
|
465
|
+
CREATE INDEX IF NOT EXISTS idx_auto_rules_type_enabled ON auto_collection_rules(type, enabled);
|
|
466
|
+
CREATE INDEX IF NOT EXISTS idx_auto_rules_collection ON auto_collection_rules(collection_id);
|
|
467
|
+
|
|
468
|
+
CREATE TABLE IF NOT EXISTS auto_collection_suggestions (
|
|
469
|
+
id TEXT PRIMARY KEY,
|
|
470
|
+
type TEXT NOT NULL,
|
|
471
|
+
pattern TEXT NOT NULL,
|
|
472
|
+
suggested_name TEXT NOT NULL,
|
|
473
|
+
suggested_parent_collection_id TEXT,
|
|
474
|
+
session_count INTEGER NOT NULL,
|
|
475
|
+
detected_at TEXT NOT NULL,
|
|
476
|
+
dismissed INTEGER NOT NULL DEFAULT 0,
|
|
477
|
+
UNIQUE(type, pattern)
|
|
478
|
+
);
|
|
479
|
+
CREATE INDEX IF NOT EXISTS idx_auto_suggestions_detected ON auto_collection_suggestions(detected_at DESC);
|
|
480
|
+
|
|
481
|
+
-- v0.15 Threads. The headline intent-grouping primitive: a DAG of sessions
|
|
482
|
+
-- connected by shared purpose (one or more origin sessions plus their
|
|
483
|
+
-- children). Additive to Projects and Collections; neither is refactored.
|
|
484
|
+
-- thread_edges is many-to-many so a session can belong to multiple threads
|
|
485
|
+
-- (planning session that seeds two features). parent_session_id is
|
|
486
|
+
-- nullable; when null, the row is an origin. confidence = 1.0 for manual
|
|
487
|
+
-- edges; < 1.0 for auto-detected edges (v0.15b). source tracks provenance
|
|
488
|
+
-- for the audit/undo paths.
|
|
489
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
490
|
+
id TEXT PRIMARY KEY,
|
|
491
|
+
name TEXT NOT NULL,
|
|
492
|
+
summary TEXT,
|
|
493
|
+
created_at TEXT NOT NULL,
|
|
494
|
+
closed_at TEXT,
|
|
495
|
+
archived INTEGER NOT NULL DEFAULT 0
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
CREATE TABLE IF NOT EXISTS thread_edges (
|
|
499
|
+
thread_id TEXT NOT NULL,
|
|
500
|
+
session_id TEXT NOT NULL,
|
|
501
|
+
parent_session_id TEXT,
|
|
502
|
+
role TEXT NOT NULL CHECK(role IN ('origin','child')),
|
|
503
|
+
confidence REAL NOT NULL DEFAULT 1.0 CHECK(confidence >= 0 AND confidence <= 1),
|
|
504
|
+
source TEXT NOT NULL DEFAULT 'manual',
|
|
505
|
+
added_at TEXT NOT NULL,
|
|
506
|
+
PRIMARY KEY (thread_id, session_id),
|
|
507
|
+
FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
|
|
508
|
+
);
|
|
509
|
+
CREATE INDEX IF NOT EXISTS idx_thread_edges_session ON thread_edges(session_id);
|
|
510
|
+
CREATE INDEX IF NOT EXISTS idx_thread_edges_parent ON thread_edges(parent_session_id);
|
|
511
|
+
CREATE INDEX IF NOT EXISTS idx_thread_edges_thread_role ON thread_edges(thread_id, role);
|
|
512
|
+
|
|
513
|
+
-- v0.6 #4: user-defined SUBFOLDERS that nest inside the auto-derived
|
|
514
|
+
-- per-project folders the Threads sidebar already renders. Critical
|
|
515
|
+
-- design rule: auto-project folders are NOT in this table \u2014 they are
|
|
516
|
+
-- computed at render time from project membership of each thread's
|
|
517
|
+
-- sessions. So even with this table empty, every project folder still
|
|
518
|
+
-- renders and every thread still has a visible home. That's the
|
|
519
|
+
-- safety property that the previous full-replacement attempt lost
|
|
520
|
+
-- (when no user folders existed, every thread fell into a single
|
|
521
|
+
-- collapsed "(unfiled)" bucket and the sidebar looked broken).
|
|
522
|
+
--
|
|
523
|
+
-- project_scope: the project this user folder is nested under. NULL
|
|
524
|
+
-- means top-level (cross-project, e.g. "v0.6 Launch" that holds
|
|
525
|
+
-- threads from multiple repos).
|
|
526
|
+
-- parent_folder_id: another USER folder this one nests inside. NULL
|
|
527
|
+
-- means it's a direct child of project_scope's auto folder, or
|
|
528
|
+
-- top-level if project_scope is also NULL.
|
|
529
|
+
CREATE TABLE IF NOT EXISTS thread_folders (
|
|
530
|
+
id TEXT PRIMARY KEY,
|
|
531
|
+
name TEXT NOT NULL,
|
|
532
|
+
parent_folder_id TEXT REFERENCES thread_folders(id) ON DELETE CASCADE,
|
|
533
|
+
project_scope TEXT,
|
|
534
|
+
created_at TEXT NOT NULL,
|
|
535
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
536
|
+
sort_order INTEGER NOT NULL DEFAULT 0
|
|
537
|
+
);
|
|
538
|
+
CREATE INDEX IF NOT EXISTS idx_thread_folders_parent ON thread_folders(parent_folder_id);
|
|
539
|
+
CREATE INDEX IF NOT EXISTS idx_thread_folders_project ON thread_folders(project_scope);
|
|
540
|
+
|
|
541
|
+
-- v0.17 -- recall event log. Every \`recall context\` invocation writes one
|
|
542
|
+
-- row. Powers share-card metadata ("recalled today") and monthly wrap
|
|
543
|
+
-- aggregation (total recalls, most-recalled session, etc.). Append-only;
|
|
544
|
+
-- no deletes. Plain-text mirror at ~/.recall/recall-events.json rewritten
|
|
545
|
+
-- on mutation.
|
|
546
|
+
CREATE TABLE IF NOT EXISTS recall_events (
|
|
547
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
548
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
549
|
+
recalled_at TEXT NOT NULL,
|
|
550
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
551
|
+
mode TEXT NOT NULL DEFAULT 'full',
|
|
552
|
+
caller TEXT NOT NULL DEFAULT 'cli'
|
|
553
|
+
);
|
|
554
|
+
CREATE INDEX IF NOT EXISTS idx_recall_events_session ON recall_events(session_id, recalled_at DESC);
|
|
555
|
+
CREATE INDEX IF NOT EXISTS idx_recall_events_at ON recall_events(recalled_at DESC);
|
|
556
|
+
|
|
557
|
+
-- v0.18 cog-graph Phase C \u2014 multi-edge schema.
|
|
558
|
+
--
|
|
559
|
+
-- Two parallel edge systems live side by side and MUST NOT be merged:
|
|
560
|
+
-- 1. thread_edges (above) \u2014 hierarchical DAG, intent grouping.
|
|
561
|
+
-- 2. session_links (below) \u2014 non-hierarchical: citations, semantic
|
|
562
|
+
-- similarity, skill tracks, bug-pattern membership, manual wiki
|
|
563
|
+
-- links, temporal proximity. Joined at query time; never schema-
|
|
564
|
+
-- level merged.
|
|
565
|
+
--
|
|
566
|
+
-- Every row carries provenance (source + evidence JSON + confidence) so
|
|
567
|
+
-- "why does this edge exist?" always has an answer. The unique constraint
|
|
568
|
+
-- on (source, target, link_type) is the soft idempotency key \u2014 re-running
|
|
569
|
+
-- inference upserts instead of duplicating. Approval gating via
|
|
570
|
+
-- session_link_suggestions (the queue) \u2192 session_links (the live store)
|
|
571
|
+
-- keeps trust intact: nothing auto-promotes without user opt-in.
|
|
572
|
+
--
|
|
573
|
+
-- Mirror layout: ~/.recall/links/<source-session-id>.json (per-source
|
|
574
|
+
-- denormalised; rewritten on mutation), ~/.recall/suggestions/index.json
|
|
575
|
+
-- (single file rewritten on mutation).
|
|
576
|
+
CREATE TABLE IF NOT EXISTS session_links (
|
|
577
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
578
|
+
source_session_id TEXT NOT NULL,
|
|
579
|
+
target_session_id TEXT NOT NULL,
|
|
580
|
+
link_type TEXT NOT NULL CHECK(link_type IN (
|
|
581
|
+
'citation','similar','skill_track','bug_pattern',
|
|
582
|
+
'wiki_link','temporal_proximity'
|
|
583
|
+
)),
|
|
584
|
+
confidence REAL NOT NULL CHECK(confidence BETWEEN 0 AND 1),
|
|
585
|
+
source TEXT NOT NULL,
|
|
586
|
+
evidence TEXT NOT NULL,
|
|
587
|
+
approved INTEGER NOT NULL DEFAULT 0,
|
|
588
|
+
created_at TEXT NOT NULL,
|
|
589
|
+
updated_at TEXT NOT NULL,
|
|
590
|
+
UNIQUE(source_session_id, target_session_id, link_type)
|
|
591
|
+
);
|
|
592
|
+
CREATE INDEX IF NOT EXISTS idx_links_source ON session_links(source_session_id);
|
|
593
|
+
CREATE INDEX IF NOT EXISTS idx_links_target ON session_links(target_session_id);
|
|
594
|
+
CREATE INDEX IF NOT EXISTS idx_links_type ON session_links(link_type);
|
|
595
|
+
|
|
596
|
+
-- v0.18 cog-graph Phase C \u2014 per-session structured outputs.
|
|
597
|
+
--
|
|
598
|
+
-- The "what did this session produce" index. Phase D's LLM-augmented
|
|
599
|
+
-- extractor sub-agent populates this row (Phase C ships the empty
|
|
600
|
+
-- table). Stays empty in Phase C. extractor_version lets us re-derive
|
|
601
|
+
-- when the extraction prompt or schema improves without losing the
|
|
602
|
+
-- existing rows. raw_extraction is the full sub-agent output blob so we
|
|
603
|
+
-- can re-derive structured fields without re-running extraction.
|
|
604
|
+
--
|
|
605
|
+
-- Mirror: ~/.recall/output-index/<session-id>.json rewritten on mutation.
|
|
606
|
+
CREATE TABLE IF NOT EXISTS session_output_index (
|
|
607
|
+
session_id TEXT PRIMARY KEY,
|
|
608
|
+
files_written TEXT,
|
|
609
|
+
brands_mentioned TEXT,
|
|
610
|
+
terms_introduced TEXT,
|
|
611
|
+
plan_ids_referenced TEXT,
|
|
612
|
+
bug_signatures TEXT,
|
|
613
|
+
raw_extraction TEXT,
|
|
614
|
+
extracted_at TEXT NOT NULL,
|
|
615
|
+
extractor_version INTEGER NOT NULL DEFAULT 1
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
-- v0.18 cog-graph Phase C \u2014 pending edge suggestions awaiting review.
|
|
619
|
+
--
|
|
620
|
+
-- Auto-inferred edges (from L1/L2/L3/L4 inference jobs) land here first
|
|
621
|
+
-- with status='pending'. User accepts \u2192 status='approved' AND a row gets
|
|
622
|
+
-- created in session_links with approved=1. User rejects \u2192 status=
|
|
623
|
+
-- 'rejected' (tombstone \u2014 prevents re-proposing the same pair from the
|
|
624
|
+
-- same inferer). Phase F builds the queue UI; Phase C ships the empty
|
|
625
|
+
-- table. The unique key includes inferred_by so two layers (e.g. L2
|
|
626
|
+
-- citation match + L3 embedding match) can both contribute evidence
|
|
627
|
+
-- without one stomping the other.
|
|
628
|
+
CREATE TABLE IF NOT EXISTS session_link_suggestions (
|
|
629
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
630
|
+
source_session_id TEXT NOT NULL,
|
|
631
|
+
target_session_id TEXT NOT NULL,
|
|
632
|
+
link_type TEXT NOT NULL,
|
|
633
|
+
confidence REAL NOT NULL,
|
|
634
|
+
evidence TEXT NOT NULL,
|
|
635
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected')),
|
|
636
|
+
inferred_by TEXT NOT NULL,
|
|
637
|
+
created_at TEXT NOT NULL,
|
|
638
|
+
decided_at TEXT,
|
|
639
|
+
UNIQUE(source_session_id, target_session_id, link_type, inferred_by)
|
|
640
|
+
);
|
|
641
|
+
CREATE INDEX IF NOT EXISTS idx_suggestions_pending ON session_link_suggestions(status, created_at) WHERE status = 'pending';
|
|
642
|
+
CREATE INDEX IF NOT EXISTS idx_suggestions_source ON session_link_suggestions(source_session_id);
|
|
643
|
+
CREATE INDEX IF NOT EXISTS idx_suggestions_target ON session_link_suggestions(target_session_id);
|
|
644
|
+
|
|
645
|
+
-- v0.18 cog-graph Phase C \u2014 bug-pattern clusters. Empty in Phase C;
|
|
646
|
+
-- populated by Phase H (HDBSCAN over bug-signature embeddings). Tables
|
|
647
|
+
-- ship now so the schema is stable across the milestone. resolved_in_
|
|
648
|
+
-- session_id is set when the user marks "this fix worked" \u2014 surfaced
|
|
649
|
+
-- the next time a session's bug signature falls into the same cluster.
|
|
650
|
+
CREATE TABLE IF NOT EXISTS bug_pattern_clusters (
|
|
651
|
+
id TEXT PRIMARY KEY,
|
|
652
|
+
signature_hash TEXT NOT NULL,
|
|
653
|
+
example_message TEXT NOT NULL,
|
|
654
|
+
occurrence_count INTEGER NOT NULL,
|
|
655
|
+
first_seen_at TEXT NOT NULL,
|
|
656
|
+
last_seen_at TEXT NOT NULL,
|
|
657
|
+
resolved_in_session_id TEXT,
|
|
658
|
+
fix_summary TEXT
|
|
659
|
+
);
|
|
660
|
+
CREATE INDEX IF NOT EXISTS idx_bug_clusters_signature ON bug_pattern_clusters(signature_hash);
|
|
661
|
+
CREATE INDEX IF NOT EXISTS idx_bug_clusters_last_seen ON bug_pattern_clusters(last_seen_at DESC);
|
|
662
|
+
|
|
663
|
+
CREATE TABLE IF NOT EXISTS bug_pattern_members (
|
|
664
|
+
cluster_id TEXT NOT NULL REFERENCES bug_pattern_clusters(id) ON DELETE CASCADE,
|
|
665
|
+
session_id TEXT NOT NULL,
|
|
666
|
+
matched_at TEXT NOT NULL,
|
|
667
|
+
PRIMARY KEY (cluster_id, session_id)
|
|
668
|
+
);
|
|
669
|
+
CREATE INDEX IF NOT EXISTS idx_bug_members_session ON bug_pattern_members(session_id);
|
|
670
|
+
|
|
671
|
+
-- v0.20 / project rollups. The user's mental model is "I have ~18 repos"
|
|
672
|
+
-- but Recall's project model is "every cwd is a project" (~32 entries).
|
|
673
|
+
-- Macro repos are a deterministic, manual grouping layer the user
|
|
674
|
+
-- defines: pick N projects, label them as one logical repo. Display
|
|
675
|
+
-- surfaces collapse member projects into the macro repo when the user
|
|
676
|
+
-- toggles "by macro repo." See docs/internal/project-rollups.md.
|
|
677
|
+
--
|
|
678
|
+
-- We deliberately reject auto-grouping: a wrong auto-rollup is worse
|
|
679
|
+
-- than no rollup. Membership is always explicit, set by the user.
|
|
680
|
+
CREATE TABLE IF NOT EXISTS macro_repos (
|
|
681
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
682
|
+
name TEXT NOT NULL UNIQUE,
|
|
683
|
+
description TEXT,
|
|
684
|
+
created_at TEXT NOT NULL,
|
|
685
|
+
updated_at TEXT NOT NULL
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
CREATE TABLE IF NOT EXISTS macro_repo_members (
|
|
689
|
+
macro_repo_id INTEGER NOT NULL REFERENCES macro_repos(id) ON DELETE CASCADE,
|
|
690
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
691
|
+
added_at TEXT NOT NULL,
|
|
692
|
+
PRIMARY KEY (macro_repo_id, project_id)
|
|
693
|
+
);
|
|
694
|
+
CREATE INDEX IF NOT EXISTS idx_macro_repo_members_project ON macro_repo_members(project_id);
|
|
695
|
+
|
|
696
|
+
-- v0.20 / synthesis result persistence. Every successful Bug Pattern
|
|
697
|
+
-- synthesis run writes its full Markdown output here so the user can
|
|
698
|
+
-- revisit reports later without re-spending tokens. Keyed by
|
|
699
|
+
-- (scope, target_id, mode, created_at) \u2014 multiple runs over time on the
|
|
700
|
+
-- same target are kept as a history (newest-first browse). Token spend
|
|
701
|
+
-- is recorded for audit. NOT subject to the three-layer durability rule
|
|
702
|
+
-- because the source data (bug_pattern_clusters + member sessions) can
|
|
703
|
+
-- always re-derive the synthesis.
|
|
704
|
+
CREATE TABLE IF NOT EXISTS bug_synthesis_results (
|
|
705
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
706
|
+
scope TEXT NOT NULL CHECK(scope IN ('cluster', 'project')),
|
|
707
|
+
target_id TEXT NOT NULL,
|
|
708
|
+
mode TEXT NOT NULL CHECK(mode IN ('synopsis', 'priorities', 'root_cause')),
|
|
709
|
+
model TEXT NOT NULL,
|
|
710
|
+
output_markdown TEXT NOT NULL,
|
|
711
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
712
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
713
|
+
context_summary TEXT NOT NULL DEFAULT '{}',
|
|
714
|
+
created_at TEXT NOT NULL,
|
|
715
|
+
job_id TEXT
|
|
716
|
+
);
|
|
717
|
+
CREATE INDEX IF NOT EXISTS idx_synth_results_target
|
|
718
|
+
ON bug_synthesis_results(scope, target_id, created_at DESC);
|
|
719
|
+
CREATE INDEX IF NOT EXISTS idx_synth_results_created
|
|
720
|
+
ON bug_synthesis_results(created_at DESC);
|
|
721
|
+
|
|
722
|
+
-- Incremental-indexing parse cursor. One row per JSONL file (a file may
|
|
723
|
+
-- hold multiple sessions, so the cursor is per-FILE, not per-session).
|
|
724
|
+
-- Lets the watcher stream only newly-appended bytes instead of reparsing
|
|
725
|
+
-- from byte 0. Derived/recomputable state \u2014 NOT subject to the three-layer
|
|
726
|
+
-- durability rule (a wrong/missing cursor just forces a full reparse).
|
|
727
|
+
-- size_bytes and line_count are diagnostic only: decideReadStrategy compares
|
|
728
|
+
-- the LIVE stat size against byte_offset, never the stored size_bytes.
|
|
729
|
+
CREATE TABLE IF NOT EXISTS file_cursor (
|
|
730
|
+
file_path TEXT PRIMARY KEY,
|
|
731
|
+
byte_offset INTEGER NOT NULL,
|
|
732
|
+
size_bytes INTEGER NOT NULL,
|
|
733
|
+
line_count INTEGER NOT NULL DEFAULT 0,
|
|
734
|
+
inode INTEGER,
|
|
735
|
+
prefix_hash TEXT,
|
|
736
|
+
mtime REAL,
|
|
737
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
738
|
+
);
|
|
739
|
+
`;function V(e){let s=e.prepare("PRAGMA table_info(sessions)").all(),t=new Set(s.map(n=>n.name)),r=[["total_input_tokens","INTEGER"],["total_output_tokens","INTEGER"],["total_cache_create_tokens","INTEGER"],["total_cache_read_tokens","INTEGER"],["primary_model","TEXT"],["auto_title","TEXT"],["auto_title_source","TEXT"],["auto_title_generated_at","INTEGER"],["auto_title_history","TEXT"],["verification_status","TEXT"],["verification_computed_at","INTEGER"],["title_quality","TEXT"],["title_quality_computed_at","INTEGER"],["archive_status","TEXT NOT NULL DEFAULT 'live'"],["archived_at","TEXT"],["skipped_reason","TEXT"]];for(let[n,u]of r)t.has(n)||e.exec(`ALTER TABLE sessions ADD COLUMN ${n} ${u}`);let o=e.prepare("PRAGMA table_info(collection_sessions)").all(),G=new Set(o.map(n=>n.name)),w=[["source","TEXT NOT NULL DEFAULT 'manual'"],["rule_id","TEXT"]];for(let[n,u]of w)G.has(n)||e.exec(`ALTER TABLE collection_sessions ADD COLUMN ${n} ${u}`);let i=e.prepare("PRAGMA table_info(session_notes)").all(),de=new Set(i.map(n=>n.name)),Ee=[["auto_synopsis","TEXT"],["auto_synopsis_generated_at","INTEGER"],["auto_synopsis_history","TEXT"]];for(let[n,u]of Ee)de.has(n)||e.exec(`ALTER TABLE session_notes ADD COLUMN ${n} ${u}`);let Te=e.prepare("PRAGMA table_info(threads)").all();new Set(Te.map(n=>n.name)).has("folder_id")||(e.exec("ALTER TABLE threads ADD COLUMN folder_id TEXT REFERENCES thread_folders(id) ON DELETE SET NULL"),e.exec("CREATE INDEX IF NOT EXISTS idx_threads_folder ON threads(folder_id)"));let le=e.prepare("PRAGMA table_info(thread_folders)").all(),j=new Set(le.map(n=>n.name));j.has("project_scope")||(e.exec("ALTER TABLE thread_folders ADD COLUMN project_scope TEXT"),e.exec("CREATE INDEX IF NOT EXISTS idx_thread_folders_project ON thread_folders(project_scope)")),j.has("sort_order")||e.exec("ALTER TABLE thread_folders ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"),e.exec(`
|
|
740
|
+
CREATE TABLE IF NOT EXISTS message_embeddings (
|
|
741
|
+
message_uuid TEXT PRIMARY KEY REFERENCES messages(uuid) ON DELETE CASCADE,
|
|
742
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
743
|
+
embedding BLOB NOT NULL,
|
|
744
|
+
embedding_model_id TEXT NOT NULL DEFAULT 'bge-base-en-v1.5',
|
|
745
|
+
embedding_dim INTEGER NOT NULL DEFAULT 768,
|
|
746
|
+
text_length INTEGER NOT NULL,
|
|
747
|
+
generated_at TEXT NOT NULL
|
|
748
|
+
);
|
|
749
|
+
CREATE INDEX IF NOT EXISTS idx_message_embeddings_session ON message_embeddings(session_id);
|
|
750
|
+
CREATE INDEX IF NOT EXISTS idx_message_embeddings_generated ON message_embeddings(generated_at DESC);
|
|
751
|
+
`);let ce=e.prepare("PRAGMA table_info(projects)").all(),ue=new Set(ce.map(n=>n.name)),me=[["repo_root","TEXT"],["main_repo","TEXT"],["is_repo","INTEGER NOT NULL DEFAULT 0"],["is_worktree","INTEGER NOT NULL DEFAULT 0"]];for(let[n,u]of me)ue.has(n)||e.exec(`ALTER TABLE projects ADD COLUMN ${n} ${u}`)}var Re=new Set(["light","full","worker"]);function Q(e,s){let t=s.RECALL_DB_PROFILE;if(t&&Re.has(t))return t;let r=(e??"").split(/[\\/]/).pop()??"";return r==="mcp-server.js"||r==="claude-recall-mcp"?"light":r==="query-worker.js"?"worker":"full"}function $(e){let s=[["busy_timeout",15e3],["journal_size_limit",67108864],["wal_autocheckpoint",1e3]];return e==="full"?[["cache_size",-64e3],["mmap_size",268435456],...s]:e==="worker"?[["cache_size",-16e3],["mmap_size",0],...s]:[["cache_size",-4e3],["mmap_size",0],...s]}import*as J from"sqlite-vec";var z=new WeakSet;function _(e){let s=e;z.has(s)||(J.load(e),z.add(s))}var Ae=`
|
|
752
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0(
|
|
753
|
+
embedding float[768] distance_metric=cosine
|
|
754
|
+
);`;function Z(e){e.prepare("SELECT 1 FROM sqlite_master WHERE name = 'vec_chunks'").get()||(_(e),e.exec(Ae))}var a=null,Oe="full";function ee(){if(a)return a;H();let e=Q(process.argv[1],process.env);Oe=e,a=new Y(K);for(let[s,t]of $(e))a.pragma(`${s} = ${t}`);a.pragma("temp_store = MEMORY"),e!=="light"&&_(a),a.exec(W),V(a),Z(a);try{a.exec("PRAGMA optimize")}catch{}return a}var b={};B(b,{embed:()=>Fe,embedQuery:()=>Pe,getEmbedderStatus:()=>ke,loadEmbedder:()=>xe,unloadEmbedder:()=>Me});import{Worker as we}from"node:worker_threads";import{join as Xe}from"node:path";import{existsSync as ve}from"node:fs";import{existsSync as be}from"node:fs";import{dirname as se}from"node:path";import{fileURLToPath as ye}from"node:url";var g=null;function I(){if(g)return g;let e=se(ye(import.meta.url));for(;!be(`${e}/package.json`);){let s=se(e);if(s===e)throw new Error(`package.json not found walking up from ${import.meta.url}`);e=s}return g=e,g}function f(e){let s=null;return()=>s||(s=(async()=>{try{return await e()}finally{s=null}})(),s)}var R="BAAI/bge-base-en-v1.5";var T=class extends Error{kind;path;underlying;cause;constructor(s){super(Se(s)),this.name="EmbedderUnavailableError",this.kind=s.kind,this.path=s.path,this.underlying=s.underlying,this.cause=s}};function Se(e){return["Semantic search is unavailable on this platform.","",`Reason: ${e.detail}`,"",`Platform: ${process.platform}/${process.arch}, Node ${process.version}`,"","Claude Recall supports macOS (arm64/x64), Linux (x64/arm64), and Windows (x64).","Core CLI features (search, list, context, daemon) work everywhere.","Only `recall semantic *` requires the on-device embedder.","","See: https://clauderecall.com/docs (Supported platforms) - or file an issue at","https://gitlab.com/clauderecallhq/clauderecallhq/-/issues with the platform line above."].join(`
|
|
755
|
+
`)}function Ue(){return Xe(I(),"dist","daemon","embedder-worker.js")}var l=null,N=new Map,C=0,d=!1;function te(e){for(let s of N.values())s.reject(e);N.clear()}function Ce(){if(l)return l;let e=Ue();if(!ve(e))throw new T({kind:"bundle-missing",detail:"embedder-worker bundle not found - run `npm run build:cli` to emit it",path:e});let s=new we(e);return s.on("message",t=>{let r=N.get(t.id);r&&(N.delete(t.id),r.resolve(t))}),s.on("error",t=>{console.error("[embedder-worker] thread error:",t);let r=t instanceof Error?t:new Error(String(t));te(r),l=null,d=!1}),s.on("exit",t=>{t!==0&&(console.error(`[embedder-worker] exited with code ${t}`),te(new Error(`embedder worker exited with code ${t}`))),l=null,d=!1}),l=s,s}function A(e){return new Promise((s,t)=>{let r;try{r=Ce()}catch(o){t(o instanceof Error?o:new Error(String(o)));return}N.set(e.id,{resolve:s,reject:t}),r.postMessage(e)})}function O(){return C=C+1>>>0,String(C)}function x(e){if(!e.ok)throw e.unavailable?new T({kind:"platform-unsupported",detail:"embedder worker reports the runtime is unavailable on this platform",underlying:new Error(e.error)}):new Error(e.error);return e}var De=f(async()=>{x(await A({id:O(),type:"load"}))});async function xe(){if(!(d&&l))try{await De(),d=!0}catch(e){throw d=!1,e}}function ke(){return{loaded:d,modelId:R,dim:768}}async function Fe(e){if(!d)throw new Error("[embedder] Model not loaded. Call loadEmbedder() before embedding.");return x(await A({id:O(),type:"embed",texts:e})).embeddings.map(t=>new Float32Array(t))}async function Pe(e){if(!d)throw new Error("[embedder] Model not loaded. Call loadEmbedder() before embedding.");let s=x(await A({id:O(),type:"embedQuery",text:e}));return new Float32Array(s.embedding)}async function Me(){if(!l){d=!1;return}try{await A({id:O(),type:"unload"})}catch{}d=!1;let e=l;l=null;try{await e.terminate()}catch{}}var P={};B(P,{LLAMACPP_MODEL_ID:()=>ne,MODEL_ID:()=>R,embed:()=>Ve,embedQuery:()=>Qe,getEmbedderStatus:()=>We,loadEmbedder:()=>He,unloadEmbedder:()=>$e});import{Worker as Ge}from"node:worker_threads";import{join as je}from"node:path";import{existsSync as Be}from"node:fs";var ne="BAAI/bge-base-en-v1.5-gguf-q8_0";function Ye(){return je(I(),"dist","daemon","embedder-worker-llamacpp.js")}var c=null,p=new Map,k=0,E=!1;function re(e){for(let s of p.values())s.reject(e);p.clear()}function qe(){if(c)return c;let e=Ye();if(!Be(e))throw new T({kind:"bundle-missing",detail:"embedder-worker-llamacpp bundle not found - run `npm run build:cli` to emit it",path:e});let s=new Ge(e);return s.on("message",t=>{let r=p.get(t.id);r&&(p.delete(t.id),r.resolve(t))}),s.on("error",t=>{console.error("[embedder-worker-llamacpp] thread error:",t);let r=t instanceof Error?t:new Error(String(t));re(r),c=null,E=!1}),s.on("exit",t=>{t!==0&&(console.error(`[embedder-worker-llamacpp] exited with code ${t}`),re(new Error(`embedder worker exited with code ${t}`))),c=null,E=!1}),c=s,s}function y(e){return new Promise((s,t)=>{let r;try{r=qe()}catch(o){t(o instanceof Error?o:new Error(String(o)));return}p.set(e.id,{resolve:s,reject:t}),r.postMessage(e)})}function S(){return k=k+1>>>0,String(k)}function F(e){if(!e.ok)throw e.unavailable?new T({kind:"platform-unsupported",detail:"embedder worker reports the llama.cpp runtime is unavailable on this platform",underlying:new Error(e.error)}):new Error(e.error);return e}var Ke=f(async()=>{F(await y({id:S(),type:"load"}))});async function He(){if(!(E&&c))try{await Ke(),E=!0}catch(e){throw E=!1,e}}function We(){return{loaded:E,modelId:ne,dim:768}}async function Ve(e){if(!E)throw new Error("[embedder] Model not loaded. Call loadEmbedder() before embedding.");return F(await y({id:S(),type:"embed",texts:e})).embeddings.map(t=>new Float32Array(t))}async function Qe(e){if(!E)throw new Error("[embedder] Model not loaded. Call loadEmbedder() before embedding.");let s=F(await y({id:S(),type:"embedQuery",text:e}));return new Float32Array(s.embedding)}async function $e(){if(!c){E=!1;return}try{await y({id:S(),type:"unload"})}catch{}E=!1;let e=c;c=null;try{await e.terminate()}catch{}}function ze(){let e=(process.env.RECALL_EMBEDDER_BACKEND??"").trim().toLowerCase();return e==="llama"||e==="llamacpp"?P:(e===""||e==="onnx"||process.stderr.write(`[embedder] RECALL_EMBEDDER_BACKEND="${e}" is not recognized; falling back to onnx. Valid values: onnx, llama.
|
|
756
|
+
`),b)}var h=ze(),oe=h.loadEmbedder,Ps=h.getEmbedderStatus,Ms=h.embed,ie=h.embedQuery,Gs=h.unloadEmbedder;var M=class extends Error{name="CorpusTooLargeError";code="CORPUS_TOO_LARGE";rowCount;limit;constructor(s,t){super(`semantic search refused: vec_chunks has ${s} rows (cap ${t}). An unindexed kNN over this corpus can pin the SQLite WAL for hours. Workarounds: (a) scope the search to a smaller project, (b) raise the cap via RECALL_KNN_MAX_CORPUS at your own risk, (c) wait for ANN indexing (tracked in the WAL death-spiral plan).`),this.rowCount=s,this.limit=t}};function ae(e,s={}){let t=s.limit??Je()??75e3,o=e.prepare("SELECT COUNT(*) AS n FROM vec_chunks").get()?.n??0;if(o>t)throw new M(o,t)}function Je(){let e=process.env.RECALL_KNN_MAX_CORPUS;if(!e)return;let s=Number(e);if(!(!Number.isFinite(s)||s<=0||s>1e7))return Math.floor(s)}if(!m)throw new Error("queryWorker.entry must run as a worker_threads child");m.on("message",async e=>{if(!Number.isInteger(e.limit)||e.limit<1||e.limit>1e3){m.postMessage({ok:!1,error:`invalid limit: ${e.limit}`});return}let s=typeof e.query=="string"&&e.query.length>0,t=e.precomputedVector!=null&&e.precomputedVector.byteLength>0;if(s===t){m.postMessage({ok:!1,error:"queryWorker requires exactly one of { query, precomputedVector }"});return}try{let r=ee();_(r),ae(r);let o;if(s){await oe();let i=await ie(e.query);o=Buffer.from(i.buffer,i.byteOffset,i.byteLength)}else{let i=e.precomputedVector;o=Buffer.isBuffer(i)?i:Buffer.from(i.buffer,i.byteOffset,i.byteLength)}let w=r.prepare(`SELECT v.rowid, v.distance, cm.session_id, cm.text, cm.message_uuids
|
|
757
|
+
FROM vec_chunks v JOIN chunk_meta cm ON cm.rowid = v.rowid
|
|
758
|
+
WHERE v.embedding MATCH ? AND k = ? ORDER BY v.distance`).all(o,e.limit).map(i=>({sessionId:i.session_id,chunkRowid:i.rowid,distance:i.distance,text:i.text,messageUuids:JSON.parse(i.message_uuids)}));m.postMessage({ok:!0,hits:w})}catch(r){m.postMessage({ok:!1,error:r.message})}});
|