opencodekit 0.17.13 → 0.18.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 (36) hide show
  1. package/dist/index.js +4 -6
  2. package/dist/template/.opencode/dcp.jsonc +81 -81
  3. package/dist/template/.opencode/memory/memory.db +0 -0
  4. package/dist/template/.opencode/memory.db +0 -0
  5. package/dist/template/.opencode/memory.db-shm +0 -0
  6. package/dist/template/.opencode/memory.db-wal +0 -0
  7. package/dist/template/.opencode/opencode.json +199 -23
  8. package/dist/template/.opencode/opencode.json.tui-migration.bak +1380 -0
  9. package/dist/template/.opencode/package.json +1 -1
  10. package/dist/template/.opencode/plugin/lib/capture.ts +177 -0
  11. package/dist/template/.opencode/plugin/lib/context.ts +194 -0
  12. package/dist/template/.opencode/plugin/lib/curator.ts +234 -0
  13. package/dist/template/.opencode/plugin/lib/db/maintenance.ts +312 -0
  14. package/dist/template/.opencode/plugin/lib/db/observations.ts +299 -0
  15. package/dist/template/.opencode/plugin/lib/db/pipeline.ts +520 -0
  16. package/dist/template/.opencode/plugin/lib/db/schema.ts +356 -0
  17. package/dist/template/.opencode/plugin/lib/db/types.ts +211 -0
  18. package/dist/template/.opencode/plugin/lib/distill.ts +376 -0
  19. package/dist/template/.opencode/plugin/lib/inject.ts +126 -0
  20. package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +188 -0
  21. package/dist/template/.opencode/plugin/lib/memory-db.ts +54 -936
  22. package/dist/template/.opencode/plugin/lib/memory-helpers.ts +202 -0
  23. package/dist/template/.opencode/plugin/lib/memory-hooks.ts +240 -0
  24. package/dist/template/.opencode/plugin/lib/memory-tools.ts +341 -0
  25. package/dist/template/.opencode/plugin/memory.ts +56 -60
  26. package/dist/template/.opencode/plugin/sessions.ts +372 -93
  27. package/dist/template/.opencode/tui.json +15 -0
  28. package/package.json +1 -1
  29. package/dist/template/.opencode/tool/action-queue.ts +0 -313
  30. package/dist/template/.opencode/tool/memory-admin.ts +0 -445
  31. package/dist/template/.opencode/tool/memory-get.ts +0 -143
  32. package/dist/template/.opencode/tool/memory-read.ts +0 -45
  33. package/dist/template/.opencode/tool/memory-search.ts +0 -264
  34. package/dist/template/.opencode/tool/memory-timeline.ts +0 -105
  35. package/dist/template/.opencode/tool/memory-update.ts +0 -63
  36. package/dist/template/.opencode/tool/observation.ts +0 -357
@@ -1,940 +1,58 @@
1
1
  /**
2
- * Memory Database Module
2
+ * Memory Database Module v2 — Barrel Export
3
3
  *
4
- * SQLite + FTS5 backend for OpenCodeKit memory system.
5
- * Provides fast full-text search and structured storage for observations.
4
+ * Re-exports all functions and types from sub-modules in ./db/.
5
+ * This preserves backward compatibility for existing imports from "./lib/memory-db.js".
6
6
  *
7
- * Features:
8
- * - WAL mode for better concurrency
9
- * - FTS5 for full-text search with BM25 ranking
10
- * - JSON1 extension for concept/file array queries
11
- * - Automatic schema migrations
12
- */
13
-
14
- import { Database } from "bun:sqlite";
15
- import path from "node:path";
16
-
17
- // ============================================================================
18
- // Types
19
- // ============================================================================
20
-
21
- export type ObservationType =
22
- | "decision"
23
- | "bugfix"
24
- | "feature"
25
- | "pattern"
26
- | "discovery"
27
- | "learning"
28
- | "warning";
29
-
30
- export type ConfidenceLevel = "high" | "medium" | "low";
31
-
32
- export interface ObservationRow {
33
- id: number;
34
- type: ObservationType;
35
- title: string;
36
- subtitle: string | null;
37
- facts: string | null; // JSON array
38
- narrative: string | null;
39
- concepts: string | null; // JSON array
40
- files_read: string | null; // JSON array
41
- files_modified: string | null; // JSON array
42
- confidence: ConfidenceLevel;
43
- bead_id: string | null;
44
- supersedes: number | null;
45
- superseded_by: number | null;
46
- valid_until: string | null;
47
- markdown_file: string | null;
48
- created_at: string;
49
- created_at_epoch: number;
50
- updated_at: string | null;
51
- }
52
-
53
- export interface ObservationInput {
54
- type: ObservationType;
55
- title: string;
56
- subtitle?: string;
57
- facts?: string[];
58
- narrative?: string;
59
- concepts?: string[];
60
- files_read?: string[];
61
- files_modified?: string[];
62
- confidence?: ConfidenceLevel;
63
- bead_id?: string;
64
- supersedes?: number;
65
- markdown_file?: string;
66
- }
67
-
68
- export interface SearchIndexResult {
69
- id: number;
70
- type: ObservationType;
71
- title: string;
72
- snippet: string;
73
- created_at: string;
74
- relevance_score: number;
75
- }
76
-
77
- export interface MemoryFileRow {
78
- id: number;
79
- file_path: string;
80
- content: string;
81
- mode: "replace" | "append";
82
- created_at: string;
83
- created_at_epoch: number;
84
- updated_at: string | null;
85
- updated_at_epoch: number | null;
86
- }
87
-
88
- export type ActionQueueSource = "approval" | "bead" | "worker";
89
- export type ActionQueueStatus = "pending" | "ready" | "idle";
90
-
91
- export interface ActionQueueItemRow {
92
- id: string;
93
- source: ActionQueueSource;
94
- status: ActionQueueStatus;
95
- title: string;
96
- owner: string | null;
97
- payload: string | null;
98
- created_at: string;
99
- created_at_epoch: number;
100
- updated_at: string | null;
101
- updated_at_epoch: number | null;
102
- }
103
-
104
- export interface ActionQueueItemInput {
105
- id: string;
106
- source: ActionQueueSource;
107
- status: ActionQueueStatus;
108
- title: string;
109
- owner?: string;
110
- payload?: Record<string, unknown>;
111
- }
112
-
113
- // ============================================================================
114
- // Schema
115
- // ============================================================================
116
-
117
- const SCHEMA_VERSION = 1;
118
-
119
- const SCHEMA_SQL = `
120
- -- Schema versioning for migrations
121
- CREATE TABLE IF NOT EXISTS schema_versions (
122
- id INTEGER PRIMARY KEY,
123
- version INTEGER UNIQUE NOT NULL,
124
- applied_at TEXT NOT NULL
125
- );
126
-
127
- -- Observations table (enhanced schema)
128
- CREATE TABLE IF NOT EXISTS observations (
129
- id INTEGER PRIMARY KEY AUTOINCREMENT,
130
- type TEXT NOT NULL CHECK(type IN ('decision','bugfix','feature','pattern','discovery','learning','warning')),
131
- title TEXT NOT NULL,
132
- subtitle TEXT,
133
- facts TEXT,
134
- narrative TEXT,
135
- concepts TEXT,
136
- files_read TEXT,
137
- files_modified TEXT,
138
- confidence TEXT CHECK(confidence IN ('high','medium','low')) DEFAULT 'high',
139
- bead_id TEXT,
140
- supersedes INTEGER,
141
- superseded_by INTEGER,
142
- valid_until TEXT,
143
- markdown_file TEXT,
144
- created_at TEXT NOT NULL,
145
- created_at_epoch INTEGER NOT NULL,
146
- updated_at TEXT,
147
- FOREIGN KEY(supersedes) REFERENCES observations(id) ON DELETE SET NULL,
148
- FOREIGN KEY(superseded_by) REFERENCES observations(id) ON DELETE SET NULL
149
- );
150
-
151
- -- FTS5 virtual table for full-text search
152
- CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
153
- title,
154
- subtitle,
155
- narrative,
156
- facts,
157
- concepts,
158
- content='observations',
159
- content_rowid='id'
160
- );
161
-
162
- -- Indexes for common queries
163
- CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
164
- CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
165
- CREATE INDEX IF NOT EXISTS idx_observations_bead_id ON observations(bead_id);
166
- CREATE INDEX IF NOT EXISTS idx_observations_superseded ON observations(superseded_by) WHERE superseded_by IS NOT NULL;
167
-
168
- -- Memory files table (for non-observation memory files)
169
- CREATE TABLE IF NOT EXISTS memory_files (
170
- id INTEGER PRIMARY KEY AUTOINCREMENT,
171
- file_path TEXT UNIQUE NOT NULL,
172
- content TEXT NOT NULL,
173
- mode TEXT CHECK(mode IN ('replace', 'append')) DEFAULT 'replace',
174
- created_at TEXT NOT NULL,
175
- created_at_epoch INTEGER NOT NULL,
176
- updated_at TEXT,
177
- updated_at_epoch INTEGER
178
- );
179
-
180
- CREATE INDEX IF NOT EXISTS idx_memory_files_path ON memory_files(file_path);
181
-
182
- -- Action queue table for orchestration status snapshots
183
- CREATE TABLE IF NOT EXISTS action_queue_items (
184
- id TEXT PRIMARY KEY,
185
- source TEXT NOT NULL CHECK(source IN ('approval','bead','worker')),
186
- status TEXT NOT NULL CHECK(status IN ('pending','ready','idle')),
187
- title TEXT NOT NULL,
188
- owner TEXT,
189
- payload TEXT,
190
- created_at TEXT NOT NULL,
191
- created_at_epoch INTEGER NOT NULL,
192
- updated_at TEXT,
193
- updated_at_epoch INTEGER
194
- );
195
-
196
- CREATE INDEX IF NOT EXISTS idx_action_queue_status ON action_queue_items(status);
197
- CREATE INDEX IF NOT EXISTS idx_action_queue_source ON action_queue_items(source);
198
- `;
199
-
200
- // FTS5 sync triggers (separate because they can't use IF NOT EXISTS)
201
- const FTS_TRIGGERS_SQL = `
202
- -- Sync trigger for INSERT
203
- CREATE TRIGGER IF NOT EXISTS observations_fts_ai AFTER INSERT ON observations BEGIN
204
- INSERT INTO observations_fts(rowid, title, subtitle, narrative, facts, concepts)
205
- VALUES (new.id, new.title, new.subtitle, new.narrative, new.facts, new.concepts);
206
- END;
207
-
208
- -- Sync trigger for DELETE
209
- CREATE TRIGGER IF NOT EXISTS observations_fts_ad AFTER DELETE ON observations BEGIN
210
- INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, facts, concepts)
211
- VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.facts, old.concepts);
212
- END;
213
-
214
- -- Sync trigger for UPDATE
215
- CREATE TRIGGER IF NOT EXISTS observations_fts_au AFTER UPDATE ON observations BEGIN
216
- INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, facts, concepts)
217
- VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.facts, old.concepts);
218
- INSERT INTO observations_fts(rowid, title, subtitle, narrative, facts, concepts)
219
- VALUES (new.id, new.title, new.subtitle, new.narrative, new.facts, new.concepts);
220
- END;
221
- `;
222
-
223
- // ============================================================================
224
- // Database Manager
225
- // ============================================================================
226
-
227
- let dbInstance: Database | null = null;
228
-
229
- /**
230
- * Get or create the memory database instance.
231
- * Uses singleton pattern to reuse connection.
232
- */
233
- export function getMemoryDB(): Database {
234
- if (dbInstance) return dbInstance;
235
-
236
- const dbPath = path.join(process.cwd(), ".opencode/memory.db");
237
- dbInstance = new Database(dbPath, { create: true });
238
-
239
- // Enable WAL mode for better concurrency
240
- dbInstance.run("PRAGMA journal_mode = WAL");
241
- dbInstance.run("PRAGMA foreign_keys = ON");
242
-
243
- // Initialize schema
244
- initializeSchema(dbInstance);
245
-
246
- return dbInstance;
247
- }
248
-
249
- /**
250
- * Close the database connection (for cleanup).
251
- */
252
- export function closeMemoryDB(): void {
253
- if (dbInstance) {
254
- dbInstance.close();
255
- dbInstance = null;
256
- }
257
- }
258
-
259
- /**
260
- * Initialize database schema if not exists.
261
- */
262
- function initializeSchema(db: Database): void {
263
- // Check current schema version
264
- try {
265
- const versionRow = db
266
- .query("SELECT MAX(version) as version FROM schema_versions")
267
- .get() as {
268
- version: number | null;
269
- } | null;
270
- const currentVersion = versionRow?.version ?? 0;
271
-
272
- if (currentVersion >= SCHEMA_VERSION) {
273
- return; // Schema is up to date
274
- }
275
- } catch {
276
- // schema_versions table doesn't exist, need full init
277
- }
278
-
279
- // Run schema creation
280
- db.exec(SCHEMA_SQL);
281
-
282
- // Run FTS triggers (handle if already exists)
283
- try {
284
- db.exec(FTS_TRIGGERS_SQL);
285
- } catch {
286
- // Triggers may already exist, ignore
287
- }
288
-
289
- // Record schema version
290
- db.run(
291
- "INSERT OR REPLACE INTO schema_versions (id, version, applied_at) VALUES (1, ?, ?)",
292
- [SCHEMA_VERSION, new Date().toISOString()],
293
- );
294
- }
295
-
296
- // ============================================================================
7
+ * Sub-module structure:
8
+ * db/types.ts — Configuration, types, interfaces
9
+ * db/schema.ts — SQL schema, migrations, DB manager
10
+ * db/observations.ts Observation CRUD, search, timeline, stats
11
+ * db/pipeline.ts — Temporal messages, distillations, relevance scoring
12
+ * db/maintenance.ts — Memory files, FTS5, DB maintenance
13
+ */
14
+
15
+ // Memory Files, FTS5, and Maintenance
16
+ export {
17
+ archiveOldObservations,
18
+ checkFTS5Available,
19
+ checkpointWAL,
20
+ getDatabaseSizes,
21
+ getMarkdownFilesInSqlite,
22
+ getMemoryFile,
23
+ optimizeFTS5,
24
+ rebuildFTS5,
25
+ runFullMaintenance,
26
+ upsertMemoryFile,
27
+ vacuumDatabase,
28
+ } from "./db/maintenance.js";
297
29
  // Observation Operations
298
- // ============================================================================
299
-
300
- /**
301
- * Store a new observation in the database.
302
- */
303
- export function storeObservation(input: ObservationInput): number {
304
- const db = getMemoryDB();
305
- const now = new Date();
306
-
307
- const result = db
308
- .query(
309
- `
310
- INSERT INTO observations (
311
- type, title, subtitle, facts, narrative, concepts,
312
- files_read, files_modified, confidence, bead_id,
313
- supersedes, markdown_file, created_at, created_at_epoch
314
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
315
- `,
316
- )
317
- .run(
318
- input.type,
319
- input.title,
320
- input.subtitle ?? null,
321
- input.facts ? JSON.stringify(input.facts) : null,
322
- input.narrative ?? null,
323
- input.concepts ? JSON.stringify(input.concepts) : null,
324
- input.files_read ? JSON.stringify(input.files_read) : null,
325
- input.files_modified ? JSON.stringify(input.files_modified) : null,
326
- input.confidence ?? "high",
327
- input.bead_id ?? null,
328
- input.supersedes ?? null,
329
- input.markdown_file ?? null,
330
- now.toISOString(),
331
- now.getTime(),
332
- );
333
-
334
- const insertedId = Number(result.lastInsertRowid);
335
-
336
- // Update supersedes relationship
337
- if (input.supersedes) {
338
- db.run("UPDATE observations SET superseded_by = ? WHERE id = ?", [
339
- insertedId,
340
- input.supersedes,
341
- ]);
342
- }
343
-
344
- return insertedId;
345
- }
346
-
347
- /**
348
- * Get observation by ID.
349
- */
350
- export function getObservationById(id: number): ObservationRow | null {
351
- const db = getMemoryDB();
352
- return db
353
- .query("SELECT * FROM observations WHERE id = ?")
354
- .get(id) as ObservationRow | null;
355
- }
356
-
357
- /**
358
- * Get multiple observations by IDs.
359
- */
360
- export function getObservationsByIds(ids: number[]): ObservationRow[] {
361
- if (ids.length === 0) return [];
362
-
363
- const db = getMemoryDB();
364
- const placeholders = ids.map(() => "?").join(",");
365
- return db
366
- .query(`SELECT * FROM observations WHERE id IN (${placeholders})`)
367
- .all(...ids) as ObservationRow[];
368
- }
369
-
370
- /**
371
- * Search observations using FTS5.
372
- * Returns compact index results for progressive disclosure.
373
- */
374
- export function searchObservationsFTS(
375
- query: string,
376
- options: {
377
- type?: ObservationType;
378
- concepts?: string[];
379
- limit?: number;
380
- } = {},
381
- ): SearchIndexResult[] {
382
- const db = getMemoryDB();
383
- const limit = options.limit ?? 10;
384
-
385
- // Build FTS5 query - escape special characters
386
- const ftsQuery = query
387
- .replace(/['"]/g, '""')
388
- .split(/\s+/)
389
- .filter((term) => term.length > 0)
390
- .map((term) => `"${term}"*`)
391
- .join(" OR ");
392
-
393
- if (!ftsQuery) {
394
- // Empty query - return recent observations
395
- return db
396
- .query(
397
- `
398
- SELECT id, type, title,
399
- substr(COALESCE(narrative, ''), 1, 100) as snippet,
400
- created_at,
401
- 0 as relevance_score
402
- FROM observations
403
- WHERE superseded_by IS NULL
404
- ${options.type ? "AND type = ?" : ""}
405
- ORDER BY created_at_epoch DESC
406
- LIMIT ?
407
- `,
408
- )
409
- .all(
410
- ...(options.type ? [options.type, limit] : [limit]),
411
- ) as SearchIndexResult[];
412
- }
413
-
414
- try {
415
- // Use FTS5 with BM25 ranking
416
- let sql = `
417
- SELECT o.id, o.type, o.title,
418
- substr(COALESCE(o.narrative, ''), 1, 100) as snippet,
419
- o.created_at,
420
- bm25(observations_fts) as relevance_score
421
- FROM observations o
422
- JOIN observations_fts fts ON fts.rowid = o.id
423
- WHERE observations_fts MATCH ?
424
- AND o.superseded_by IS NULL
425
- `;
426
-
427
- const params: (string | number)[] = [ftsQuery];
428
-
429
- if (options.type) {
430
- sql += " AND o.type = ?";
431
- params.push(options.type);
432
- }
433
-
434
- sql += " ORDER BY relevance_score LIMIT ?";
435
- params.push(limit);
436
-
437
- return db.query(sql).all(...params) as SearchIndexResult[];
438
- } catch {
439
- // FTS5 query failed, fallback to LIKE search
440
- return fallbackLikeSearch(db, query, options.type, limit);
441
- }
442
- }
443
-
444
- /**
445
- * Fallback search using LIKE (for when FTS5 fails).
446
- */
447
- function fallbackLikeSearch(
448
- db: Database,
449
- query: string,
450
- type: ObservationType | undefined,
451
- limit: number,
452
- ): SearchIndexResult[] {
453
- const likePattern = `%${query}%`;
454
-
455
- let sql = `
456
- SELECT id, type, title,
457
- substr(COALESCE(narrative, ''), 1, 100) as snippet,
458
- created_at,
459
- 0 as relevance_score
460
- FROM observations
461
- WHERE superseded_by IS NULL
462
- AND (title LIKE ? OR narrative LIKE ? OR concepts LIKE ?)
463
- `;
464
-
465
- const params: (string | number)[] = [likePattern, likePattern, likePattern];
466
-
467
- if (type) {
468
- sql += " AND type = ?";
469
- params.push(type);
470
- }
471
-
472
- sql += " ORDER BY created_at_epoch DESC LIMIT ?";
473
- params.push(limit);
474
-
475
- return db.query(sql).all(...params) as SearchIndexResult[];
476
- }
477
-
478
- /**
479
- * Get timeline around an anchor observation.
480
- */
481
- export function getTimelineAroundObservation(
482
- anchorId: number,
483
- depthBefore = 5,
484
- depthAfter = 5,
485
- ): {
486
- anchor: ObservationRow | null;
487
- before: SearchIndexResult[];
488
- after: SearchIndexResult[];
489
- } {
490
- const db = getMemoryDB();
491
-
492
- const anchor = getObservationById(anchorId);
493
- if (!anchor) {
494
- return { anchor: null, before: [], after: [] };
495
- }
496
-
497
- const before = db
498
- .query(
499
- `
500
- SELECT id, type, title,
501
- substr(COALESCE(narrative, ''), 1, 100) as snippet,
502
- created_at,
503
- 0 as relevance_score
504
- FROM observations
505
- WHERE created_at_epoch < ?
506
- AND superseded_by IS NULL
507
- ORDER BY created_at_epoch DESC
508
- LIMIT ?
509
- `,
510
- )
511
- .all(anchor.created_at_epoch, depthBefore) as SearchIndexResult[];
512
-
513
- const after = db
514
- .query(
515
- `
516
- SELECT id, type, title,
517
- substr(COALESCE(narrative, ''), 1, 100) as snippet,
518
- created_at,
519
- 0 as relevance_score
520
- FROM observations
521
- WHERE created_at_epoch > ?
522
- AND superseded_by IS NULL
523
- ORDER BY created_at_epoch ASC
524
- LIMIT ?
525
- `,
526
- )
527
- .all(anchor.created_at_epoch, depthAfter) as SearchIndexResult[];
528
-
529
- return {
530
- anchor,
531
- before: before.reverse(),
532
- after,
533
- };
534
- }
535
-
536
- /**
537
- * Get most recent observation.
538
- */
539
- export function getMostRecentObservation(): ObservationRow | null {
540
- const db = getMemoryDB();
541
- return db
542
- .query(
543
- "SELECT * FROM observations WHERE superseded_by IS NULL ORDER BY created_at_epoch DESC LIMIT 1",
544
- )
545
- .get() as ObservationRow | null;
546
- }
547
-
548
- /**
549
- * Get observation count by type.
550
- */
551
- export function getObservationStats(): Record<string, number> {
552
- const db = getMemoryDB();
553
- const rows = db
554
- .query(
555
- `
556
- SELECT type, COUNT(*) as count
557
- FROM observations
558
- WHERE superseded_by IS NULL
559
- GROUP BY type
560
- `,
561
- )
562
- .all() as { type: string; count: number }[];
563
-
564
- const stats: Record<string, number> = { total: 0 };
565
- for (const row of rows) {
566
- stats[row.type] = row.count;
567
- stats.total += row.count;
568
- }
569
- return stats;
570
- }
571
-
572
- // ============================================================================
573
- // Memory File Operations
574
- // ============================================================================
575
-
576
- /**
577
- * Store or update a memory file.
578
- */
579
- export function upsertMemoryFile(
580
- filePath: string,
581
- content: string,
582
- mode: "replace" | "append" = "replace",
583
- ): void {
584
- const db = getMemoryDB();
585
- const now = new Date();
586
-
587
- db.run(
588
- `
589
- INSERT INTO memory_files (file_path, content, mode, created_at, created_at_epoch)
590
- VALUES (?, ?, ?, ?, ?)
591
- ON CONFLICT(file_path) DO UPDATE SET
592
- content = CASE WHEN excluded.mode = 'append' THEN memory_files.content || '\n\n' || excluded.content ELSE excluded.content END,
593
- mode = excluded.mode,
594
- updated_at = ?,
595
- updated_at_epoch = ?
596
- `,
597
- [
598
- filePath,
599
- content,
600
- mode,
601
- now.toISOString(),
602
- now.getTime(),
603
- now.toISOString(),
604
- now.getTime(),
605
- ],
606
- );
607
- }
608
-
609
- /**
610
- * Get a memory file by path.
611
- */
612
- export function getMemoryFile(filePath: string): MemoryFileRow | null {
613
- const db = getMemoryDB();
614
- return db
615
- .query("SELECT * FROM memory_files WHERE file_path = ?")
616
- .get(filePath) as MemoryFileRow | null;
617
- }
618
-
619
- /**
620
- * Replace action queue snapshot with a new set of items.
621
- */
622
- export function replaceActionQueueItems(items: ActionQueueItemInput[]): void {
623
- const db = getMemoryDB();
624
- const now = new Date();
625
-
626
- const insertStmt = db.query(
627
- `
628
- INSERT INTO action_queue_items
629
- (id, source, status, title, owner, payload, created_at, created_at_epoch)
630
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
631
- ON CONFLICT(id) DO UPDATE SET
632
- source = excluded.source,
633
- status = excluded.status,
634
- title = excluded.title,
635
- owner = excluded.owner,
636
- payload = excluded.payload,
637
- updated_at = excluded.created_at,
638
- updated_at_epoch = excluded.created_at_epoch
639
- `,
640
- );
641
-
642
- db.transaction(() => {
643
- db.run("DELETE FROM action_queue_items");
644
- for (const item of items) {
645
- insertStmt.run(
646
- item.id,
647
- item.source,
648
- item.status,
649
- item.title,
650
- item.owner ?? null,
651
- item.payload ? JSON.stringify(item.payload) : null,
652
- now.toISOString(),
653
- now.getTime(),
654
- );
655
- }
656
- })();
657
- }
658
-
659
- /**
660
- * Return action queue items, optionally filtered by status.
661
- */
662
- export function listActionQueueItems(
663
- status?: ActionQueueStatus,
664
- ): ActionQueueItemRow[] {
665
- const db = getMemoryDB();
666
- if (!status) {
667
- return db
668
- .query(
669
- "SELECT * FROM action_queue_items ORDER BY created_at_epoch DESC, id ASC",
670
- )
671
- .all() as ActionQueueItemRow[];
672
- }
673
-
674
- return db
675
- .query(
676
- "SELECT * FROM action_queue_items WHERE status = ? ORDER BY created_at_epoch DESC, id ASC",
677
- )
678
- .all(status) as ActionQueueItemRow[];
679
- }
680
-
681
- /**
682
- * Clear all action queue items.
683
- */
684
- export function clearActionQueueItems(): void {
685
- const db = getMemoryDB();
686
- db.run("DELETE FROM action_queue_items");
687
- }
688
-
689
- // ============================================================================
690
- // FTS5 Maintenance
691
- // ============================================================================
692
-
693
- /**
694
- * Optimize FTS5 index (run periodically).
695
- */
696
- export function optimizeFTS5(): void {
697
- const db = getMemoryDB();
698
- db.run("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
699
- }
700
-
701
- /**
702
- * Rebuild FTS5 index from scratch.
703
- */
704
- export function rebuildFTS5(): void {
705
- const db = getMemoryDB();
706
- db.run("INSERT INTO observations_fts(observations_fts) VALUES('rebuild')");
707
- }
708
-
709
- /**
710
- * Check if FTS5 is available and working.
711
- */
712
- export function checkFTS5Available(): boolean {
713
- try {
714
- const db = getMemoryDB();
715
- db.query("SELECT * FROM observations_fts LIMIT 1").get();
716
- return true;
717
- } catch {
718
- return false;
719
- }
720
- }
721
-
722
- // ============================================================================
723
- // Database Maintenance
724
- // ============================================================================
725
-
726
- export interface MaintenanceStats {
727
- archived: number;
728
- vacuumed: boolean;
729
- checkpointed: boolean;
730
- prunedMarkdown: number;
731
- freedBytes: number;
732
- dbSizeBefore: number;
733
- dbSizeAfter: number;
734
- }
735
-
736
- export interface ArchiveOptions {
737
- /** Archive observations older than this many days (default: 90) */
738
- olderThanDays?: number;
739
- /** Archive superseded observations regardless of age */
740
- includeSuperseded?: boolean;
741
- /** Dry run - don't actually archive, just count */
742
- dryRun?: boolean;
743
- }
744
-
745
- /**
746
- * Archive old observations to a separate table.
747
- * Archived observations are removed from main table and FTS index.
748
- */
749
- export function archiveOldObservations(options: ArchiveOptions = {}): number {
750
- const db = getMemoryDB();
751
- const olderThanDays = options.olderThanDays ?? 90;
752
- const includeSuperseded = options.includeSuperseded ?? true;
753
- const dryRun = options.dryRun ?? false;
754
-
755
- const cutoffEpoch = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
756
-
757
- // Create archive table if not exists
758
- db.run(`
759
- CREATE TABLE IF NOT EXISTS observations_archive (
760
- id INTEGER PRIMARY KEY,
761
- type TEXT NOT NULL,
762
- title TEXT NOT NULL,
763
- subtitle TEXT,
764
- facts TEXT,
765
- narrative TEXT,
766
- concepts TEXT,
767
- files_read TEXT,
768
- files_modified TEXT,
769
- confidence TEXT,
770
- bead_id TEXT,
771
- supersedes INTEGER,
772
- superseded_by INTEGER,
773
- valid_until TEXT,
774
- markdown_file TEXT,
775
- created_at TEXT NOT NULL,
776
- created_at_epoch INTEGER NOT NULL,
777
- updated_at TEXT,
778
- archived_at TEXT NOT NULL
779
- )
780
- `);
781
-
782
- // Build WHERE clause
783
- let whereClause = `created_at_epoch < ${cutoffEpoch}`;
784
- if (includeSuperseded) {
785
- whereClause = `(${whereClause} OR superseded_by IS NOT NULL)`;
786
- }
787
-
788
- // Count candidates
789
- const countResult = db
790
- .query(`SELECT COUNT(*) as count FROM observations WHERE ${whereClause}`)
791
- .get() as { count: number };
792
-
793
- if (dryRun || countResult.count === 0) {
794
- return countResult.count;
795
- }
796
-
797
- // Move to archive
798
- const now = new Date().toISOString();
799
- db.run(`
800
- INSERT INTO observations_archive
801
- SELECT *, '${now}' as archived_at FROM observations WHERE ${whereClause}
802
- `);
803
-
804
- // Delete from main table (triggers will remove from FTS)
805
- db.run(`DELETE FROM observations WHERE ${whereClause}`);
806
-
807
- return countResult.count;
808
- }
809
-
810
- /**
811
- * Checkpoint WAL file back to main database.
812
- * This reclaims space and improves read performance.
813
- */
814
- export function checkpointWAL(): { walSize: number; checkpointed: boolean } {
815
- const db = getMemoryDB();
816
-
817
- try {
818
- // TRUNCATE mode: checkpoint and truncate WAL to zero
819
- const result = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get() as {
820
- busy: number;
821
- log: number;
822
- checkpointed: number;
823
- };
824
-
825
- return {
826
- walSize: result.log,
827
- checkpointed: result.busy === 0,
828
- };
829
- } catch {
830
- return { walSize: 0, checkpointed: false };
831
- }
832
- }
833
-
834
- /**
835
- * Vacuum database to reclaim space and defragment.
836
- */
837
- export function vacuumDatabase(): boolean {
838
- const db = getMemoryDB();
839
- try {
840
- db.run("VACUUM");
841
- return true;
842
- } catch {
843
- return false;
844
- }
845
- }
846
-
847
- /**
848
- * Get database file sizes.
849
- */
850
- export function getDatabaseSizes(): {
851
- mainDb: number;
852
- wal: number;
853
- shm: number;
854
- total: number;
855
- } {
856
- const db = getMemoryDB();
857
-
858
- try {
859
- const pageCount = db.query("PRAGMA page_count").get() as {
860
- page_count: number;
861
- };
862
- const pageSize = db.query("PRAGMA page_size").get() as {
863
- page_size: number;
864
- };
865
- const mainDb = pageCount.page_count * pageSize.page_size;
866
-
867
- // WAL and SHM sizes from pragma
868
- const walResult = db.query("PRAGMA wal_checkpoint").get() as {
869
- busy: number;
870
- log: number;
871
- checkpointed: number;
872
- };
873
- const wal = walResult.log * pageSize.page_size;
874
-
875
- return {
876
- mainDb,
877
- wal,
878
- shm: 32768, // SHM is typically 32KB
879
- total: mainDb + wal + 32768,
880
- };
881
- } catch {
882
- return { mainDb: 0, wal: 0, shm: 0, total: 0 };
883
- }
884
- }
885
-
886
- /**
887
- * Get list of markdown files that exist in SQLite (for pruning).
888
- */
889
- export function getMarkdownFilesInSqlite(): string[] {
890
- const db = getMemoryDB();
891
- const rows = db
892
- .query(
893
- "SELECT markdown_file FROM observations WHERE markdown_file IS NOT NULL",
894
- )
895
- .all() as { markdown_file: string }[];
896
-
897
- return rows.map((r) => r.markdown_file);
898
- }
899
-
900
- /**
901
- * Run full maintenance cycle.
902
- */
903
- export function runFullMaintenance(
904
- options: ArchiveOptions = {},
905
- ): MaintenanceStats {
906
- const sizesBefore = getDatabaseSizes();
907
-
908
- // 1. Archive old observations
909
- const archived = archiveOldObservations(options);
910
-
911
- // 2. Optimize FTS5
912
- if (!options.dryRun) {
913
- optimizeFTS5();
914
- }
915
-
916
- // 3. Checkpoint WAL
917
- let checkpointed = false;
918
- if (!options.dryRun) {
919
- const walResult = checkpointWAL();
920
- checkpointed = walResult.checkpointed;
921
- }
922
-
923
- // 4. Vacuum
924
- let vacuumed = false;
925
- if (!options.dryRun) {
926
- vacuumed = vacuumDatabase();
927
- }
928
-
929
- const sizesAfter = getDatabaseSizes();
930
-
931
- return {
932
- archived,
933
- vacuumed,
934
- checkpointed,
935
- prunedMarkdown: 0, // Will be set by the tool after file operations
936
- freedBytes: sizesBefore.total - sizesAfter.total,
937
- dbSizeBefore: sizesBefore.total,
938
- dbSizeAfter: sizesAfter.total,
939
- };
940
- }
30
+ export {
31
+ getMostRecentObservation,
32
+ getObservationById,
33
+ getObservationStats,
34
+ getObservationsByIds,
35
+ getTimelineAroundObservation,
36
+ searchObservationsFTS,
37
+ storeObservation,
38
+ } from "./db/observations.js";
39
+ // Temporal Message & Distillation Operations
40
+ export {
41
+ estimateTokens,
42
+ getCaptureStats,
43
+ getDistillationById,
44
+ getDistillationStats,
45
+ getRecentDistillations,
46
+ getRelevantKnowledge,
47
+ getUndistilledMessageCount,
48
+ getUndistilledMessages,
49
+ markMessagesDistilled,
50
+ purgeOldTemporalMessages,
51
+ searchDistillationsFTS,
52
+ storeDistillation,
53
+ storeTemporalMessage,
54
+ } from "./db/pipeline.js";
55
+ // Database Manager
56
+ export { closeMemoryDB, getMemoryDB } from "./db/schema.js";
57
+ // Types & Configuration
58
+ export * from "./db/types.js";