@getplumb/core 0.1.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/dist/bm25.d.ts +23 -0
  4. package/dist/bm25.d.ts.map +1 -0
  5. package/dist/bm25.js +74 -0
  6. package/dist/bm25.js.map +1 -0
  7. package/dist/chunker.d.ts +35 -0
  8. package/dist/chunker.d.ts.map +1 -0
  9. package/dist/chunker.js +45 -0
  10. package/dist/chunker.js.map +1 -0
  11. package/dist/context-builder.d.ts +33 -0
  12. package/dist/context-builder.d.ts.map +1 -0
  13. package/dist/context-builder.js +101 -0
  14. package/dist/context-builder.js.map +1 -0
  15. package/dist/embedder.d.ts +34 -0
  16. package/dist/embedder.d.ts.map +1 -0
  17. package/dist/embedder.js +88 -0
  18. package/dist/embedder.js.map +1 -0
  19. package/dist/extractor.d.ts +21 -0
  20. package/dist/extractor.d.ts.map +1 -0
  21. package/dist/extractor.js +89 -0
  22. package/dist/extractor.js.map +1 -0
  23. package/dist/fact-search.d.ts +28 -0
  24. package/dist/fact-search.d.ts.map +1 -0
  25. package/dist/fact-search.js +155 -0
  26. package/dist/fact-search.js.map +1 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +12 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/llm-client.d.ts +28 -0
  32. package/dist/llm-client.d.ts.map +1 -0
  33. package/dist/llm-client.js +115 -0
  34. package/dist/llm-client.js.map +1 -0
  35. package/dist/local-store.d.ts +99 -0
  36. package/dist/local-store.d.ts.map +1 -0
  37. package/dist/local-store.js +292 -0
  38. package/dist/local-store.js.map +1 -0
  39. package/dist/raw-log-search.d.ts +33 -0
  40. package/dist/raw-log-search.d.ts.map +1 -0
  41. package/dist/raw-log-search.js +137 -0
  42. package/dist/raw-log-search.js.map +1 -0
  43. package/dist/read-path.d.ts +60 -0
  44. package/dist/read-path.d.ts.map +1 -0
  45. package/dist/read-path.js +76 -0
  46. package/dist/read-path.js.map +1 -0
  47. package/dist/schema.d.ts +34 -0
  48. package/dist/schema.d.ts.map +1 -0
  49. package/dist/schema.js +119 -0
  50. package/dist/schema.js.map +1 -0
  51. package/dist/scorer.d.ts +30 -0
  52. package/dist/scorer.d.ts.map +1 -0
  53. package/dist/scorer.js +50 -0
  54. package/dist/scorer.js.map +1 -0
  55. package/dist/store.d.ts +25 -0
  56. package/dist/store.d.ts.map +1 -0
  57. package/dist/store.js +2 -0
  58. package/dist/store.js.map +1 -0
  59. package/dist/types.d.ts +44 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +7 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +43 -0
@@ -0,0 +1,292 @@
1
+ import Database from 'better-sqlite3';
2
+ import * as sqliteVec from 'sqlite-vec';
3
+ import { createHash } from 'node:crypto';
4
+ import { homedir } from 'node:os';
5
+ import { mkdirSync } from 'node:fs';
6
+ import { join, dirname } from 'node:path';
7
+ import { applySchema } from './schema.js';
8
+ import { extractFacts } from './extractor.js';
9
+ import { embed } from './embedder.js';
10
+ import { formatExchange } from './chunker.js';
11
+ import { searchRawLog } from './raw-log-search.js';
12
+ import { searchFacts } from './fact-search.js';
13
+ export class LocalStore {
14
+ #db;
15
+ #userId;
16
+ #inFlightExtractions = new Set();
17
+ /** Expose database for plugin use (e.g., NudgeManager) */
18
+ get db() {
19
+ return this.#db;
20
+ }
21
+ /** Expose userId for plugin use */
22
+ get userId() {
23
+ return this.#userId;
24
+ }
25
+ constructor(options = {}) {
26
+ const dbPath = options.dbPath ?? join(homedir(), '.plumb', 'memory.db');
27
+ this.#userId = options.userId ?? 'default';
28
+ mkdirSync(dirname(dbPath), { recursive: true });
29
+ this.#db = new Database(dbPath);
30
+ this.#db.pragma('journal_mode = WAL');
31
+ this.#db.pragma('foreign_keys = ON');
32
+ // Load sqlite-vec extension — vector operations implemented in T-004.
33
+ sqliteVec.load(this.#db);
34
+ applySchema(this.#db);
35
+ }
36
+ async store(fact) {
37
+ const id = crypto.randomUUID();
38
+ // Embed concatenated fact text for vector search.
39
+ const text = `${fact.subject} ${fact.predicate} ${fact.object} ${fact.context ?? ''}`.trim();
40
+ const embedding = await embed(text);
41
+ const vecBlob = Buffer.from(embedding.buffer);
42
+ const doInsert = this.#db.transaction(() => {
43
+ this.#db.prepare(`
44
+ INSERT INTO facts
45
+ (id, user_id, subject, predicate, object,
46
+ confidence, decay_rate, timestamp, source_session_id,
47
+ source_session_label, context)
48
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
49
+ `).run(id, this.#userId, fact.subject, fact.predicate, fact.object, fact.confidence, fact.decayRate, fact.timestamp.toISOString(), fact.sourceSessionId, fact.sourceSessionLabel ?? null, fact.context ?? null);
50
+ // Insert embedding into vec_facts (auto-assigned rowid).
51
+ const vecInfo = this.#db.prepare(`INSERT INTO vec_facts(embedding) VALUES (?)`).run(vecBlob);
52
+ // Back-fill vec_rowid so fact-search can join without a mapping table.
53
+ this.#db.prepare(`UPDATE facts SET vec_rowid = ? WHERE id = ?`).run(vecInfo.lastInsertRowid, id);
54
+ });
55
+ doInsert();
56
+ return id;
57
+ }
58
+ async search(query, limit = 20) {
59
+ return searchFacts(this.#db, this.#userId, query, limit);
60
+ }
61
+ async delete(id) {
62
+ // Soft delete only — never hard delete.
63
+ this.#db.prepare(`
64
+ UPDATE facts SET deleted_at = ? WHERE id = ? AND user_id = ?
65
+ `).run(new Date().toISOString(), id, this.#userId);
66
+ }
67
+ async status() {
68
+ const factCount = this.#db.prepare(`SELECT COUNT(*) AS c FROM facts WHERE user_id = ? AND deleted_at IS NULL`).get(this.#userId).c;
69
+ const rawLogCount = this.#db.prepare(`SELECT COUNT(*) AS c FROM raw_log WHERE user_id = ?`).get(this.#userId).c;
70
+ const lastIngestionRow = this.#db.prepare(`SELECT MAX(timestamp) AS ts FROM raw_log WHERE user_id = ?`).get(this.#userId);
71
+ const pageCount = this.#db.pragma('page_count', { simple: true });
72
+ const pageSize = this.#db.pragma('page_size', { simple: true });
73
+ return {
74
+ factCount,
75
+ rawLogCount,
76
+ lastIngestion: lastIngestionRow.ts !== null ? new Date(lastIngestionRow.ts) : null,
77
+ storageBytes: pageCount * pageSize,
78
+ };
79
+ }
80
+ async ingest(exchange) {
81
+ const rawLogId = crypto.randomUUID();
82
+ const chunkText = formatExchange(exchange);
83
+ // Compute content hash for deduplication (scoped per userId).
84
+ const contentHash = createHash('sha256').update(chunkText).digest('hex');
85
+ // Embed before opening the synchronous DB transaction.
86
+ const embedding = await embed(chunkText);
87
+ const vecBlob = Buffer.from(embedding.buffer);
88
+ // Layer 1: write raw exchange to raw_log and store vector in vec_raw_log.
89
+ // vec_raw_log auto-assigns its own rowid; we store it back in raw_log.vec_rowid
90
+ // so raw-log-search can join the two tables without a separate mapping table.
91
+ const doInsert = this.#db.transaction(() => {
92
+ this.#db.prepare(`
93
+ INSERT INTO raw_log
94
+ (id, user_id, session_id, session_label,
95
+ user_message, agent_response, timestamp, source, chunk_text, chunk_index, content_hash)
96
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
97
+ `).run(rawLogId, this.#userId, exchange.sessionId, exchange.sessionLabel ?? null, exchange.userMessage, exchange.agentResponse, exchange.timestamp.toISOString(), exchange.source, chunkText, 0, contentHash);
98
+ // Insert embedding into sqlite-vec (auto-assigned rowid).
99
+ const vecInfo = this.#db.prepare(`INSERT INTO vec_raw_log(embedding) VALUES (?)`).run(vecBlob);
100
+ // Back-fill vec_rowid so raw-log-search can join without a mapping table.
101
+ this.#db.prepare(`UPDATE raw_log SET vec_rowid = ? WHERE id = ?`).run(vecInfo.lastInsertRowid, rawLogId);
102
+ });
103
+ // Attempt insert — catch UNIQUE constraint violations (duplicate content_hash).
104
+ try {
105
+ doInsert();
106
+ }
107
+ catch (err) {
108
+ // Check for SQLite UNIQUE constraint error on content_hash.
109
+ if (err instanceof Error && 'code' in err && err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
110
+ // Duplicate content — skip ingestion and fact extraction.
111
+ return {
112
+ rawLogId: '',
113
+ factsExtracted: 0,
114
+ factIds: [],
115
+ skipped: true,
116
+ };
117
+ }
118
+ // Re-throw other errors (e.g., real DB issues).
119
+ throw err;
120
+ }
121
+ // Layer 2: fire-and-forget fact extraction — never blocks ingest().
122
+ // Track the promise so drain() can wait for completion before close().
123
+ const extractionPromise = extractFacts(exchange, this.#userId, this)
124
+ .catch((err) => {
125
+ console.error('[plumb/local-store] Fact extraction failed:', err);
126
+ return []; // Return empty array on error
127
+ })
128
+ .finally(() => {
129
+ this.#inFlightExtractions.delete(extractionPromise);
130
+ });
131
+ this.#inFlightExtractions.add(extractionPromise);
132
+ return {
133
+ rawLogId,
134
+ factsExtracted: 0,
135
+ factIds: [],
136
+ };
137
+ }
138
+ /**
139
+ * Hybrid search over raw_log (Layer 1 retrieval).
140
+ * See raw-log-search.ts for the full pipeline description.
141
+ */
142
+ async searchRawLog(query, limit = 10) {
143
+ return searchRawLog(this.#db, this.#userId, query, limit);
144
+ }
145
+ /**
146
+ * Wait for all in-flight fact extractions to complete.
147
+ * Call this before close() to ensure all async work is done.
148
+ */
149
+ async drain() {
150
+ if (this.#inFlightExtractions.size === 0)
151
+ return;
152
+ await Promise.allSettled(Array.from(this.#inFlightExtractions));
153
+ }
154
+ /**
155
+ * Re-extract facts for orphaned raw_log chunks (chunks with no corresponding facts).
156
+ *
157
+ * This is useful when fact extraction failed during initial ingest (e.g., missing API key,
158
+ * rate limits, crashes). Re-running the normal seeder won't help because content-hash dedup
159
+ * skips already-ingested chunks before reaching the extraction phase.
160
+ *
161
+ * This method directly calls extractFacts() for each orphaned chunk, bypassing the dedup gate.
162
+ *
163
+ * @param throttleMs - Delay between extractions (default 1000ms) to stay under rate limits
164
+ * @returns Statistics: orphansFound, factsCreated
165
+ */
166
+ async reextractOrphans(throttleMs = 1000) {
167
+ // Query for raw_log entries with no corresponding facts.
168
+ // A session_id may have multiple raw_log chunks, but if ANY chunk has facts,
169
+ // we skip the entire session. This is conservative but prevents re-extracting
170
+ // sessions that already have partial facts.
171
+ const orphanRows = this.#db.prepare(`
172
+ SELECT
173
+ id,
174
+ user_id AS userId,
175
+ session_id AS sessionId,
176
+ session_label AS sessionLabel,
177
+ user_message AS userMessage,
178
+ agent_response AS agentResponse,
179
+ timestamp,
180
+ source
181
+ FROM raw_log
182
+ WHERE user_id = ?
183
+ AND NOT EXISTS (
184
+ SELECT 1 FROM facts
185
+ WHERE facts.source_session_id = raw_log.session_id
186
+ )
187
+ ORDER BY timestamp ASC
188
+ `).all(this.#userId);
189
+ const orphansFound = orphanRows.length;
190
+ if (orphansFound === 0) {
191
+ return { orphansFound: 0, factsCreated: 0 };
192
+ }
193
+ let factsCreated = 0;
194
+ for (let i = 0; i < orphanRows.length; i++) {
195
+ const row = orphanRows[i];
196
+ if (!row)
197
+ continue; // Type guard: skip if row is undefined (shouldn't happen)
198
+ // Reconstruct MessageExchange from raw_log data
199
+ const exchange = {
200
+ userMessage: row.userMessage,
201
+ agentResponse: row.agentResponse,
202
+ timestamp: new Date(row.timestamp),
203
+ source: row.source,
204
+ sessionId: row.sessionId,
205
+ // Conditionally include sessionLabel only if it's not null (exactOptionalPropertyTypes)
206
+ ...(row.sessionLabel !== null ? { sessionLabel: row.sessionLabel } : {}),
207
+ };
208
+ // Extract facts directly (bypasses ingest dedup gate)
209
+ try {
210
+ const facts = await extractFacts(exchange, this.#userId, this);
211
+ factsCreated += facts.length;
212
+ console.log(` ✅ [${i + 1}/${orphansFound}] Re-extracted ${facts.length} fact(s) from session ${row.sessionId}`);
213
+ }
214
+ catch (err) {
215
+ console.error(` ❌ [${i + 1}/${orphansFound}] Failed to re-extract facts from session ${row.sessionId}:`, err);
216
+ }
217
+ // Throttle to stay under rate limits (skip delay after last item)
218
+ if (i < orphanRows.length - 1) {
219
+ await new Promise(resolve => setTimeout(resolve, throttleMs));
220
+ }
221
+ }
222
+ return { orphansFound, factsCreated };
223
+ }
224
+ /**
225
+ * Get top subjects by fact count (for plumb status command).
226
+ * Returns subjects ordered by number of facts (non-deleted only).
227
+ */
228
+ topSubjects(userId, limit = 5) {
229
+ return this.#db.prepare(`
230
+ SELECT subject, COUNT(*) as count
231
+ FROM facts
232
+ WHERE user_id = ? AND deleted_at IS NULL
233
+ GROUP BY subject
234
+ ORDER BY count DESC
235
+ LIMIT ?
236
+ `).all(userId, limit);
237
+ }
238
+ /**
239
+ * Export all data for a user (for plumb export command).
240
+ * Returns raw database rows (no vector data).
241
+ * Includes soft-deleted facts for transparency.
242
+ */
243
+ exportAll(userId) {
244
+ // Export all non-deleted facts only (soft-deleted facts are excluded).
245
+ const factRows = this.#db.prepare(`
246
+ SELECT
247
+ id,
248
+ user_id AS userId,
249
+ subject,
250
+ predicate,
251
+ object,
252
+ confidence,
253
+ decay_rate AS decayRate,
254
+ timestamp,
255
+ source_session_id AS sourceSessionId,
256
+ source_session_label AS sourceSessionLabel,
257
+ context,
258
+ deleted_at AS deletedAt
259
+ FROM facts
260
+ WHERE user_id = ? AND deleted_at IS NULL
261
+ ORDER BY timestamp DESC
262
+ `).all(userId);
263
+ const facts = factRows.map((row) => ({
264
+ ...row,
265
+ deleted: false, // All exported facts are non-deleted
266
+ }));
267
+ // Export all raw_log entries (no vector data).
268
+ const rawLog = this.#db.prepare(`
269
+ SELECT
270
+ id,
271
+ user_id AS userId,
272
+ session_id AS sessionId,
273
+ session_label AS sessionLabel,
274
+ user_message AS userMessage,
275
+ agent_response AS agentResponse,
276
+ timestamp,
277
+ source,
278
+ chunk_text AS chunkText,
279
+ chunk_index AS chunkIndex,
280
+ content_hash AS contentHash
281
+ FROM raw_log
282
+ WHERE user_id = ?
283
+ ORDER BY timestamp DESC
284
+ `).all(userId);
285
+ return { facts, rawLog };
286
+ }
287
+ /** Close the database connection. Call when done (e.g. in tests). */
288
+ close() {
289
+ this.#db.close();
290
+ }
291
+ }
292
+ //# sourceMappingURL=local-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-store.js","sourceRoot":"","sources":["../src/local-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Raw-log hybrid search (Layer 1 retrieval).
3
+ *
4
+ * Pipeline:
5
+ * 1. BM25 keyword search over all raw_log chunk_text (built at query time)
6
+ * 2. KNN vector search via sqlite-vec vec_raw_log virtual table
7
+ * 3. Reciprocal Rank Fusion (RRF, k=60) merges both ranked lists
8
+ * 4. Recency decay: score *= e^(-0.012 × age_in_days)
9
+ * 5. Cross-encoder reranker (top-20 candidates → Xenova/ms-marco-MiniLM-L-6-v2)
10
+ * 6. Return top-k by reranker score (falls back to RRF×decay if reranker fails)
11
+ *
12
+ * Search is cross-session: no session filter is applied.
13
+ * The caller (LocalStore) passes its internal db handle — no separate DB connection.
14
+ */
15
+ import type Database from 'better-sqlite3';
16
+ export interface RawLogSearchResult {
17
+ readonly chunk_text: string;
18
+ readonly session_id: string;
19
+ readonly session_label: string | null;
20
+ readonly timestamp: string;
21
+ /** Final score after RRF × recency_decay × reranker (or RRF × recency if reranker failed). */
22
+ readonly final_score: number;
23
+ }
24
+ /**
25
+ * Hybrid search over raw_log.
26
+ *
27
+ * @param db The better-sqlite3 Database instance (with sqlite-vec loaded)
28
+ * @param userId Scopes the search to this user's data
29
+ * @param query Natural language query string
30
+ * @param limit Number of results to return (default 10)
31
+ */
32
+ export declare function searchRawLog(db: Database.Database, userId: string, query: string, limit?: number): Promise<readonly RawLogSearchResult[]>;
33
+ //# sourceMappingURL=raw-log-search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw-log-search.d.ts","sourceRoot":"","sources":["../src/raw-log-search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAe3C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8FAA8F;IAC9F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAsDD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,KAAK,SAAK,GACT,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAoGxC"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Raw-log hybrid search (Layer 1 retrieval).
3
+ *
4
+ * Pipeline:
5
+ * 1. BM25 keyword search over all raw_log chunk_text (built at query time)
6
+ * 2. KNN vector search via sqlite-vec vec_raw_log virtual table
7
+ * 3. Reciprocal Rank Fusion (RRF, k=60) merges both ranked lists
8
+ * 4. Recency decay: score *= e^(-0.012 × age_in_days)
9
+ * 5. Cross-encoder reranker (top-20 candidates → Xenova/ms-marco-MiniLM-L-6-v2)
10
+ * 6. Return top-k by reranker score (falls back to RRF×decay if reranker fails)
11
+ *
12
+ * Search is cross-session: no session filter is applied.
13
+ * The caller (LocalStore) passes its internal db handle — no separate DB connection.
14
+ */
15
+ import { Bm25 } from './bm25.js';
16
+ import { embedQuery, rerankScores } from './embedder.js';
17
+ // RRF constant (standard k=60; higher = less weight on top-1 rank).
18
+ const RRF_K = 60;
19
+ // Recency decay lambda for raw logs (medium — half-life ≈ 58 days).
20
+ const RECENCY_LAMBDA = 0.012;
21
+ // Number of candidates passed to the cross-encoder.
22
+ const RERANK_TOP_K = 20;
23
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
24
+ function ageInDays(timestamp) {
25
+ return (Date.now() - new Date(timestamp).getTime()) / (1_000 * 60 * 60 * 24);
26
+ }
27
+ function recencyDecay(timestamp) {
28
+ return Math.exp(-RECENCY_LAMBDA * ageInDays(timestamp));
29
+ }
30
+ /**
31
+ * Merge two ranked lists via Reciprocal Rank Fusion.
32
+ * Each list entry is [id, score] ordered by descending relevance (rank 1 = index 0).
33
+ * Returns a map of id → rrf_score.
34
+ */
35
+ function rrf(vecRanked, bm25Ranked) {
36
+ const scores = new Map();
37
+ for (let rank = 0; rank < vecRanked.length; rank++) {
38
+ const id = vecRanked[rank]?.[0];
39
+ if (id === undefined)
40
+ continue;
41
+ scores.set(id, (scores.get(id) ?? 0) + 1 / (RRF_K + rank + 1));
42
+ }
43
+ for (let rank = 0; rank < bm25Ranked.length; rank++) {
44
+ const id = bm25Ranked[rank]?.[0];
45
+ if (id === undefined)
46
+ continue;
47
+ scores.set(id, (scores.get(id) ?? 0) + 1 / (RRF_K + rank + 1));
48
+ }
49
+ return scores;
50
+ }
51
+ // ─── Public API ───────────────────────────────────────────────────────────────
52
+ /**
53
+ * Hybrid search over raw_log.
54
+ *
55
+ * @param db The better-sqlite3 Database instance (with sqlite-vec loaded)
56
+ * @param userId Scopes the search to this user's data
57
+ * @param query Natural language query string
58
+ * @param limit Number of results to return (default 10)
59
+ */
60
+ export async function searchRawLog(db, userId, query, limit = 10) {
61
+ // ── 1. Fetch all raw_log rows for this user ──────────────────────────────
62
+ const allRows = db
63
+ .prepare(`SELECT id, session_id, session_label, timestamp, chunk_text
64
+ FROM raw_log
65
+ WHERE user_id = ?
66
+ ORDER BY timestamp DESC`)
67
+ .all(userId);
68
+ if (allRows.length === 0)
69
+ return [];
70
+ const idToRow = new Map(allRows.map((r) => [r.id, r]));
71
+ // ── 2. BM25 search ───────────────────────────────────────────────────────
72
+ const corpus = allRows.map((r) => r.chunk_text);
73
+ const bm25 = new Bm25(corpus);
74
+ const bm25RawScores = bm25.scores(query);
75
+ const bm25Ranked = allRows
76
+ .map((r, i) => [r.id, bm25RawScores[i] ?? 0])
77
+ .sort((a, b) => b[1] - a[1]);
78
+ // ── 3. Vector search via sqlite-vec ─────────────────────────────────────
79
+ const queryVec = await embedQuery(query);
80
+ const queryBlob = Buffer.from(queryVec.buffer);
81
+ // Fetch enough candidates for RRF merge (all docs for small corpora, top-N otherwise).
82
+ const vecFetchLimit = Math.min(allRows.length, Math.max(RERANK_TOP_K * 2, limit * 3, 50));
83
+ // Query vec_raw_log for nearest neighbours; resolve back to raw_log via vec_rowid column.
84
+ const vecRows = db
85
+ .prepare(`SELECT rowid, distance FROM vec_raw_log
86
+ WHERE embedding MATCH ?
87
+ ORDER BY distance
88
+ LIMIT ?`)
89
+ .all(queryBlob, vecFetchLimit);
90
+ const vecRanked = [];
91
+ for (const vecRow of vecRows) {
92
+ const logRow = db
93
+ .prepare(`SELECT id FROM raw_log WHERE vec_rowid = ? AND user_id = ?`)
94
+ .get(vecRow.rowid, userId);
95
+ if (logRow !== undefined) {
96
+ // sqlite-vec cosine distance is in [0,2] for normalized vectors.
97
+ // Similarity = 1 − distance gives [−1,1]; in practice ≥ 0 for meaningful matches.
98
+ vecRanked.push([logRow.id, 1 - vecRow.distance]);
99
+ }
100
+ }
101
+ // ── 4. RRF merge ────────────────────────────────────────────────────────
102
+ const rrfScores = rrf(vecRanked, bm25Ranked);
103
+ // ── 5. Recency decay ────────────────────────────────────────────────────
104
+ const decayedScores = [];
105
+ for (const [id, rrfScore] of rrfScores) {
106
+ const row = idToRow.get(id);
107
+ if (row === undefined)
108
+ continue;
109
+ const decay = recencyDecay(row.timestamp);
110
+ decayedScores.push([id, rrfScore * decay]);
111
+ }
112
+ decayedScores.sort((a, b) => b[1] - a[1]);
113
+ // ── 6. Take top candidates for reranking ────────────────────────────────
114
+ const candidates = decayedScores.slice(0, Math.max(RERANK_TOP_K, limit));
115
+ // ── 7. Cross-encoder reranking ──────────────────────────────────────────
116
+ const passages = candidates.map(([id]) => idToRow.get(id)?.chunk_text ?? '');
117
+ const rerankerScores = await rerankScores(query, passages);
118
+ // Detect all-zero fallback (reranker unavailable) → keep RRF×decay order.
119
+ const hasRerankerSignal = rerankerScores.some((s) => s !== 0);
120
+ const ranked = candidates.map(([id, rrfDecayScore], i) => ({
121
+ id,
122
+ finalScore: hasRerankerSignal ? (rerankerScores[i] ?? 0) : rrfDecayScore,
123
+ }));
124
+ ranked.sort((a, b) => b.finalScore - a.finalScore);
125
+ // ── 8. Build output ─────────────────────────────────────────────────────
126
+ return ranked.slice(0, limit).map(({ id, finalScore }) => {
127
+ const row = idToRow.get(id);
128
+ return {
129
+ chunk_text: row.chunk_text,
130
+ session_id: row.session_id,
131
+ session_label: row.session_label,
132
+ timestamp: row.timestamp,
133
+ final_score: finalScore,
134
+ };
135
+ });
136
+ }
137
+ //# sourceMappingURL=raw-log-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw-log-search.js","sourceRoot":"","sources":["../src/raw-log-search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEzD,oEAAoE;AACpE,MAAM,KAAK,GAAG,EAAE,CAAC;AAEjB,oEAAoE;AACpE,MAAM,cAAc,GAAG,KAAK,CAAC;AAE7B,oDAAoD;AACpD,MAAM,YAAY,GAAG,EAAE,CAAC;AA4BxB,iFAAiF;AAEjF,SAAS,SAAS,CAAC,SAAiB;IAClC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;AAC/E,CAAC;AAED,SAAS,YAAY,CAAC,SAAiB;IACrC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,cAAc,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,SAAS,GAAG,CACV,SAAkC,EAClC,UAAmC;IAEnC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEzC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;QACnD,MAAM,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,EAAE,KAAK,SAAS;YAAE,SAAS;QAC/B,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,UAAU,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;QACpD,MAAM,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACjC,IAAI,EAAE,KAAK,SAAS;YAAE,SAAS;QAC/B,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,iFAAiF;AAEjF;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,EAAqB,EACrB,MAAc,EACd,KAAa,EACb,KAAK,GAAG,EAAE;IAEV,4EAA4E;IAC5E,MAAM,OAAO,GAAG,EAAE;SACf,OAAO,CACN;;;+BAGyB,CAC1B;SACA,GAAG,CAAC,MAAM,CAAC,CAAC;IAEf,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEpC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAoB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAE1E,4EAA4E;IAC5E,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAEzC,MAAM,UAAU,GAA4B,OAAO;SAChD,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAoB,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;SAC9D,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE/B,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE/C,uFAAuF;IACvF,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAE1F,0FAA0F;IAC1F,MAAM,OAAO,GAAG,EAAE;SACf,OAAO,CACN;;;eAGS,CACV;SACA,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IAIjC,MAAM,SAAS,GAA4B,EAAE,CAAC;IAC9C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,EAAE;aACd,OAAO,CACN,4DAA4D,CAC7D;aACA,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC7B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,iEAAiE;YACjE,kFAAkF;YAClF,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAE7C,2EAA2E;IAC3E,MAAM,aAAa,GAA4B,EAAE,CAAC;IAClD,KAAK,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC,IAAI,SAAS,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,GAAG,KAAK,SAAS;YAAE,SAAS;QAChC,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC1C,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC;IAC7C,CAAC;IACD,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE1C,2EAA2E;IAC3E,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC;IAEzE,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC;IAC7E,MAAM,cAAc,GAAG,MAAM,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAE3D,0EAA0E;IAC1E,MAAM,iBAAiB,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAE9D,MAAM,MAAM,GAA8C,UAAU,CAAC,GAAG,CACtE,CAAC,CAAC,EAAE,EAAE,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3B,EAAE;QACF,UAAU,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa;KACzE,CAAC,CACH,CAAC;IAEF,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC;IAEnD,2EAA2E;IAC3E,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE;QACvD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAE,CAAC;QAC7B,OAAO;YACL,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,aAAa,EAAE,GAAG,CAAC,aAAa;YAChC,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,WAAW,EAAE,UAAU;SACxB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Read path — Layer 1 + Layer 2 retrieval, scoring, and tiering.
3
+ *
4
+ * buildMemoryContext() is called before every agent response. It queries both
5
+ * memory layers in parallel, re-scores facts with decay, tiers by confidence
6
+ * band, and returns a MemoryContext ready for formatContextBlock().
7
+ *
8
+ * Search is always cross-session — no session filter is applied.
9
+ */
10
+ import type { SearchResult, Fact } from './types.js';
11
+ import type { RawLogSearchResult } from './local-store.js';
12
+ export interface ScoredFact {
13
+ readonly fact: Fact;
14
+ /** Decay-adjusted score in [0,1]. */
15
+ readonly score: number;
16
+ readonly ageInDays: number;
17
+ }
18
+ export interface RawChunk {
19
+ readonly chunkText: string;
20
+ readonly sessionId: string;
21
+ readonly sessionLabel: string | null;
22
+ readonly timestamp: Date;
23
+ /** Final score from hybrid search (RRF × decay × reranker). */
24
+ readonly score: number;
25
+ }
26
+ export interface MemoryContext {
27
+ readonly highConfidence: ScoredFact[];
28
+ readonly mediumConfidence: ScoredFact[];
29
+ readonly lowConfidence: ScoredFact[];
30
+ readonly relatedConversations: RawChunk[];
31
+ }
32
+ export interface ReadPathOptions {
33
+ /** Max facts returned per confidence tier. Default: 5. */
34
+ maxFactsPerTier?: number;
35
+ /** Max raw log chunks returned. Default: 3. */
36
+ maxRawChunks?: number;
37
+ /** Point-in-time for decay computation. Default: new Date(). */
38
+ now?: Date;
39
+ }
40
+ /**
41
+ * Minimal store interface required by the read path.
42
+ * LocalStore satisfies this; tests can pass a mock.
43
+ */
44
+ export interface ReadPathStore {
45
+ search(query: string, limit?: number): Promise<readonly SearchResult[]>;
46
+ searchRawLog(query: string, limit?: number): Promise<readonly RawLogSearchResult[]>;
47
+ }
48
+ /**
49
+ * Build a structured MemoryContext for a given query.
50
+ *
51
+ * Queries Layer 1 (raw log hybrid search) and Layer 2 (fact store) in parallel.
52
+ * Facts are re-scored with scoreFact() and tiered by confidence band.
53
+ * Raw chunks are returned in hybrid-search score order.
54
+ *
55
+ * @param query Natural language query (the incoming user message or context).
56
+ * @param store Any store implementing ReadPathStore (typically LocalStore).
57
+ * @param options Optional limits and now-override.
58
+ */
59
+ export declare function buildMemoryContext(query: string, store: ReadPathStore, options?: ReadPathOptions): Promise<MemoryContext>;
60
+ //# sourceMappingURL=read-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-path.d.ts","sourceRoot":"","sources":["../src/read-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAI3D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,qCAAqC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,cAAc,EAAE,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,gBAAgB,EAAE,UAAU,EAAE,CAAC;IACxC,QAAQ,CAAC,aAAa,EAAE,UAAU,EAAE,CAAC;IACrC,QAAQ,CAAC,oBAAoB,EAAE,QAAQ,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,CAAC;IACxE,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;CACrF;AASD;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,aAAa,CAAC,CAqDxB"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Read path — Layer 1 + Layer 2 retrieval, scoring, and tiering.
3
+ *
4
+ * buildMemoryContext() is called before every agent response. It queries both
5
+ * memory layers in parallel, re-scores facts with decay, tiers by confidence
6
+ * band, and returns a MemoryContext ready for formatContextBlock().
7
+ *
8
+ * Search is always cross-session — no session filter is applied.
9
+ */
10
+ import { scoreFact } from './scorer.js';
11
+ // ─── Confidence band boundaries ───────────────────────────────────────────────
12
+ const HIGH_THRESHOLD = 0.7;
13
+ const LOW_THRESHOLD = 0.3;
14
+ // ─── Public API ───────────────────────────────────────────────────────────────
15
+ /**
16
+ * Build a structured MemoryContext for a given query.
17
+ *
18
+ * Queries Layer 1 (raw log hybrid search) and Layer 2 (fact store) in parallel.
19
+ * Facts are re-scored with scoreFact() and tiered by confidence band.
20
+ * Raw chunks are returned in hybrid-search score order.
21
+ *
22
+ * @param query Natural language query (the incoming user message or context).
23
+ * @param store Any store implementing ReadPathStore (typically LocalStore).
24
+ * @param options Optional limits and now-override.
25
+ */
26
+ export async function buildMemoryContext(query, store, options) {
27
+ const maxFactsPerTier = options?.maxFactsPerTier ?? 5;
28
+ const maxRawChunks = options?.maxRawChunks ?? 3;
29
+ const now = options?.now ?? new Date();
30
+ // Fetch more candidates than needed so tiering has enough to fill each band.
31
+ const factCandidateLimit = maxFactsPerTier * 3 * 3; // 3 tiers × 3× headroom
32
+ const rawCandidateLimit = maxRawChunks * 3;
33
+ // ── Query Layer 1 and Layer 2 in parallel ─────────────────────────────────
34
+ const [searchResults, rawLogResults] = await Promise.all([
35
+ store.search(query, factCandidateLimit),
36
+ store.searchRawLog(query, rawCandidateLimit),
37
+ ]);
38
+ // ── Re-score facts with decay ──────────────────────────────────────────────
39
+ const scoredFacts = searchResults.map((result) => {
40
+ const { score } = scoreFact(result.fact, now);
41
+ const ageInDays = (now.getTime() - result.fact.timestamp.getTime()) / (1_000 * 60 * 60 * 24);
42
+ return { fact: result.fact, score, ageInDays };
43
+ });
44
+ // Sort by score descending.
45
+ scoredFacts.sort((a, b) => b.score - a.score);
46
+ // ── Tier facts into confidence bands ──────────────────────────────────────
47
+ const highConfidence = [];
48
+ const mediumConfidence = [];
49
+ const lowConfidence = [];
50
+ for (const sf of scoredFacts) {
51
+ if (sf.score > HIGH_THRESHOLD) {
52
+ if (highConfidence.length < maxFactsPerTier)
53
+ highConfidence.push(sf);
54
+ }
55
+ else if (sf.score >= LOW_THRESHOLD) {
56
+ if (mediumConfidence.length < maxFactsPerTier)
57
+ mediumConfidence.push(sf);
58
+ }
59
+ else {
60
+ if (lowConfidence.length < maxFactsPerTier)
61
+ lowConfidence.push(sf);
62
+ }
63
+ }
64
+ // ── Build raw chunks (already ranked by hybrid search score) ──────────────
65
+ const relatedConversations = rawLogResults
66
+ .slice(0, maxRawChunks)
67
+ .map((r) => ({
68
+ chunkText: r.chunk_text,
69
+ sessionId: r.session_id,
70
+ sessionLabel: r.session_label,
71
+ timestamp: new Date(r.timestamp),
72
+ score: r.final_score,
73
+ }));
74
+ return { highConfidence, mediumConfidence, lowConfidence, relatedConversations };
75
+ }
76
+ //# sourceMappingURL=read-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-path.js","sourceRoot":"","sources":["../src/read-path.ts"],"names":[],"mappings":"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"}