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.
- package/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +64 -3
- package/dist/template/.opencode/command/create.md +34 -0
- package/dist/template/.opencode/command/design.md +35 -0
- package/dist/template/.opencode/command/handoff.md +15 -0
- package/dist/template/.opencode/command/init.md +40 -47
- package/dist/template/.opencode/command/plan.md +1 -0
- package/dist/template/.opencode/command/pr.md +15 -0
- package/dist/template/.opencode/command/research.md +3 -0
- package/dist/template/.opencode/command/resume.md +1 -0
- package/dist/template/.opencode/command/review-codebase.md +30 -0
- package/dist/template/.opencode/command/ship.md +43 -0
- package/dist/template/.opencode/command/start.md +1 -0
- package/dist/template/.opencode/command/status.md +24 -1
- package/dist/template/.opencode/command/ui-review.md +31 -0
- package/dist/template/.opencode/command/verify.md +35 -7
- package/dist/template/.opencode/memory/project/tech-stack.md +25 -22
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +817 -916
- package/dist/template/.opencode/package.json +1 -0
- package/dist/template/.opencode/plans/1770006237537-mighty-otter.md +418 -0
- package/dist/template/.opencode/plans/1770006913647-glowing-forest.md +170 -0
- package/dist/template/.opencode/plans/1770013678126-witty-planet.md +278 -0
- package/dist/template/.opencode/plugin/lib/memory-db.ts +828 -0
- package/dist/template/.opencode/plugin/memory.ts +38 -1
- package/dist/template/.opencode/skill/index-knowledge/SKILL.md +76 -31
- package/dist/template/.opencode/skill/memory-system/SKILL.md +110 -55
- package/dist/template/.opencode/tool/memory-get.ts +143 -0
- package/dist/template/.opencode/tool/memory-maintain.ts +167 -0
- package/dist/template/.opencode/tool/memory-migrate.ts +319 -0
- package/dist/template/.opencode/tool/memory-read.ts +17 -46
- package/dist/template/.opencode/tool/memory-search.ts +131 -28
- package/dist/template/.opencode/tool/memory-timeline.ts +105 -0
- package/dist/template/.opencode/tool/memory-update.ts +21 -26
- package/dist/template/.opencode/tool/observation.ts +112 -100
- package/dist/template/.opencode/tsconfig.json +19 -19
- package/package.json +1 -1
- package/dist/template/.opencode/memory/_templates/README.md +0 -73
- package/dist/template/.opencode/memory/_templates/observation.md +0 -39
- package/dist/template/.opencode/memory/_templates/prompt-engineering.md +0 -333
- package/dist/template/.opencode/memory/observations/2026-01-22-decision-agents-md-prompt-engineering-improvement.md +0 -29
- package/dist/template/.opencode/memory/observations/2026-01-25-decision-agent-roles-build-orchestrates-general-e.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-01-25-decision-simplified-swarm-helper-tool-to-fix-type.md +0 -20
- package/dist/template/.opencode/memory/observations/2026-01-25-decision-use-beads-as-swarm-board-source-of-truth.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-01-25-learning-user-wants-real-swarm-coordination-guida.md +0 -15
- package/dist/template/.opencode/memory/observations/2026-01-28-decision-created-deep-research-skill-for-thorough.md +0 -29
- package/dist/template/.opencode/memory/observations/2026-01-28-decision-gh-grep-mcp-wrapper-vs-native-grep-searc.md +0 -21
- package/dist/template/.opencode/memory/observations/2026-01-28-decision-oracle-tool-optimal-usage-patterns.md +0 -32
- package/dist/template/.opencode/memory/observations/2026-01-28-learning-ampcode-deep-mode-research-integration-w.md +0 -42
- package/dist/template/.opencode/memory/observations/2026-01-28-pattern-research-delegation-pattern-explore-for-.md +0 -32
- package/dist/template/.opencode/memory/observations/2026-01-29-decision-copilot-auth-plugin-rate-limit-handling.md +0 -27
- package/dist/template/.opencode/memory/observations/2026-01-29-decision-spec-driven-approach-for-opencodekit.md +0 -21
- package/dist/template/.opencode/memory/observations/2026-01-29-learning-karpathy-llm-coding-insights-dec-2025.md +0 -44
- package/dist/template/.opencode/memory/observations/2026-01-30-decision-github-copilot-claude-routing-keep-disab.md +0 -32
- package/dist/template/.opencode/memory/observations/2026-01-30-discovery-context-management-research-critical-gap.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-01-30-discovery-kimi-k2-5-agent-swarm-architecture-patte.md +0 -45
- package/dist/template/.opencode/memory/observations/2026-01-30-pattern-swarm-tools-architecture.md +0 -28
- package/dist/template/.opencode/memory/observations/2026-01-31-decision-copilot-auth-plugin-updated-with-baseurl.md +0 -63
- package/dist/template/.opencode/memory/observations/2026-01-31-decision-created-dedicated-worker-agent-for-swarm.md +0 -20
- package/dist/template/.opencode/memory/observations/2026-01-31-decision-rollback-to-v1-1-47-for-copilot-claude-r.md +0 -21
- package/dist/template/.opencode/memory/observations/2026-01-31-decision-simplified-swarm-to-task-tool-pattern.md +0 -44
- package/dist/template/.opencode/memory/observations/2026-01-31-decision-swarm-architecture-task-tool-over-tmux.md +0 -33
- package/dist/template/.opencode/memory/observations/2026-01-31-decision-worker-skills-defined-for-swarm-delegati.md +0 -30
- package/dist/template/.opencode/memory/observations/2026-01-31-learning-gpt-reasoning-config-for-github-copilot.md +0 -51
- package/dist/template/.opencode/memory/observations/2026-01-31-learning-opencode-copilot-auth-comparison-finding.md +0 -61
- package/dist/template/.opencode/memory/observations/2026-01-31-learning-opencode-copilot-reasoning-architecture-.md +0 -66
- package/dist/template/.opencode/memory/observations/2026-01-31-learning-opencode-custom-tools-api.md +0 -48
- package/dist/template/.opencode/memory/observations/2026-01-31-learning-opencode-v1-1-48-skills-as-slash-command.md +0 -21
- package/dist/template/.opencode/memory/observations/2026-01-31-learning-swarm-system-simplified-removed-mailbox-.md +0 -30
- package/dist/template/.opencode/memory/observations/2026-01-31-learning-v1-1-48-native-copilot-reasoning-via-pr-.md +0 -45
- package/dist/template/.opencode/memory/observations/2026-01-31-warning-cannot-add-custom-config-to-opencode-jso.md +0 -18
- package/dist/template/.opencode/memory/observations/2026-01-31-warning-copilot-claude-v1-endpoint-returns-404-c.md +0 -48
- package/dist/template/.opencode/memory/observations/2026-01-31-warning-opencode-v1-1-48-claude-thinking-block-s.md +0 -51
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-add-skills-vs-commands-to-global-agents-.md +0 -15
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-build-agent-auto-loads-skills-contextual.md +0 -31
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-fixed-agent-configuration-for-opencodeki.md +0 -25
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-focused-agents-md-upgrade-for-opencode-k.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-implement-tier-1-permission-upgrades.md +0 -15
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-instructions-config-explicit-paths-not-w.md +0 -40
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-merged-context-into-memory-project-singl.md +0 -42
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-oracle-tool-should-use-review-agent-not-.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-plan-agent-auto-loads-skills-contextuall.md +0 -31
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-plan-phased-oracle-command-merge-into-ne.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-prd-workflow-uses-prd-and-prd-task-skill.md +0 -23
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-prefer-review-agent-via-opencode-cli-ove.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-remove-oracle-tool-add-ship-command-with.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-remove-oracle-tool-and-add-ship-command-.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-remove-oracle-tool-and-add-ship-command.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-remove-skills-vs-commands-section-from-a.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-replace-oracle-tool-with-ship-command-fl.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-replace-oracle-with-ship-command-workflo.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-replace-proxypal-oracle-with-cli-review-.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-simplified-dist-template-only-tech-stack.md +0 -50
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-simplified-templates-only-tech-stack-md.md +0 -26
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-subagents-load-minimal-skills-stay-lean.md +0 -29
- package/dist/template/.opencode/memory/observations/2026-02-01-decision-user-approved-permission-upgrades-in-ope.md +0 -15
- package/dist/template/.opencode/memory/observations/2026-02-01-discovery-verify-command-already-implemented.md +0 -28
- package/dist/template/.opencode/memory/observations/2026-02-01-feature-openspec-phase-b-complete-template-upgra.md +0 -43
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-build-agent-should-use-dynamic-lsp-not-f.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-kimi-k2-5-model-requires-temperature-1-0.md +0 -22
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-opencode-context-injection-already-imple.md +0 -27
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-opencode-context-injection-uses-instruct.md +0 -35
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-update-build-agent-prompt-to-use-context.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-upgrade-agents-md-using-opencode-expert-.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-upgrade-agents-md-with-opencode-expert-g.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-upgrade-agents-md-with-opencode-expert-r.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-user-prefers-copilot-gpt-5-2-codex-mediu.md +0 -14
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-user-wants-general-agent-prompt-contextu.md +0 -15
- package/dist/template/.opencode/memory/observations/2026-02-01-learning-user-wants-general-agent-prompt-reviewed.md +0 -15
- package/dist/template/.opencode/memory/project/architecture.md +0 -60
- package/dist/template/.opencode/memory/project/command-rules.md +0 -122
- package/dist/template/.opencode/memory/project/commands.md +0 -72
- package/dist/template/.opencode/memory/project/conventions.md +0 -68
- package/dist/template/.opencode/memory/project/gotchas.md +0 -41
- /package/dist/template/.opencode/memory/_templates/{project/tech-stack.md → tech-stack.md} +0 -0
- /package/dist/template/.opencode/memory/{user.example.md → _templates/user.md} +0 -0
- /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
|
+
}
|