Skip to content

Commit 834595e

Browse files
author
StackMemory Bot (CLI)
committed
fix(schema): unify FrameDatabase and SQLiteAdapter schema initialization
Single canonical schema source in FrameDatabase.initSchema(): - Add project_id to anchors table and Anchor domain type - Add retention_policy to frames table - Add system tables (schema_version, retrieval_log, maintenance_state, project_registry) - Add all missing indexes from SQLiteAdapter SQLiteAdapter.initializeSchema() now delegates base tables to FrameDatabase, keeping only FTS5/vec/migration-specific concerns. Fix ensureCascadeConstraints to handle old DBs missing project_id column. Also: add initializeSchema() call in skills.ts, remove orphaned initDB() from MCP server.
1 parent a69a898 commit 834595e

5 files changed

Lines changed: 106 additions & 129 deletions

File tree

src/cli/commands/skills.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ async function initializeSkillContext(): Promise<{
9393
const embeddingProvider = (await createTransformersProvider()) ?? undefined;
9494
const database = new SQLiteAdapter(projectId, { dbPath, embeddingProvider });
9595
await database.connect();
96+
await database.initializeSchema();
9697

9798
// Get raw database for FrameManager
9899
const rawDatabase = database.getRawDatabase();

src/core/context/frame-database.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ interface EventRow {
3939
interface AnchorRow {
4040
anchor_id: string;
4141
frame_id: string;
42+
project_id: string;
4243
type: string;
4344
text: string;
4445
priority: number;
45-
metadata: string;
4646
created_at: number;
47+
metadata: string;
4748
}
4849

4950
interface CountRow {
@@ -116,6 +117,7 @@ export class FrameDatabase {
116117
digest_json TEXT DEFAULT '{}',
117118
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
118119
closed_at INTEGER,
120+
retention_policy TEXT DEFAULT 'default',
119121
FOREIGN KEY (parent_frame_id) REFERENCES frames(frame_id)
120122
);
121123
`);
@@ -139,23 +141,72 @@ export class FrameDatabase {
139141
CREATE TABLE IF NOT EXISTS anchors (
140142
anchor_id TEXT PRIMARY KEY,
141143
frame_id TEXT NOT NULL,
144+
project_id TEXT NOT NULL DEFAULT '',
142145
type TEXT NOT NULL,
143146
text TEXT NOT NULL,
144147
priority INTEGER NOT NULL DEFAULT 5,
145-
metadata TEXT DEFAULT '{}',
146148
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
149+
metadata TEXT DEFAULT '{}',
147150
FOREIGN KEY (frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE
148151
);
149152
`);
150153

151154
// Create indexes for performance
152155
this.db.exec(`
153-
CREATE INDEX IF NOT EXISTS idx_frames_project_state ON frames(project_id, state);
156+
CREATE INDEX IF NOT EXISTS idx_frames_run ON frames(run_id);
157+
CREATE INDEX IF NOT EXISTS idx_frames_project ON frames(project_id);
154158
CREATE INDEX IF NOT EXISTS idx_frames_parent ON frames(parent_frame_id);
155-
CREATE INDEX IF NOT EXISTS idx_events_frame_seq ON events(frame_id, seq);
159+
CREATE INDEX IF NOT EXISTS idx_frames_state ON frames(state);
160+
CREATE INDEX IF NOT EXISTS idx_frames_created ON frames(created_at DESC);
161+
CREATE INDEX IF NOT EXISTS idx_frames_project_state ON frames(project_id, state);
162+
CREATE INDEX IF NOT EXISTS idx_frames_project_created ON frames(project_id, created_at DESC);
163+
CREATE INDEX IF NOT EXISTS idx_frames_retention_created ON frames(retention_policy, created_at);
164+
CREATE INDEX IF NOT EXISTS idx_events_frame ON events(frame_id);
165+
CREATE INDEX IF NOT EXISTS idx_events_seq ON events(frame_id, seq);
166+
CREATE INDEX IF NOT EXISTS idx_anchors_frame ON anchors(frame_id);
156167
CREATE INDEX IF NOT EXISTS idx_anchors_frame_priority ON anchors(frame_id, priority DESC);
157168
`);
158169

170+
// System tables
171+
this.db.exec(`
172+
CREATE TABLE IF NOT EXISTS schema_version (
173+
version INTEGER PRIMARY KEY,
174+
applied_at INTEGER DEFAULT (unixepoch())
175+
);
176+
177+
CREATE TABLE IF NOT EXISTS retrieval_log (
178+
id INTEGER PRIMARY KEY AUTOINCREMENT,
179+
query_text TEXT NOT NULL,
180+
strategy TEXT NOT NULL,
181+
results_count INTEGER NOT NULL,
182+
top_score REAL,
183+
latency_ms INTEGER NOT NULL,
184+
result_frame_ids TEXT,
185+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
186+
);
187+
CREATE INDEX IF NOT EXISTS idx_retrieval_log_created ON retrieval_log(created_at);
188+
189+
CREATE TABLE IF NOT EXISTS maintenance_state (
190+
key TEXT PRIMARY KEY,
191+
value TEXT NOT NULL,
192+
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
193+
);
194+
195+
CREATE TABLE IF NOT EXISTS project_registry (
196+
project_id TEXT PRIMARY KEY,
197+
repo_path TEXT NOT NULL,
198+
display_name TEXT,
199+
db_path TEXT NOT NULL,
200+
is_active INTEGER DEFAULT 0,
201+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
202+
last_accessed INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
203+
);
204+
CREATE INDEX IF NOT EXISTS idx_project_registry_active ON project_registry(is_active);
205+
206+
-- Set initial schema version if not exists
207+
INSERT OR IGNORE INTO schema_version (version) VALUES (1);
208+
`);
209+
159210
logger.info('Frame database schema initialized');
160211
} catch (error: unknown) {
161212
throw new DatabaseError(
@@ -469,13 +520,14 @@ export class FrameDatabase {
469520
insertAnchor(anchor: Omit<Anchor, 'created_at'>): Anchor {
470521
try {
471522
const stmt = this.db.prepare(`
472-
INSERT INTO anchors (anchor_id, frame_id, type, text, priority, metadata)
473-
VALUES (?, ?, ?, ?, ?, ?)
523+
INSERT INTO anchors (anchor_id, frame_id, project_id, type, text, priority, metadata)
524+
VALUES (?, ?, ?, ?, ?, ?, ?)
474525
`);
475526

476527
const result = stmt.run(
477528
anchor.anchor_id,
478529
anchor.frame_id,
530+
anchor.project_id || '',
479531
anchor.type,
480532
anchor.text,
481533
anchor.priority,

src/core/context/frame-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface FrameContext {
4646
export interface Anchor {
4747
anchor_id: string;
4848
frame_id: string;
49+
project_id?: string;
4950
type:
5051
| 'FACT'
5152
| 'DECISION'

src/core/database/sqlite-adapter.ts

Lines changed: 24 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { Frame, Event, Anchor } from '../context/index.js';
2323
import { logger } from '../monitoring/logger.js';
2424
import { DatabaseError, ErrorCode, ValidationError } from '../errors/index.js';
2525
import type { EmbeddingProvider } from './embedding-provider.js';
26+
import { FrameDatabase } from '../context/frame-database.js';
2627
import * as fs from 'fs/promises';
2728
import * as path from 'path';
2829

@@ -150,101 +151,9 @@ export class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
150151
ErrorCode.DB_CONNECTION_FAILED
151152
);
152153

153-
this.db.exec(`
154-
CREATE TABLE IF NOT EXISTS frames (
155-
frame_id TEXT PRIMARY KEY,
156-
run_id TEXT NOT NULL,
157-
project_id TEXT NOT NULL,
158-
parent_frame_id TEXT REFERENCES frames(frame_id),
159-
depth INTEGER NOT NULL DEFAULT 0,
160-
type TEXT NOT NULL,
161-
name TEXT NOT NULL,
162-
state TEXT DEFAULT 'active',
163-
inputs TEXT DEFAULT '{}',
164-
outputs TEXT DEFAULT '{}',
165-
digest_text TEXT,
166-
digest_json TEXT DEFAULT '{}',
167-
created_at INTEGER DEFAULT (unixepoch()),
168-
closed_at INTEGER
169-
);
170-
171-
CREATE TABLE IF NOT EXISTS events (
172-
event_id TEXT PRIMARY KEY,
173-
run_id TEXT NOT NULL,
174-
frame_id TEXT NOT NULL,
175-
seq INTEGER NOT NULL,
176-
event_type TEXT NOT NULL,
177-
payload TEXT NOT NULL,
178-
ts INTEGER DEFAULT (unixepoch()),
179-
FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE
180-
);
181-
182-
CREATE TABLE IF NOT EXISTS anchors (
183-
anchor_id TEXT PRIMARY KEY,
184-
frame_id TEXT NOT NULL,
185-
project_id TEXT NOT NULL,
186-
type TEXT NOT NULL,
187-
text TEXT NOT NULL,
188-
priority INTEGER DEFAULT 0,
189-
created_at INTEGER DEFAULT (unixepoch()),
190-
metadata TEXT DEFAULT '{}',
191-
FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE
192-
);
193-
194-
CREATE TABLE IF NOT EXISTS schema_version (
195-
version INTEGER PRIMARY KEY,
196-
applied_at INTEGER DEFAULT (unixepoch())
197-
);
198-
199-
-- Indexes for performance
200-
CREATE INDEX IF NOT EXISTS idx_frames_run ON frames(run_id);
201-
CREATE INDEX IF NOT EXISTS idx_frames_project ON frames(project_id);
202-
CREATE INDEX IF NOT EXISTS idx_frames_parent ON frames(parent_frame_id);
203-
CREATE INDEX IF NOT EXISTS idx_frames_state ON frames(state);
204-
CREATE INDEX IF NOT EXISTS idx_frames_created ON frames(created_at DESC);
205-
CREATE INDEX IF NOT EXISTS idx_events_frame ON events(frame_id);
206-
CREATE INDEX IF NOT EXISTS idx_events_seq ON events(frame_id, seq);
207-
CREATE INDEX IF NOT EXISTS idx_anchors_frame ON anchors(frame_id);
208-
209-
-- Composite index for project-scoped time queries (most common access pattern)
210-
CREATE INDEX IF NOT EXISTS idx_frames_project_created ON frames(project_id, created_at DESC);
211-
212-
-- Note: frame_embeddings is a vec0 virtual table with frame_id as PRIMARY KEY;
213-
-- vec0 handles its own indexing so no CREATE INDEX needed.
214-
215-
CREATE TABLE IF NOT EXISTS retrieval_log (
216-
id INTEGER PRIMARY KEY AUTOINCREMENT,
217-
query_text TEXT NOT NULL,
218-
strategy TEXT NOT NULL,
219-
results_count INTEGER NOT NULL,
220-
top_score REAL,
221-
latency_ms INTEGER NOT NULL,
222-
result_frame_ids TEXT,
223-
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
224-
);
225-
226-
CREATE INDEX IF NOT EXISTS idx_retrieval_log_created ON retrieval_log(created_at);
227-
228-
CREATE TABLE IF NOT EXISTS maintenance_state (
229-
key TEXT PRIMARY KEY,
230-
value TEXT NOT NULL,
231-
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
232-
);
233-
234-
CREATE TABLE IF NOT EXISTS project_registry (
235-
project_id TEXT PRIMARY KEY,
236-
repo_path TEXT NOT NULL,
237-
display_name TEXT,
238-
db_path TEXT NOT NULL,
239-
is_active INTEGER DEFAULT 0,
240-
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
241-
last_accessed INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
242-
);
243-
CREATE INDEX IF NOT EXISTS idx_project_registry_active ON project_registry(is_active);
244-
245-
-- Set initial schema version if not exists
246-
INSERT OR IGNORE INTO schema_version (version) VALUES (1);
247-
`);
154+
// Delegate base table creation to FrameDatabase (single canonical schema source)
155+
const frameDb = new FrameDatabase(this.db);
156+
frameDb.initSchema();
248157

249158
// Migration: add retention_policy column if not exists
250159
try {
@@ -294,6 +203,13 @@ export class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
294203
);
295204
};
296205

206+
const hasColumn = (table: string, column: string): boolean => {
207+
const cols = this.db!.prepare(
208+
`PRAGMA table_info(${table})`
209+
).all() as Array<{ name: string }>;
210+
return cols.some((c) => c.name === column);
211+
};
212+
297213
const migrateTable = (table: 'events' | 'anchors') => {
298214
const createSql =
299215
table === 'events'
@@ -310,7 +226,7 @@ export class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
310226
: `CREATE TABLE anchors_new (
311227
anchor_id TEXT PRIMARY KEY,
312228
frame_id TEXT NOT NULL,
313-
project_id TEXT NOT NULL,
229+
project_id TEXT NOT NULL DEFAULT '',
314230
type TEXT NOT NULL,
315231
text TEXT NOT NULL,
316232
priority INTEGER DEFAULT 0,
@@ -319,6 +235,17 @@ export class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
319235
FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE
320236
);`;
321237

238+
// For anchors, handle missing project_id column in old schemas
239+
let selectCols: string;
240+
if (table === 'anchors') {
241+
const hasProjectId = hasColumn('anchors', 'project_id');
242+
selectCols = hasProjectId
243+
? 'anchor_id, frame_id, project_id, type, text, priority, created_at, metadata'
244+
: `anchor_id, frame_id, '${this.projectId}' as project_id, type, text, priority, created_at, metadata`;
245+
} else {
246+
selectCols = 'event_id, run_id, frame_id, seq, event_type, payload, ts';
247+
}
248+
322249
const cols =
323250
table === 'events'
324251
? 'event_id, run_id, frame_id, seq, event_type, payload, ts'
@@ -338,7 +265,7 @@ export class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
338265
this.db!.exec('BEGIN;');
339266
this.db!.exec(createSql);
340267
this.db!.prepare(
341-
`INSERT INTO ${table === 'events' ? 'events_new' : 'anchors_new'} (${cols}) SELECT ${cols} FROM ${table}`
268+
`INSERT INTO ${table === 'events' ? 'events_new' : 'anchors_new'} (${cols}) SELECT ${selectCols} FROM ${table}`
342269
).run();
343270
this.db!.exec(`DROP TABLE ${table};`);
344271
this.db!.exec(`ALTER TABLE ${table}_new RENAME TO ${table};`);

src/integrations/mcp/server.ts

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,28 @@ class LocalStackMemoryMCP {
9999
// Initialize database
100100
const dbPath = join(dbDir, 'context.db');
101101
this.db = new Database(dbPath);
102-
this.initDB();
102+
103+
// MCP-specific tables for context tracking and attention logging
104+
this.db.exec(`
105+
CREATE TABLE IF NOT EXISTS contexts (
106+
id TEXT PRIMARY KEY,
107+
type TEXT NOT NULL,
108+
content TEXT NOT NULL,
109+
importance REAL DEFAULT 0.5,
110+
created_at INTEGER DEFAULT (unixepoch()),
111+
last_accessed INTEGER DEFAULT (unixepoch()),
112+
access_count INTEGER DEFAULT 1
113+
);
114+
115+
CREATE TABLE IF NOT EXISTS attention_log (
116+
id INTEGER PRIMARY KEY AUTOINCREMENT,
117+
context_id TEXT,
118+
query TEXT,
119+
response TEXT,
120+
influence_score REAL,
121+
timestamp INTEGER DEFAULT (unixepoch())
122+
);
123+
`);
103124

104125
// Initialize frame manager
105126
this.frameManager = new FrameManager(this.db, this.projectId);
@@ -205,31 +226,6 @@ class LocalStackMemoryMCP {
205226
}
206227
}
207228

208-
private initDB() {
209-
// Note: Don't create frames table here - FrameManager handles the schema
210-
// with the full run_id, project_id, parent_frame_id columns
211-
this.db.exec(`
212-
CREATE TABLE IF NOT EXISTS contexts (
213-
id TEXT PRIMARY KEY,
214-
type TEXT NOT NULL,
215-
content TEXT NOT NULL,
216-
importance REAL DEFAULT 0.5,
217-
created_at INTEGER DEFAULT (unixepoch()),
218-
last_accessed INTEGER DEFAULT (unixepoch()),
219-
access_count INTEGER DEFAULT 1
220-
);
221-
222-
CREATE TABLE IF NOT EXISTS attention_log (
223-
id INTEGER PRIMARY KEY AUTOINCREMENT,
224-
context_id TEXT,
225-
query TEXT,
226-
response TEXT,
227-
influence_score REAL,
228-
timestamp INTEGER DEFAULT (unixepoch())
229-
);
230-
`);
231-
}
232-
233229
private loadInitialContext() {
234230
// Load project information
235231
const projectInfo = this.getProjectInfo();

0 commit comments

Comments
 (0)