opencodekit 0.16.0 → 0.16.1

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 (119) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/template/.opencode/AGENTS.md +64 -3
  3. package/dist/template/.opencode/command/create.md +34 -0
  4. package/dist/template/.opencode/command/design.md +35 -0
  5. package/dist/template/.opencode/command/handoff.md +15 -0
  6. package/dist/template/.opencode/command/init.md +40 -47
  7. package/dist/template/.opencode/command/plan.md +1 -0
  8. package/dist/template/.opencode/command/pr.md +15 -0
  9. package/dist/template/.opencode/command/research.md +3 -0
  10. package/dist/template/.opencode/command/resume.md +1 -0
  11. package/dist/template/.opencode/command/review-codebase.md +30 -0
  12. package/dist/template/.opencode/command/ship.md +43 -0
  13. package/dist/template/.opencode/command/start.md +1 -0
  14. package/dist/template/.opencode/command/status.md +24 -1
  15. package/dist/template/.opencode/command/ui-review.md +31 -0
  16. package/dist/template/.opencode/command/verify.md +35 -7
  17. package/dist/template/.opencode/memory/project/tech-stack.md +25 -22
  18. package/dist/template/.opencode/memory.db +0 -0
  19. package/dist/template/.opencode/memory.db-shm +0 -0
  20. package/dist/template/.opencode/memory.db-wal +0 -0
  21. package/dist/template/.opencode/opencode.json +817 -916
  22. package/dist/template/.opencode/package.json +1 -0
  23. package/dist/template/.opencode/plans/1770006237537-mighty-otter.md +418 -0
  24. package/dist/template/.opencode/plans/1770006913647-glowing-forest.md +170 -0
  25. package/dist/template/.opencode/plans/1770013678126-witty-planet.md +278 -0
  26. package/dist/template/.opencode/plugin/lib/memory-db.ts +828 -0
  27. package/dist/template/.opencode/plugin/memory.ts +38 -1
  28. package/dist/template/.opencode/skill/index-knowledge/SKILL.md +76 -31
  29. package/dist/template/.opencode/skill/memory-system/SKILL.md +110 -55
  30. package/dist/template/.opencode/tool/memory-get.ts +143 -0
  31. package/dist/template/.opencode/tool/memory-maintain.ts +167 -0
  32. package/dist/template/.opencode/tool/memory-migrate.ts +319 -0
  33. package/dist/template/.opencode/tool/memory-read.ts +17 -46
  34. package/dist/template/.opencode/tool/memory-search.ts +131 -28
  35. package/dist/template/.opencode/tool/memory-timeline.ts +105 -0
  36. package/dist/template/.opencode/tool/memory-update.ts +21 -26
  37. package/dist/template/.opencode/tool/observation.ts +112 -100
  38. package/dist/template/.opencode/tsconfig.json +19 -19
  39. package/package.json +1 -1
  40. package/dist/template/.opencode/memory/_templates/README.md +0 -73
  41. package/dist/template/.opencode/memory/_templates/observation.md +0 -39
  42. package/dist/template/.opencode/memory/_templates/prompt-engineering.md +0 -333
  43. package/dist/template/.opencode/memory/observations/2026-01-22-decision-agents-md-prompt-engineering-improvement.md +0 -29
  44. package/dist/template/.opencode/memory/observations/2026-01-25-decision-agent-roles-build-orchestrates-general-e.md +0 -14
  45. package/dist/template/.opencode/memory/observations/2026-01-25-decision-simplified-swarm-helper-tool-to-fix-type.md +0 -20
  46. package/dist/template/.opencode/memory/observations/2026-01-25-decision-use-beads-as-swarm-board-source-of-truth.md +0 -14
  47. package/dist/template/.opencode/memory/observations/2026-01-25-learning-user-wants-real-swarm-coordination-guida.md +0 -15
  48. package/dist/template/.opencode/memory/observations/2026-01-28-decision-created-deep-research-skill-for-thorough.md +0 -29
  49. package/dist/template/.opencode/memory/observations/2026-01-28-decision-gh-grep-mcp-wrapper-vs-native-grep-searc.md +0 -21
  50. package/dist/template/.opencode/memory/observations/2026-01-28-decision-oracle-tool-optimal-usage-patterns.md +0 -32
  51. package/dist/template/.opencode/memory/observations/2026-01-28-learning-ampcode-deep-mode-research-integration-w.md +0 -42
  52. package/dist/template/.opencode/memory/observations/2026-01-28-pattern-research-delegation-pattern-explore-for-.md +0 -32
  53. package/dist/template/.opencode/memory/observations/2026-01-29-decision-copilot-auth-plugin-rate-limit-handling.md +0 -27
  54. package/dist/template/.opencode/memory/observations/2026-01-29-decision-spec-driven-approach-for-opencodekit.md +0 -21
  55. package/dist/template/.opencode/memory/observations/2026-01-29-learning-karpathy-llm-coding-insights-dec-2025.md +0 -44
  56. package/dist/template/.opencode/memory/observations/2026-01-30-decision-github-copilot-claude-routing-keep-disab.md +0 -32
  57. package/dist/template/.opencode/memory/observations/2026-01-30-discovery-context-management-research-critical-gap.md +0 -14
  58. package/dist/template/.opencode/memory/observations/2026-01-30-discovery-kimi-k2-5-agent-swarm-architecture-patte.md +0 -45
  59. package/dist/template/.opencode/memory/observations/2026-01-30-pattern-swarm-tools-architecture.md +0 -28
  60. package/dist/template/.opencode/memory/observations/2026-01-31-decision-copilot-auth-plugin-updated-with-baseurl.md +0 -63
  61. package/dist/template/.opencode/memory/observations/2026-01-31-decision-created-dedicated-worker-agent-for-swarm.md +0 -20
  62. package/dist/template/.opencode/memory/observations/2026-01-31-decision-rollback-to-v1-1-47-for-copilot-claude-r.md +0 -21
  63. package/dist/template/.opencode/memory/observations/2026-01-31-decision-simplified-swarm-to-task-tool-pattern.md +0 -44
  64. package/dist/template/.opencode/memory/observations/2026-01-31-decision-swarm-architecture-task-tool-over-tmux.md +0 -33
  65. package/dist/template/.opencode/memory/observations/2026-01-31-decision-worker-skills-defined-for-swarm-delegati.md +0 -30
  66. package/dist/template/.opencode/memory/observations/2026-01-31-learning-gpt-reasoning-config-for-github-copilot.md +0 -51
  67. package/dist/template/.opencode/memory/observations/2026-01-31-learning-opencode-copilot-auth-comparison-finding.md +0 -61
  68. package/dist/template/.opencode/memory/observations/2026-01-31-learning-opencode-copilot-reasoning-architecture-.md +0 -66
  69. package/dist/template/.opencode/memory/observations/2026-01-31-learning-opencode-custom-tools-api.md +0 -48
  70. package/dist/template/.opencode/memory/observations/2026-01-31-learning-opencode-v1-1-48-skills-as-slash-command.md +0 -21
  71. package/dist/template/.opencode/memory/observations/2026-01-31-learning-swarm-system-simplified-removed-mailbox-.md +0 -30
  72. package/dist/template/.opencode/memory/observations/2026-01-31-learning-v1-1-48-native-copilot-reasoning-via-pr-.md +0 -45
  73. package/dist/template/.opencode/memory/observations/2026-01-31-warning-cannot-add-custom-config-to-opencode-jso.md +0 -18
  74. package/dist/template/.opencode/memory/observations/2026-01-31-warning-copilot-claude-v1-endpoint-returns-404-c.md +0 -48
  75. package/dist/template/.opencode/memory/observations/2026-01-31-warning-opencode-v1-1-48-claude-thinking-block-s.md +0 -51
  76. package/dist/template/.opencode/memory/observations/2026-02-01-decision-add-skills-vs-commands-to-global-agents-.md +0 -15
  77. package/dist/template/.opencode/memory/observations/2026-02-01-decision-build-agent-auto-loads-skills-contextual.md +0 -31
  78. package/dist/template/.opencode/memory/observations/2026-02-01-decision-fixed-agent-configuration-for-opencodeki.md +0 -25
  79. package/dist/template/.opencode/memory/observations/2026-02-01-decision-focused-agents-md-upgrade-for-opencode-k.md +0 -14
  80. package/dist/template/.opencode/memory/observations/2026-02-01-decision-implement-tier-1-permission-upgrades.md +0 -15
  81. package/dist/template/.opencode/memory/observations/2026-02-01-decision-instructions-config-explicit-paths-not-w.md +0 -40
  82. package/dist/template/.opencode/memory/observations/2026-02-01-decision-merged-context-into-memory-project-singl.md +0 -42
  83. package/dist/template/.opencode/memory/observations/2026-02-01-decision-oracle-tool-should-use-review-agent-not-.md +0 -14
  84. package/dist/template/.opencode/memory/observations/2026-02-01-decision-plan-agent-auto-loads-skills-contextuall.md +0 -31
  85. package/dist/template/.opencode/memory/observations/2026-02-01-decision-plan-phased-oracle-command-merge-into-ne.md +0 -14
  86. package/dist/template/.opencode/memory/observations/2026-02-01-decision-prd-workflow-uses-prd-and-prd-task-skill.md +0 -23
  87. package/dist/template/.opencode/memory/observations/2026-02-01-decision-prefer-review-agent-via-opencode-cli-ove.md +0 -14
  88. package/dist/template/.opencode/memory/observations/2026-02-01-decision-remove-oracle-tool-add-ship-command-with.md +0 -14
  89. package/dist/template/.opencode/memory/observations/2026-02-01-decision-remove-oracle-tool-and-add-ship-command-.md +0 -14
  90. package/dist/template/.opencode/memory/observations/2026-02-01-decision-remove-oracle-tool-and-add-ship-command.md +0 -14
  91. package/dist/template/.opencode/memory/observations/2026-02-01-decision-remove-skills-vs-commands-section-from-a.md +0 -14
  92. package/dist/template/.opencode/memory/observations/2026-02-01-decision-replace-oracle-tool-with-ship-command-fl.md +0 -14
  93. package/dist/template/.opencode/memory/observations/2026-02-01-decision-replace-oracle-with-ship-command-workflo.md +0 -14
  94. package/dist/template/.opencode/memory/observations/2026-02-01-decision-replace-proxypal-oracle-with-cli-review-.md +0 -14
  95. package/dist/template/.opencode/memory/observations/2026-02-01-decision-simplified-dist-template-only-tech-stack.md +0 -50
  96. package/dist/template/.opencode/memory/observations/2026-02-01-decision-simplified-templates-only-tech-stack-md.md +0 -26
  97. package/dist/template/.opencode/memory/observations/2026-02-01-decision-subagents-load-minimal-skills-stay-lean.md +0 -29
  98. package/dist/template/.opencode/memory/observations/2026-02-01-decision-user-approved-permission-upgrades-in-ope.md +0 -15
  99. package/dist/template/.opencode/memory/observations/2026-02-01-discovery-verify-command-already-implemented.md +0 -28
  100. package/dist/template/.opencode/memory/observations/2026-02-01-feature-openspec-phase-b-complete-template-upgra.md +0 -43
  101. package/dist/template/.opencode/memory/observations/2026-02-01-learning-build-agent-should-use-dynamic-lsp-not-f.md +0 -14
  102. package/dist/template/.opencode/memory/observations/2026-02-01-learning-kimi-k2-5-model-requires-temperature-1-0.md +0 -22
  103. package/dist/template/.opencode/memory/observations/2026-02-01-learning-opencode-context-injection-already-imple.md +0 -27
  104. package/dist/template/.opencode/memory/observations/2026-02-01-learning-opencode-context-injection-uses-instruct.md +0 -35
  105. package/dist/template/.opencode/memory/observations/2026-02-01-learning-update-build-agent-prompt-to-use-context.md +0 -14
  106. package/dist/template/.opencode/memory/observations/2026-02-01-learning-upgrade-agents-md-using-opencode-expert-.md +0 -14
  107. package/dist/template/.opencode/memory/observations/2026-02-01-learning-upgrade-agents-md-with-opencode-expert-g.md +0 -14
  108. package/dist/template/.opencode/memory/observations/2026-02-01-learning-upgrade-agents-md-with-opencode-expert-r.md +0 -14
  109. package/dist/template/.opencode/memory/observations/2026-02-01-learning-user-prefers-copilot-gpt-5-2-codex-mediu.md +0 -14
  110. package/dist/template/.opencode/memory/observations/2026-02-01-learning-user-wants-general-agent-prompt-contextu.md +0 -15
  111. package/dist/template/.opencode/memory/observations/2026-02-01-learning-user-wants-general-agent-prompt-reviewed.md +0 -15
  112. package/dist/template/.opencode/memory/project/architecture.md +0 -60
  113. package/dist/template/.opencode/memory/project/command-rules.md +0 -122
  114. package/dist/template/.opencode/memory/project/commands.md +0 -72
  115. package/dist/template/.opencode/memory/project/conventions.md +0 -68
  116. package/dist/template/.opencode/memory/project/gotchas.md +0 -41
  117. /package/dist/template/.opencode/memory/_templates/{project/tech-stack.md → tech-stack.md} +0 -0
  118. /package/dist/template/.opencode/memory/{user.example.md → _templates/user.md} +0 -0
  119. /package/dist/template/.opencode/memory/{user.md → project/user.md} +0 -0
@@ -0,0 +1,828 @@
1
+ /**
2
+ * Memory Database Module
3
+ *
4
+ * SQLite + FTS5 backend for OpenCodeKit memory system.
5
+ * Provides fast full-text search and structured storage for observations.
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
+ // ============================================================================
89
+ // Schema
90
+ // ============================================================================
91
+
92
+ const SCHEMA_VERSION = 1;
93
+
94
+ const SCHEMA_SQL = `
95
+ -- Schema versioning for migrations
96
+ CREATE TABLE IF NOT EXISTS schema_versions (
97
+ id INTEGER PRIMARY KEY,
98
+ version INTEGER UNIQUE NOT NULL,
99
+ applied_at TEXT NOT NULL
100
+ );
101
+
102
+ -- Observations table (enhanced schema)
103
+ CREATE TABLE IF NOT EXISTS observations (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ type TEXT NOT NULL CHECK(type IN ('decision','bugfix','feature','pattern','discovery','learning','warning')),
106
+ title TEXT NOT NULL,
107
+ subtitle TEXT,
108
+ facts TEXT,
109
+ narrative TEXT,
110
+ concepts TEXT,
111
+ files_read TEXT,
112
+ files_modified TEXT,
113
+ confidence TEXT CHECK(confidence IN ('high','medium','low')) DEFAULT 'high',
114
+ bead_id TEXT,
115
+ supersedes INTEGER,
116
+ superseded_by INTEGER,
117
+ valid_until TEXT,
118
+ markdown_file TEXT,
119
+ created_at TEXT NOT NULL,
120
+ created_at_epoch INTEGER NOT NULL,
121
+ updated_at TEXT,
122
+ FOREIGN KEY(supersedes) REFERENCES observations(id) ON DELETE SET NULL,
123
+ FOREIGN KEY(superseded_by) REFERENCES observations(id) ON DELETE SET NULL
124
+ );
125
+
126
+ -- FTS5 virtual table for full-text search
127
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
128
+ title,
129
+ subtitle,
130
+ narrative,
131
+ facts,
132
+ concepts,
133
+ content='observations',
134
+ content_rowid='id'
135
+ );
136
+
137
+ -- Indexes for common queries
138
+ CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
139
+ CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
140
+ CREATE INDEX IF NOT EXISTS idx_observations_bead_id ON observations(bead_id);
141
+ CREATE INDEX IF NOT EXISTS idx_observations_superseded ON observations(superseded_by) WHERE superseded_by IS NOT NULL;
142
+
143
+ -- Memory files table (for non-observation memory files)
144
+ CREATE TABLE IF NOT EXISTS memory_files (
145
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
146
+ file_path TEXT UNIQUE NOT NULL,
147
+ content TEXT NOT NULL,
148
+ mode TEXT CHECK(mode IN ('replace', 'append')) DEFAULT 'replace',
149
+ created_at TEXT NOT NULL,
150
+ created_at_epoch INTEGER NOT NULL,
151
+ updated_at TEXT,
152
+ updated_at_epoch INTEGER
153
+ );
154
+
155
+ CREATE INDEX IF NOT EXISTS idx_memory_files_path ON memory_files(file_path);
156
+ `;
157
+
158
+ // FTS5 sync triggers (separate because they can't use IF NOT EXISTS)
159
+ const FTS_TRIGGERS_SQL = `
160
+ -- Sync trigger for INSERT
161
+ CREATE TRIGGER IF NOT EXISTS observations_fts_ai AFTER INSERT ON observations BEGIN
162
+ INSERT INTO observations_fts(rowid, title, subtitle, narrative, facts, concepts)
163
+ VALUES (new.id, new.title, new.subtitle, new.narrative, new.facts, new.concepts);
164
+ END;
165
+
166
+ -- Sync trigger for DELETE
167
+ CREATE TRIGGER IF NOT EXISTS observations_fts_ad AFTER DELETE ON observations BEGIN
168
+ INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, facts, concepts)
169
+ VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.facts, old.concepts);
170
+ END;
171
+
172
+ -- Sync trigger for UPDATE
173
+ CREATE TRIGGER IF NOT EXISTS observations_fts_au AFTER UPDATE ON observations BEGIN
174
+ INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, facts, concepts)
175
+ VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.facts, old.concepts);
176
+ INSERT INTO observations_fts(rowid, title, subtitle, narrative, facts, concepts)
177
+ VALUES (new.id, new.title, new.subtitle, new.narrative, new.facts, new.concepts);
178
+ END;
179
+ `;
180
+
181
+ // ============================================================================
182
+ // Database Manager
183
+ // ============================================================================
184
+
185
+ let dbInstance: Database | null = null;
186
+
187
+ /**
188
+ * Get or create the memory database instance.
189
+ * Uses singleton pattern to reuse connection.
190
+ */
191
+ export function getMemoryDB(): Database {
192
+ if (dbInstance) return dbInstance;
193
+
194
+ const dbPath = path.join(process.cwd(), ".opencode/memory.db");
195
+ dbInstance = new Database(dbPath, { create: true });
196
+
197
+ // Enable WAL mode for better concurrency
198
+ dbInstance.run("PRAGMA journal_mode = WAL");
199
+ dbInstance.run("PRAGMA foreign_keys = ON");
200
+
201
+ // Initialize schema
202
+ initializeSchema(dbInstance);
203
+
204
+ return dbInstance;
205
+ }
206
+
207
+ /**
208
+ * Close the database connection (for cleanup).
209
+ */
210
+ export function closeMemoryDB(): void {
211
+ if (dbInstance) {
212
+ dbInstance.close();
213
+ dbInstance = null;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Initialize database schema if not exists.
219
+ */
220
+ function initializeSchema(db: Database): void {
221
+ // Check current schema version
222
+ try {
223
+ const versionRow = db
224
+ .query("SELECT MAX(version) as version FROM schema_versions")
225
+ .get() as {
226
+ version: number | null;
227
+ } | null;
228
+ const currentVersion = versionRow?.version ?? 0;
229
+
230
+ if (currentVersion >= SCHEMA_VERSION) {
231
+ return; // Schema is up to date
232
+ }
233
+ } catch {
234
+ // schema_versions table doesn't exist, need full init
235
+ }
236
+
237
+ // Run schema creation
238
+ db.exec(SCHEMA_SQL);
239
+
240
+ // Run FTS triggers (handle if already exists)
241
+ try {
242
+ db.exec(FTS_TRIGGERS_SQL);
243
+ } catch {
244
+ // Triggers may already exist, ignore
245
+ }
246
+
247
+ // Record schema version
248
+ db.run(
249
+ "INSERT OR REPLACE INTO schema_versions (id, version, applied_at) VALUES (1, ?, ?)",
250
+ [SCHEMA_VERSION, new Date().toISOString()],
251
+ );
252
+ }
253
+
254
+ // ============================================================================
255
+ // Observation Operations
256
+ // ============================================================================
257
+
258
+ /**
259
+ * Store a new observation in the database.
260
+ */
261
+ export function storeObservation(input: ObservationInput): number {
262
+ const db = getMemoryDB();
263
+ const now = new Date();
264
+
265
+ const result = db
266
+ .query(
267
+ `
268
+ INSERT INTO observations (
269
+ type, title, subtitle, facts, narrative, concepts,
270
+ files_read, files_modified, confidence, bead_id,
271
+ supersedes, markdown_file, created_at, created_at_epoch
272
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
273
+ `,
274
+ )
275
+ .run(
276
+ input.type,
277
+ input.title,
278
+ input.subtitle ?? null,
279
+ input.facts ? JSON.stringify(input.facts) : null,
280
+ input.narrative ?? null,
281
+ input.concepts ? JSON.stringify(input.concepts) : null,
282
+ input.files_read ? JSON.stringify(input.files_read) : null,
283
+ input.files_modified ? JSON.stringify(input.files_modified) : null,
284
+ input.confidence ?? "high",
285
+ input.bead_id ?? null,
286
+ input.supersedes ?? null,
287
+ input.markdown_file ?? null,
288
+ now.toISOString(),
289
+ now.getTime(),
290
+ );
291
+
292
+ const insertedId = Number(result.lastInsertRowid);
293
+
294
+ // Update supersedes relationship
295
+ if (input.supersedes) {
296
+ db.run("UPDATE observations SET superseded_by = ? WHERE id = ?", [
297
+ insertedId,
298
+ input.supersedes,
299
+ ]);
300
+ }
301
+
302
+ return insertedId;
303
+ }
304
+
305
+ /**
306
+ * Get observation by ID.
307
+ */
308
+ export function getObservationById(id: number): ObservationRow | null {
309
+ const db = getMemoryDB();
310
+ return db
311
+ .query("SELECT * FROM observations WHERE id = ?")
312
+ .get(id) as ObservationRow | null;
313
+ }
314
+
315
+ /**
316
+ * Get multiple observations by IDs.
317
+ */
318
+ export function getObservationsByIds(ids: number[]): ObservationRow[] {
319
+ if (ids.length === 0) return [];
320
+
321
+ const db = getMemoryDB();
322
+ const placeholders = ids.map(() => "?").join(",");
323
+ return db
324
+ .query(`SELECT * FROM observations WHERE id IN (${placeholders})`)
325
+ .all(...ids) as ObservationRow[];
326
+ }
327
+
328
+ /**
329
+ * Search observations using FTS5.
330
+ * Returns compact index results for progressive disclosure.
331
+ */
332
+ export function searchObservationsFTS(
333
+ query: string,
334
+ options: {
335
+ type?: ObservationType;
336
+ concepts?: string[];
337
+ limit?: number;
338
+ } = {},
339
+ ): SearchIndexResult[] {
340
+ const db = getMemoryDB();
341
+ const limit = options.limit ?? 10;
342
+
343
+ // Build FTS5 query - escape special characters
344
+ const ftsQuery = query
345
+ .replace(/['"]/g, '""')
346
+ .split(/\s+/)
347
+ .filter((term) => term.length > 0)
348
+ .map((term) => `"${term}"*`)
349
+ .join(" OR ");
350
+
351
+ if (!ftsQuery) {
352
+ // Empty query - return recent observations
353
+ return db
354
+ .query(
355
+ `
356
+ SELECT id, type, title,
357
+ substr(COALESCE(narrative, ''), 1, 100) as snippet,
358
+ created_at,
359
+ 0 as relevance_score
360
+ FROM observations
361
+ WHERE superseded_by IS NULL
362
+ ${options.type ? "AND type = ?" : ""}
363
+ ORDER BY created_at_epoch DESC
364
+ LIMIT ?
365
+ `,
366
+ )
367
+ .all(
368
+ ...(options.type ? [options.type, limit] : [limit]),
369
+ ) as SearchIndexResult[];
370
+ }
371
+
372
+ try {
373
+ // Use FTS5 with BM25 ranking
374
+ let sql = `
375
+ SELECT o.id, o.type, o.title,
376
+ substr(COALESCE(o.narrative, ''), 1, 100) as snippet,
377
+ o.created_at,
378
+ bm25(observations_fts) as relevance_score
379
+ FROM observations o
380
+ JOIN observations_fts fts ON fts.rowid = o.id
381
+ WHERE observations_fts MATCH ?
382
+ AND o.superseded_by IS NULL
383
+ `;
384
+
385
+ const params: (string | number)[] = [ftsQuery];
386
+
387
+ if (options.type) {
388
+ sql += " AND o.type = ?";
389
+ params.push(options.type);
390
+ }
391
+
392
+ sql += " ORDER BY relevance_score LIMIT ?";
393
+ params.push(limit);
394
+
395
+ return db.query(sql).all(...params) as SearchIndexResult[];
396
+ } catch {
397
+ // FTS5 query failed, fallback to LIKE search
398
+ return fallbackLikeSearch(db, query, options.type, limit);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Fallback search using LIKE (for when FTS5 fails).
404
+ */
405
+ function fallbackLikeSearch(
406
+ db: Database,
407
+ query: string,
408
+ type: ObservationType | undefined,
409
+ limit: number,
410
+ ): SearchIndexResult[] {
411
+ const likePattern = `%${query}%`;
412
+
413
+ let sql = `
414
+ SELECT id, type, title,
415
+ substr(COALESCE(narrative, ''), 1, 100) as snippet,
416
+ created_at,
417
+ 0 as relevance_score
418
+ FROM observations
419
+ WHERE superseded_by IS NULL
420
+ AND (title LIKE ? OR narrative LIKE ? OR concepts LIKE ?)
421
+ `;
422
+
423
+ const params: (string | number)[] = [likePattern, likePattern, likePattern];
424
+
425
+ if (type) {
426
+ sql += " AND type = ?";
427
+ params.push(type);
428
+ }
429
+
430
+ sql += " ORDER BY created_at_epoch DESC LIMIT ?";
431
+ params.push(limit);
432
+
433
+ return db.query(sql).all(...params) as SearchIndexResult[];
434
+ }
435
+
436
+ /**
437
+ * Get timeline around an anchor observation.
438
+ */
439
+ export function getTimelineAroundObservation(
440
+ anchorId: number,
441
+ depthBefore = 5,
442
+ depthAfter = 5,
443
+ ): {
444
+ anchor: ObservationRow | null;
445
+ before: SearchIndexResult[];
446
+ after: SearchIndexResult[];
447
+ } {
448
+ const db = getMemoryDB();
449
+
450
+ const anchor = getObservationById(anchorId);
451
+ if (!anchor) {
452
+ return { anchor: null, before: [], after: [] };
453
+ }
454
+
455
+ const before = db
456
+ .query(
457
+ `
458
+ SELECT id, type, title,
459
+ substr(COALESCE(narrative, ''), 1, 100) as snippet,
460
+ created_at,
461
+ 0 as relevance_score
462
+ FROM observations
463
+ WHERE created_at_epoch < ?
464
+ AND superseded_by IS NULL
465
+ ORDER BY created_at_epoch DESC
466
+ LIMIT ?
467
+ `,
468
+ )
469
+ .all(anchor.created_at_epoch, depthBefore) as SearchIndexResult[];
470
+
471
+ const after = db
472
+ .query(
473
+ `
474
+ SELECT id, type, title,
475
+ substr(COALESCE(narrative, ''), 1, 100) as snippet,
476
+ created_at,
477
+ 0 as relevance_score
478
+ FROM observations
479
+ WHERE created_at_epoch > ?
480
+ AND superseded_by IS NULL
481
+ ORDER BY created_at_epoch ASC
482
+ LIMIT ?
483
+ `,
484
+ )
485
+ .all(anchor.created_at_epoch, depthAfter) as SearchIndexResult[];
486
+
487
+ return {
488
+ anchor,
489
+ before: before.reverse(),
490
+ after,
491
+ };
492
+ }
493
+
494
+ /**
495
+ * Get most recent observation.
496
+ */
497
+ export function getMostRecentObservation(): ObservationRow | null {
498
+ const db = getMemoryDB();
499
+ return db
500
+ .query(
501
+ "SELECT * FROM observations WHERE superseded_by IS NULL ORDER BY created_at_epoch DESC LIMIT 1",
502
+ )
503
+ .get() as ObservationRow | null;
504
+ }
505
+
506
+ /**
507
+ * Get observation count by type.
508
+ */
509
+ export function getObservationStats(): Record<string, number> {
510
+ const db = getMemoryDB();
511
+ const rows = db
512
+ .query(
513
+ `
514
+ SELECT type, COUNT(*) as count
515
+ FROM observations
516
+ WHERE superseded_by IS NULL
517
+ GROUP BY type
518
+ `,
519
+ )
520
+ .all() as { type: string; count: number }[];
521
+
522
+ const stats: Record<string, number> = { total: 0 };
523
+ for (const row of rows) {
524
+ stats[row.type] = row.count;
525
+ stats.total += row.count;
526
+ }
527
+ return stats;
528
+ }
529
+
530
+ // ============================================================================
531
+ // Memory File Operations
532
+ // ============================================================================
533
+
534
+ /**
535
+ * Store or update a memory file.
536
+ */
537
+ export function upsertMemoryFile(
538
+ filePath: string,
539
+ content: string,
540
+ mode: "replace" | "append" = "replace",
541
+ ): void {
542
+ const db = getMemoryDB();
543
+ const now = new Date();
544
+
545
+ db.run(
546
+ `
547
+ INSERT INTO memory_files (file_path, content, mode, created_at, created_at_epoch)
548
+ VALUES (?, ?, ?, ?, ?)
549
+ ON CONFLICT(file_path) DO UPDATE SET
550
+ content = CASE WHEN excluded.mode = 'append' THEN memory_files.content || '\n\n' || excluded.content ELSE excluded.content END,
551
+ mode = excluded.mode,
552
+ updated_at = ?,
553
+ updated_at_epoch = ?
554
+ `,
555
+ [
556
+ filePath,
557
+ content,
558
+ mode,
559
+ now.toISOString(),
560
+ now.getTime(),
561
+ now.toISOString(),
562
+ now.getTime(),
563
+ ],
564
+ );
565
+ }
566
+
567
+ /**
568
+ * Get a memory file by path.
569
+ */
570
+ export function getMemoryFile(filePath: string): MemoryFileRow | null {
571
+ const db = getMemoryDB();
572
+ return db
573
+ .query("SELECT * FROM memory_files WHERE file_path = ?")
574
+ .get(filePath) as MemoryFileRow | null;
575
+ }
576
+
577
+ // ============================================================================
578
+ // FTS5 Maintenance
579
+ // ============================================================================
580
+
581
+ /**
582
+ * Optimize FTS5 index (run periodically).
583
+ */
584
+ export function optimizeFTS5(): void {
585
+ const db = getMemoryDB();
586
+ db.run("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
587
+ }
588
+
589
+ /**
590
+ * Rebuild FTS5 index from scratch.
591
+ */
592
+ export function rebuildFTS5(): void {
593
+ const db = getMemoryDB();
594
+ db.run("INSERT INTO observations_fts(observations_fts) VALUES('rebuild')");
595
+ }
596
+
597
+ /**
598
+ * Check if FTS5 is available and working.
599
+ */
600
+ export function checkFTS5Available(): boolean {
601
+ try {
602
+ const db = getMemoryDB();
603
+ db.query("SELECT * FROM observations_fts LIMIT 1").get();
604
+ return true;
605
+ } catch {
606
+ return false;
607
+ }
608
+ }
609
+
610
+ // ============================================================================
611
+ // Database Maintenance
612
+ // ============================================================================
613
+
614
+ export interface MaintenanceStats {
615
+ archived: number;
616
+ vacuumed: boolean;
617
+ checkpointed: boolean;
618
+ prunedMarkdown: number;
619
+ freedBytes: number;
620
+ dbSizeBefore: number;
621
+ dbSizeAfter: number;
622
+ }
623
+
624
+ export interface ArchiveOptions {
625
+ /** Archive observations older than this many days (default: 90) */
626
+ olderThanDays?: number;
627
+ /** Archive superseded observations regardless of age */
628
+ includeSuperseded?: boolean;
629
+ /** Dry run - don't actually archive, just count */
630
+ dryRun?: boolean;
631
+ }
632
+
633
+ /**
634
+ * Archive old observations to a separate table.
635
+ * Archived observations are removed from main table and FTS index.
636
+ */
637
+ export function archiveOldObservations(options: ArchiveOptions = {}): number {
638
+ const db = getMemoryDB();
639
+ const olderThanDays = options.olderThanDays ?? 90;
640
+ const includeSuperseded = options.includeSuperseded ?? true;
641
+ const dryRun = options.dryRun ?? false;
642
+
643
+ const cutoffEpoch = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
644
+
645
+ // Create archive table if not exists
646
+ db.run(`
647
+ CREATE TABLE IF NOT EXISTS observations_archive (
648
+ id INTEGER PRIMARY KEY,
649
+ type TEXT NOT NULL,
650
+ title TEXT NOT NULL,
651
+ subtitle TEXT,
652
+ facts TEXT,
653
+ narrative TEXT,
654
+ concepts TEXT,
655
+ files_read TEXT,
656
+ files_modified TEXT,
657
+ confidence TEXT,
658
+ bead_id TEXT,
659
+ supersedes INTEGER,
660
+ superseded_by INTEGER,
661
+ valid_until TEXT,
662
+ markdown_file TEXT,
663
+ created_at TEXT NOT NULL,
664
+ created_at_epoch INTEGER NOT NULL,
665
+ updated_at TEXT,
666
+ archived_at TEXT NOT NULL
667
+ )
668
+ `);
669
+
670
+ // Build WHERE clause
671
+ let whereClause = `created_at_epoch < ${cutoffEpoch}`;
672
+ if (includeSuperseded) {
673
+ whereClause = `(${whereClause} OR superseded_by IS NOT NULL)`;
674
+ }
675
+
676
+ // Count candidates
677
+ const countResult = db
678
+ .query(`SELECT COUNT(*) as count FROM observations WHERE ${whereClause}`)
679
+ .get() as { count: number };
680
+
681
+ if (dryRun || countResult.count === 0) {
682
+ return countResult.count;
683
+ }
684
+
685
+ // Move to archive
686
+ const now = new Date().toISOString();
687
+ db.run(`
688
+ INSERT INTO observations_archive
689
+ SELECT *, '${now}' as archived_at FROM observations WHERE ${whereClause}
690
+ `);
691
+
692
+ // Delete from main table (triggers will remove from FTS)
693
+ db.run(`DELETE FROM observations WHERE ${whereClause}`);
694
+
695
+ return countResult.count;
696
+ }
697
+
698
+ /**
699
+ * Checkpoint WAL file back to main database.
700
+ * This reclaims space and improves read performance.
701
+ */
702
+ export function checkpointWAL(): { walSize: number; checkpointed: boolean } {
703
+ const db = getMemoryDB();
704
+
705
+ try {
706
+ // TRUNCATE mode: checkpoint and truncate WAL to zero
707
+ const result = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get() as {
708
+ busy: number;
709
+ log: number;
710
+ checkpointed: number;
711
+ };
712
+
713
+ return {
714
+ walSize: result.log,
715
+ checkpointed: result.busy === 0,
716
+ };
717
+ } catch {
718
+ return { walSize: 0, checkpointed: false };
719
+ }
720
+ }
721
+
722
+ /**
723
+ * Vacuum database to reclaim space and defragment.
724
+ */
725
+ export function vacuumDatabase(): boolean {
726
+ const db = getMemoryDB();
727
+ try {
728
+ db.run("VACUUM");
729
+ return true;
730
+ } catch {
731
+ return false;
732
+ }
733
+ }
734
+
735
+ /**
736
+ * Get database file sizes.
737
+ */
738
+ export function getDatabaseSizes(): {
739
+ mainDb: number;
740
+ wal: number;
741
+ shm: number;
742
+ total: number;
743
+ } {
744
+ const db = getMemoryDB();
745
+
746
+ try {
747
+ const pageCount = db.query("PRAGMA page_count").get() as {
748
+ page_count: number;
749
+ };
750
+ const pageSize = db.query("PRAGMA page_size").get() as {
751
+ page_size: number;
752
+ };
753
+ const mainDb = pageCount.page_count * pageSize.page_size;
754
+
755
+ // WAL and SHM sizes from pragma
756
+ const walResult = db.query("PRAGMA wal_checkpoint").get() as {
757
+ busy: number;
758
+ log: number;
759
+ checkpointed: number;
760
+ };
761
+ const wal = walResult.log * pageSize.page_size;
762
+
763
+ return {
764
+ mainDb,
765
+ wal,
766
+ shm: 32768, // SHM is typically 32KB
767
+ total: mainDb + wal + 32768,
768
+ };
769
+ } catch {
770
+ return { mainDb: 0, wal: 0, shm: 0, total: 0 };
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Get list of markdown files that exist in SQLite (for pruning).
776
+ */
777
+ export function getMarkdownFilesInSqlite(): string[] {
778
+ const db = getMemoryDB();
779
+ const rows = db
780
+ .query(
781
+ "SELECT markdown_file FROM observations WHERE markdown_file IS NOT NULL",
782
+ )
783
+ .all() as { markdown_file: string }[];
784
+
785
+ return rows.map((r) => r.markdown_file);
786
+ }
787
+
788
+ /**
789
+ * Run full maintenance cycle.
790
+ */
791
+ export function runFullMaintenance(
792
+ options: ArchiveOptions = {},
793
+ ): MaintenanceStats {
794
+ const sizesBefore = getDatabaseSizes();
795
+
796
+ // 1. Archive old observations
797
+ const archived = archiveOldObservations(options);
798
+
799
+ // 2. Optimize FTS5
800
+ if (!options.dryRun) {
801
+ optimizeFTS5();
802
+ }
803
+
804
+ // 3. Checkpoint WAL
805
+ let checkpointed = false;
806
+ if (!options.dryRun) {
807
+ const walResult = checkpointWAL();
808
+ checkpointed = walResult.checkpointed;
809
+ }
810
+
811
+ // 4. Vacuum
812
+ let vacuumed = false;
813
+ if (!options.dryRun) {
814
+ vacuumed = vacuumDatabase();
815
+ }
816
+
817
+ const sizesAfter = getDatabaseSizes();
818
+
819
+ return {
820
+ archived,
821
+ vacuumed,
822
+ checkpointed,
823
+ prunedMarkdown: 0, // Will be set by the tool after file operations
824
+ freedBytes: sizesBefore.total - sizesAfter.total,
825
+ dbSizeBefore: sizesBefore.total,
826
+ dbSizeAfter: sizesAfter.total,
827
+ };
828
+ }