learning-agent 0.2.2 → 0.2.3

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/dist/index.d.ts CHANGED
@@ -222,61 +222,40 @@ declare function appendLesson(repoRoot: string, lesson: Lesson): Promise<void>;
222
222
  declare function readLessons(repoRoot: string, options?: ReadLessonsOptions): Promise<ReadLessonsResult>;
223
223
 
224
224
  /**
225
- * SQLite storage layer with FTS5 for full-text search
226
- *
227
- * Rebuildable index - not the source of truth.
228
- * Stored in .claude/.cache (gitignored).
225
+ * SQLite database connection management.
229
226
  */
230
227
 
231
228
  /** Relative path to database file from repo root */
232
229
  declare const DB_PATH = ".claude/.cache/lessons.sqlite";
233
230
  /**
234
- * Close the database connection and release resources.
235
- *
236
- * **Resource lifecycle:**
237
- * - The database is opened lazily on first call to `openDb()` or any function that uses it
238
- * (e.g., `searchKeyword`, `rebuildIndex`, `syncIfNeeded`, `getCachedEmbedding`)
239
- * - Once opened, the connection remains active until `closeDb()` is called
240
- * - After closing, subsequent database operations will reopen the connection
241
- *
242
- * **When to call:**
243
- * - At the end of CLI commands to ensure clean process exit
244
- * - When transitioning between repositories in long-running processes
245
- * - Before process exit in graceful shutdown handlers
246
- *
247
- * **Best practices for long-running processes:**
248
- * - In single-operation scripts: call before exit
249
- * - In daemon/server processes: call in shutdown handler
250
- * - Not necessary to call between operations in the same repository
251
- *
252
- * @example
253
- * ```typescript
254
- * // CLI command pattern
255
- * try {
256
- * await searchKeyword(repoRoot, 'typescript', 10);
257
- * // ... process results
258
- * } finally {
259
- * closeDb();
260
- * }
261
- *
262
- * // Graceful shutdown pattern
263
- * process.on('SIGTERM', () => {
264
- * closeDb();
265
- * process.exit(0);
266
- * });
267
- * ```
231
+ * Close the SQLite database connection.
268
232
  */
269
233
  declare function closeDb(): void;
234
+
235
+ /**
236
+ * SQLite index synchronization with JSONL source of truth.
237
+ */
238
+
270
239
  /**
271
- * Rebuild the SQLite index from the JSONL source of truth.
272
- * Preserves embeddings where content hash is unchanged.
273
- * Updates the last sync mtime after successful rebuild.
240
+ * Rebuild the SQLite index from JSONL source of truth.
241
+ * Gracefully degrades: no-op with warning if SQLite unavailable.
242
+ * Preserves cached embeddings when lesson content hasn't changed.
243
+ * @param repoRoot - Absolute path to repository root
274
244
  */
275
245
  declare function rebuildIndex(repoRoot: string): Promise<void>;
246
+
247
+ /**
248
+ * SQLite search operations using FTS5 full-text search.
249
+ */
250
+
276
251
  /**
277
- * Search lessons using FTS5 keyword search.
278
- * Returns matching lessons up to the specified limit.
279
- * Increments retrieval count for all returned lessons.
252
+ * Search lessons using FTS5 full-text search.
253
+ * Does NOT degrade gracefully: throws error if SQLite unavailable.
254
+ * @param repoRoot - Absolute path to repository root
255
+ * @param query - FTS5 query string
256
+ * @param limit - Maximum number of results
257
+ * @returns Matching lessons
258
+ * @throws Error if SQLite unavailable (FTS5 required)
280
259
  */
281
260
  declare function searchKeyword(repoRoot: string, query: string, limit: number): Promise<Lesson[]>;
282
261
 
@@ -813,6 +792,10 @@ declare function formatLessonsCheck(lessons: ScoredLesson[]): string;
813
792
  * @see {@link unloadEmbedding} for embedding model cleanup
814
793
  * @module learning-agent
815
794
  */
816
- declare const VERSION = "0.1.0";
795
+ /**
796
+ * Package version - must match package.json.
797
+ * Update this when releasing a new version.
798
+ */
799
+ declare const VERSION = "0.2.3";
817
800
 
818
801
  export { type ActionabilityResult, type Context, type CorrectionSignal, DB_PATH, type DetectedCorrection, type DetectedSelfCorrection, type DetectedTestFailure, type EditEntry, type EditHistory, LESSONS_PATH, type Lesson, LessonSchema, type LessonType, LessonTypeSchema, MODEL_FILENAME, MODEL_URI, type NoveltyOptions, type NoveltyResult, type ParseError, type PlanRetrievalResult, type ProposeResult, type RankedLesson, type ReadLessonsOptions, type ReadLessonsResult, type ScoredLesson, type SearchVectorOptions, type Severity, type Source, type SpecificityResult, type TestResult, type Tombstone, TombstoneSchema, VERSION, appendLesson, calculateScore, closeDb, confirmationBoost, cosineSimilarity, detectSelfCorrection, detectTestFailure, detectUserCorrection, embedText, embedTexts, formatLessonsCheck, generateId, getEmbedding, isActionable, isModelAvailable, isNovel, isSpecific, loadSessionLessons, rankLessons, readLessons, rebuildIndex, recencyBoost, resolveModel, retrieveForPlan, searchKeyword, searchVector, severityBoost, shouldPropose, unloadEmbedding };
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import { mkdir, appendFile, readFile } from 'fs/promises';
2
2
  import { join, dirname } from 'path';
3
3
  import { createHash } from 'crypto';
4
4
  import { z } from 'zod';
5
+ import { createRequire } from 'module';
5
6
  import { existsSync, mkdirSync, statSync } from 'fs';
6
- import Database from 'better-sqlite3';
7
7
  import { resolveModelFile, getLlama } from 'node-llama-cpp';
8
8
  import { homedir } from 'os';
9
9
 
@@ -158,9 +158,45 @@ async function readLessons(repoRoot, options = {}) {
158
158
  }
159
159
  return { lessons: Array.from(lessons.values()), skippedCount };
160
160
  }
161
- var DB_PATH = ".claude/.cache/lessons.sqlite";
161
+ var require2 = createRequire(import.meta.url);
162
+ var sqliteAvailable = null;
163
+ var sqliteWarningLogged = false;
164
+ var DatabaseConstructor = null;
165
+ function isSqliteAvailable() {
166
+ if (sqliteAvailable !== null) {
167
+ return sqliteAvailable;
168
+ }
169
+ try {
170
+ const module = require2("better-sqlite3");
171
+ const Constructor = module.default || module;
172
+ const testDb = new Constructor(":memory:");
173
+ testDb.close();
174
+ DatabaseConstructor = Constructor;
175
+ sqliteAvailable = true;
176
+ } catch {
177
+ sqliteAvailable = false;
178
+ if (!sqliteWarningLogged) {
179
+ console.warn("SQLite unavailable, running in JSONL-only mode");
180
+ sqliteWarningLogged = true;
181
+ }
182
+ }
183
+ return sqliteAvailable;
184
+ }
185
+ function logDegradationWarning() {
186
+ if (!sqliteAvailable && !sqliteWarningLogged) {
187
+ console.warn("SQLite unavailable, running in JSONL-only mode");
188
+ sqliteWarningLogged = true;
189
+ }
190
+ }
191
+ function getDatabaseConstructor() {
192
+ if (!isSqliteAvailable()) {
193
+ return null;
194
+ }
195
+ return DatabaseConstructor;
196
+ }
197
+
198
+ // src/storage/sqlite/schema.ts
162
199
  var SCHEMA_SQL = `
163
- -- Main lessons table
164
200
  CREATE TABLE IF NOT EXISTS lessons (
165
201
  id TEXT PRIMARY KEY,
166
202
  type TEXT NOT NULL,
@@ -180,7 +216,6 @@ var SCHEMA_SQL = `
180
216
  last_retrieved TEXT,
181
217
  embedding BLOB,
182
218
  content_hash TEXT,
183
- -- v0.2.2 fields
184
219
  invalidated_at TEXT,
185
220
  invalidation_reason TEXT,
186
221
  citation_file TEXT,
@@ -190,29 +225,21 @@ var SCHEMA_SQL = `
190
225
  compacted_at TEXT
191
226
  );
192
227
 
193
- -- FTS5 virtual table for full-text search
194
228
  CREATE VIRTUAL TABLE IF NOT EXISTS lessons_fts USING fts5(
195
- id,
196
- trigger,
197
- insight,
198
- tags,
199
- content='lessons',
200
- content_rowid='rowid'
229
+ id, trigger, insight, tags,
230
+ content='lessons', content_rowid='rowid'
201
231
  );
202
232
 
203
- -- Trigger to sync FTS on INSERT
204
233
  CREATE TRIGGER IF NOT EXISTS lessons_ai AFTER INSERT ON lessons BEGIN
205
234
  INSERT INTO lessons_fts(rowid, id, trigger, insight, tags)
206
235
  VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
207
236
  END;
208
237
 
209
- -- Trigger to sync FTS on DELETE
210
238
  CREATE TRIGGER IF NOT EXISTS lessons_ad AFTER DELETE ON lessons BEGIN
211
239
  INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
212
240
  VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
213
241
  END;
214
242
 
215
- -- Trigger to sync FTS on UPDATE
216
243
  CREATE TRIGGER IF NOT EXISTS lessons_au AFTER UPDATE ON lessons BEGIN
217
244
  INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
218
245
  VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
@@ -220,12 +247,10 @@ var SCHEMA_SQL = `
220
247
  VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
221
248
  END;
222
249
 
223
- -- Index for common queries
224
250
  CREATE INDEX IF NOT EXISTS idx_lessons_created ON lessons(created);
225
251
  CREATE INDEX IF NOT EXISTS idx_lessons_confirmed ON lessons(confirmed);
226
252
  CREATE INDEX IF NOT EXISTS idx_lessons_severity ON lessons(severity);
227
253
 
228
- -- Metadata table for sync tracking
229
254
  CREATE TABLE IF NOT EXISTS metadata (
230
255
  key TEXT PRIMARY KEY,
231
256
  value TEXT NOT NULL
@@ -234,17 +259,35 @@ var SCHEMA_SQL = `
234
259
  function createSchema(database) {
235
260
  database.exec(SCHEMA_SQL);
236
261
  }
262
+
263
+ // src/storage/sqlite/connection.ts
264
+ var DB_PATH = ".claude/.cache/lessons.sqlite";
237
265
  var db = null;
238
- function contentHash(trigger, insight) {
239
- return createHash("sha256").update(`${trigger} ${insight}`).digest("hex");
240
- }
241
- function openDb(repoRoot) {
242
- if (db) return db;
243
- const dbPath = join(repoRoot, DB_PATH);
244
- const dir = dirname(dbPath);
245
- mkdirSync(dir, { recursive: true });
246
- db = new Database(dbPath);
247
- db.pragma("journal_mode = WAL");
266
+ var dbIsInMemory = false;
267
+ function openDb(repoRoot, options = {}) {
268
+ if (!isSqliteAvailable()) {
269
+ return null;
270
+ }
271
+ const { inMemory = false } = options;
272
+ if (db) {
273
+ if (inMemory !== dbIsInMemory) {
274
+ closeDb();
275
+ } else {
276
+ return db;
277
+ }
278
+ }
279
+ const Database = getDatabaseConstructor();
280
+ if (inMemory) {
281
+ db = new Database(":memory:");
282
+ dbIsInMemory = true;
283
+ } else {
284
+ const dbPath = join(repoRoot, DB_PATH);
285
+ const dir = dirname(dbPath);
286
+ mkdirSync(dir, { recursive: true });
287
+ db = new Database(dbPath);
288
+ dbIsInMemory = false;
289
+ db.pragma("journal_mode = WAL");
290
+ }
248
291
  createSchema(db);
249
292
  return db;
250
293
  }
@@ -252,10 +295,18 @@ function closeDb() {
252
295
  if (db) {
253
296
  db.close();
254
297
  db = null;
298
+ dbIsInMemory = false;
255
299
  }
256
300
  }
301
+ function contentHash(trigger, insight) {
302
+ return createHash("sha256").update(`${trigger} ${insight}`).digest("hex");
303
+ }
257
304
  function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
258
305
  const database = openDb(repoRoot);
306
+ if (!database) {
307
+ logDegradationWarning();
308
+ return null;
309
+ }
259
310
  const row = database.prepare("SELECT embedding, content_hash FROM lessons WHERE id = ?").get(lessonId);
260
311
  if (!row || !row.embedding || !row.content_hash) {
261
312
  return null;
@@ -272,60 +323,14 @@ function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
272
323
  }
273
324
  function setCachedEmbedding(repoRoot, lessonId, embedding, hash) {
274
325
  const database = openDb(repoRoot);
326
+ if (!database) {
327
+ logDegradationWarning();
328
+ return;
329
+ }
275
330
  const float32 = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
276
331
  const buffer = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
277
332
  database.prepare("UPDATE lessons SET embedding = ?, content_hash = ? WHERE id = ?").run(buffer, hash, lessonId);
278
333
  }
279
- function rowToLesson(row) {
280
- const lesson = {
281
- id: row.id,
282
- type: row.type,
283
- trigger: row.trigger,
284
- insight: row.insight,
285
- tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
286
- source: row.source,
287
- context: JSON.parse(row.context),
288
- supersedes: JSON.parse(row.supersedes),
289
- related: JSON.parse(row.related),
290
- created: row.created,
291
- confirmed: row.confirmed === 1
292
- };
293
- if (row.evidence !== null) {
294
- lesson.evidence = row.evidence;
295
- }
296
- if (row.severity !== null) {
297
- lesson.severity = row.severity;
298
- }
299
- if (row.deleted === 1) {
300
- lesson.deleted = true;
301
- }
302
- if (row.retrieval_count > 0) {
303
- lesson.retrievalCount = row.retrieval_count;
304
- }
305
- if (row.invalidated_at !== null) {
306
- lesson.invalidatedAt = row.invalidated_at;
307
- }
308
- if (row.invalidation_reason !== null) {
309
- lesson.invalidationReason = row.invalidation_reason;
310
- }
311
- if (row.citation_file !== null) {
312
- lesson.citation = {
313
- file: row.citation_file,
314
- ...row.citation_line !== null && { line: row.citation_line },
315
- ...row.citation_commit !== null && { commit: row.citation_commit }
316
- };
317
- }
318
- if (row.compaction_level !== null && row.compaction_level !== 0) {
319
- lesson.compactionLevel = row.compaction_level;
320
- }
321
- if (row.compacted_at !== null) {
322
- lesson.compactedAt = row.compacted_at;
323
- }
324
- if (row.last_retrieved !== null) {
325
- lesson.lastRetrieved = row.last_retrieved;
326
- }
327
- return lesson;
328
- }
329
334
  function collectCachedEmbeddings(database) {
330
335
  const cache = /* @__PURE__ */ new Map();
331
336
  const rows = database.prepare("SELECT id, embedding, content_hash FROM lessons WHERE embedding IS NOT NULL").all();
@@ -358,6 +363,10 @@ function setLastSyncMtime(database, mtime) {
358
363
  }
359
364
  async function rebuildIndex(repoRoot) {
360
365
  const database = openDb(repoRoot);
366
+ if (!database) {
367
+ logDegradationWarning();
368
+ return;
369
+ }
361
370
  const { lessons } = await readLessons(repoRoot);
362
371
  const cachedEmbeddings = collectCachedEmbeddings(database);
363
372
  database.exec("DELETE FROM lessons");
@@ -393,7 +402,6 @@ async function rebuildIndex(repoRoot) {
393
402
  last_retrieved: lesson.lastRetrieved ?? null,
394
403
  embedding: hasValidCache ? cached.embedding : null,
395
404
  content_hash: hasValidCache ? cached.contentHash : null,
396
- // v0.2.2 fields
397
405
  invalidated_at: lesson.invalidatedAt ?? null,
398
406
  invalidation_reason: lesson.invalidationReason ?? null,
399
407
  citation_file: lesson.citation?.file ?? null,
@@ -411,12 +419,17 @@ async function rebuildIndex(repoRoot) {
411
419
  }
412
420
  }
413
421
  async function syncIfNeeded(repoRoot, options = {}) {
422
+ if (!isSqliteAvailable()) {
423
+ logDegradationWarning();
424
+ return false;
425
+ }
414
426
  const { force = false } = options;
415
427
  const jsonlMtime = getJsonlMtime(repoRoot);
416
428
  if (jsonlMtime === null && !force) {
417
429
  return false;
418
430
  }
419
431
  const database = openDb(repoRoot);
432
+ if (!database) return false;
420
433
  const lastSyncMtime = getLastSyncMtime(database);
421
434
  const needsRebuild = force || lastSyncMtime === null || jsonlMtime !== null && jsonlMtime > lastSyncMtime;
422
435
  if (needsRebuild) {
@@ -425,8 +438,70 @@ async function syncIfNeeded(repoRoot, options = {}) {
425
438
  }
426
439
  return false;
427
440
  }
441
+
442
+ // src/storage/sqlite/search.ts
443
+ function rowToLesson(row) {
444
+ const lesson = {
445
+ id: row.id,
446
+ type: row.type,
447
+ trigger: row.trigger,
448
+ insight: row.insight,
449
+ tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
450
+ source: row.source,
451
+ context: JSON.parse(row.context),
452
+ supersedes: JSON.parse(row.supersedes),
453
+ related: JSON.parse(row.related),
454
+ created: row.created,
455
+ confirmed: row.confirmed === 1
456
+ };
457
+ if (row.evidence !== null) lesson.evidence = row.evidence;
458
+ if (row.severity !== null) lesson.severity = row.severity;
459
+ if (row.deleted === 1) lesson.deleted = true;
460
+ if (row.retrieval_count > 0) lesson.retrievalCount = row.retrieval_count;
461
+ if (row.invalidated_at !== null) lesson.invalidatedAt = row.invalidated_at;
462
+ if (row.invalidation_reason !== null) lesson.invalidationReason = row.invalidation_reason;
463
+ if (row.citation_file !== null) {
464
+ lesson.citation = {
465
+ file: row.citation_file,
466
+ ...row.citation_line !== null && { line: row.citation_line },
467
+ ...row.citation_commit !== null && { commit: row.citation_commit }
468
+ };
469
+ }
470
+ if (row.compaction_level !== null && row.compaction_level !== 0) {
471
+ lesson.compactionLevel = row.compaction_level;
472
+ }
473
+ if (row.compacted_at !== null) lesson.compactedAt = row.compacted_at;
474
+ if (row.last_retrieved !== null) lesson.lastRetrieved = row.last_retrieved;
475
+ return lesson;
476
+ }
477
+ function incrementRetrievalCount(repoRoot, lessonIds) {
478
+ if (lessonIds.length === 0) return;
479
+ const database = openDb(repoRoot);
480
+ if (!database) {
481
+ logDegradationWarning();
482
+ return;
483
+ }
484
+ const now = (/* @__PURE__ */ new Date()).toISOString();
485
+ const update = database.prepare(`
486
+ UPDATE lessons
487
+ SET retrieval_count = retrieval_count + 1,
488
+ last_retrieved = ?
489
+ WHERE id = ?
490
+ `);
491
+ const updateMany = database.transaction((ids) => {
492
+ for (const id of ids) {
493
+ update.run(now, id);
494
+ }
495
+ });
496
+ updateMany(lessonIds);
497
+ }
428
498
  async function searchKeyword(repoRoot, query, limit) {
429
499
  const database = openDb(repoRoot);
500
+ if (!database) {
501
+ throw new Error(
502
+ "Keyword search requires SQLite (FTS5 required). Install native build tools or use vector search instead."
503
+ );
504
+ }
430
505
  const countResult = database.prepare("SELECT COUNT(*) as cnt FROM lessons").get();
431
506
  if (countResult.cnt === 0) return [];
432
507
  const rows = database.prepare(
@@ -444,22 +519,13 @@ async function searchKeyword(repoRoot, query, limit) {
444
519
  }
445
520
  return rows.map(rowToLesson);
446
521
  }
447
- function incrementRetrievalCount(repoRoot, lessonIds) {
448
- if (lessonIds.length === 0) return;
449
- const database = openDb(repoRoot);
450
- const now = (/* @__PURE__ */ new Date()).toISOString();
451
- const update = database.prepare(`
452
- UPDATE lessons
453
- SET retrieval_count = retrieval_count + 1,
454
- last_retrieved = ?
455
- WHERE id = ?
456
- `);
457
- const updateMany = database.transaction((ids) => {
458
- for (const id of ids) {
459
- update.run(now, id);
460
- }
461
- });
462
- updateMany(lessonIds);
522
+
523
+ // src/utils.ts
524
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
525
+ function getLessonAgeDays(lesson) {
526
+ const created = new Date(lesson.created).getTime();
527
+ const now = Date.now();
528
+ return Math.floor((now - created) / MS_PER_DAY);
463
529
  }
464
530
  var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
465
531
  var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
@@ -544,14 +610,6 @@ async function searchVector(repoRoot, query, options) {
544
610
  return scored.slice(0, limit);
545
611
  }
546
612
 
547
- // src/utils.ts
548
- var MS_PER_DAY = 24 * 60 * 60 * 1e3;
549
- function getLessonAgeDays(lesson) {
550
- const created = new Date(lesson.created).getTime();
551
- const now = Date.now();
552
- return Math.floor((now - created) / MS_PER_DAY);
553
- }
554
-
555
613
  // src/search/ranking.ts
556
614
  var RECENCY_THRESHOLD_DAYS = 30;
557
615
  var HIGH_SEVERITY_BOOST = 1.5;
@@ -783,7 +841,7 @@ async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
783
841
  return { lessons: topLessons, message };
784
842
  }
785
843
  function formatLessonsCheck(lessons) {
786
- const header = "\u{1F4DA} Lessons Check\n" + "\u2500".repeat(40);
844
+ const header = "Lessons Check\n" + "\u2500".repeat(40);
787
845
  if (lessons.length === 0) {
788
846
  return `${header}
789
847
  No relevant lessons found for this plan.`;
@@ -798,7 +856,7 @@ ${lessonLines.join("\n")}`;
798
856
  }
799
857
 
800
858
  // src/index.ts
801
- var VERSION = "0.1.0";
859
+ var VERSION = "0.2.3";
802
860
 
803
861
  export { DB_PATH, LESSONS_PATH, LessonSchema, LessonTypeSchema, MODEL_FILENAME, MODEL_URI, TombstoneSchema, VERSION, appendLesson, calculateScore, closeDb, confirmationBoost, cosineSimilarity, detectSelfCorrection, detectTestFailure, detectUserCorrection, embedText, embedTexts, formatLessonsCheck, generateId, getEmbedding, isActionable, isModelAvailable, isNovel, isSpecific, loadSessionLessons, rankLessons, readLessons, rebuildIndex, recencyBoost, resolveModel, retrieveForPlan, searchKeyword, searchVector, severityBoost, shouldPropose, unloadEmbedding };
804
862
  //# sourceMappingURL=index.js.map