Skip to content

Commit 3f0ea86

Browse files
committed
fix: close 7 audit findings from v0.2.0 council review
HIGH fixes: - #5: clearSession now cleans FTS/index data (prevents unbounded DB growth) - #13: Memory TTL expires_at enforced in getMemoriesByProject and searchMemoriesFTS - #14: Embedding API key hash included in model identity (fixes stale provider on key rotation) MEDIUM fixes: - #16: updateSessionMeta moved inside compaction transaction - #11: Added idx_dream_queue_pending index for started_at/enqueued_at - #18: Dream timer returns cleanup function for proper lifecycle management LOW fix: - #12: Removed redundant if(args.drop) guard after early return
1 parent 75ea10c commit 3f0ea86

File tree

12 files changed

+27
-102
lines changed

12 files changed

+27
-102
lines changed

src/features/magic-context/compaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export function createCompactionHandler(): CompactionHandler {
1313
"UPDATE tags SET status = 'compacted' WHERE session_id = ? AND status IN ('active', 'dropped')",
1414
).run(sessionId);
1515
db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
16+
updateSessionMeta(db, sessionId, { lastNudgeBand: null });
1617
})();
17-
updateSessionMeta(db, sessionId, { lastNudgeBand: null });
1818
},
1919
};
2020
}

src/features/magic-context/dreamer/queue.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export function ensureDreamQueueTable(db: Database): void {
2121
)
2222
`);
2323
db.run("CREATE INDEX IF NOT EXISTS idx_dream_queue_project ON dream_queue(project_path)");
24+
db.run(
25+
"CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, enqueued_at)",
26+
);
2427
}
2528

2629
/** Enqueue a project for dreaming. Skips if the same project already has any queue entry (queued or running). */

src/features/magic-context/memory/embedding.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { cosineSimilarity } from "./cosine-similarity";
55
import { LocalEmbeddingProvider } from "./embedding-local";
66
import { OpenAICompatibleEmbeddingProvider } from "./embedding-openai";
77
import type { EmbeddingProvider } from "./embedding-provider";
8+
import { computeNormalizedHash } from "./normalize-hash";
89

910
const DEFAULT_EMBEDDING_CONFIG: EmbeddingConfig = {
1011
provider: "local",
@@ -43,7 +44,8 @@ function resolveModelId(config: EmbeddingConfig): string {
4344
if (config.provider === "openai-compatible") {
4445
const endpoint = config.endpoint.trim();
4546
const model = config.model.trim();
46-
return `openai-compat:${endpoint}:${model}`;
47+
const keyHash = config.api_key ? computeNormalizedHash(config.api_key) : "nokey";
48+
return `openai-compat:${endpoint}:${model}:${keyHash}`;
4749
}
4850

4951
return config.model.trim() || DEFAULT_LOCAL_EMBEDDING_MODEL;

src/features/magic-context/memory/storage-memory-fts.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function getSearchStatement(db: Database): PreparedStatement {
1515
.join(", ");
1616

1717
stmt = db.prepare(
18-
`SELECT ${selectColumns} FROM memories_fts INNER JOIN memories ON memories.id = memories_fts.rowid WHERE memories.project_path = ? AND memories.status IN ('active', 'permanent') AND memories_fts MATCH ? ORDER BY bm25(memories_fts), memories.updated_at DESC, memories.id ASC LIMIT ?`,
18+
`SELECT ${selectColumns} FROM memories_fts INNER JOIN memories ON memories.id = memories_fts.rowid WHERE memories.project_path = ? AND memories.status IN ('active', 'permanent') AND (memories.expires_at IS NULL OR memories.expires_at > ?) AND memories_fts MATCH ? ORDER BY bm25(memories_fts), memories.updated_at DESC, memories.id ASC LIMIT ?`,
1919
);
2020
searchStatements.set(db, stmt);
2121
}
@@ -52,7 +52,9 @@ export function searchMemoriesFTS(
5252
return [];
5353
}
5454

55-
const rows = getSearchStatement(db).all(projectPath, sanitized, limit).filter(isMemoryRow);
55+
const rows = getSearchStatement(db)
56+
.all(projectPath, Date.now(), sanitized, limit)
57+
.filter(isMemoryRow);
5658

5759
return rows.map((row) => ({ ...row }));
5860
}

src/features/magic-context/memory/storage-memory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ function getMemoriesByProjectStatement(db: Database, statuses: MemoryStatus[]):
212212
if (!stmt) {
213213
const placeholders = statuses.map(() => "?").join(", ");
214214
stmt = db.prepare(
215-
`SELECT ${getMemorySelectColumns()} FROM memories WHERE project_path = ? AND status IN (${placeholders}) ORDER BY category ASC, updated_at DESC, id ASC`,
215+
`SELECT ${getMemorySelectColumns()} FROM memories WHERE project_path = ? AND status IN (${placeholders}) AND (expires_at IS NULL OR expires_at > ?) ORDER BY category ASC, updated_at DESC, id ASC`,
216216
);
217217
statements.set(db, stmt);
218218
}
@@ -399,7 +399,7 @@ export function getMemoriesByProject(
399399
}
400400

401401
const rows = getMemoriesByProjectStatement(db, statuses)
402-
.all(projectPath, ...statuses)
402+
.all(projectPath, ...statuses, Date.now())
403403
.filter(isMemoryRow);
404404

405405
return rows.map(toMemory);

src/features/magic-context/message-index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function getLastIndexedOrdinal(db: Database, sessionId: string): number {
7979
return typeof row?.last_indexed_ordinal === "number" ? row.last_indexed_ordinal : 0;
8080
}
8181

82-
function clearIndexedMessages(db: Database, sessionId: string): void {
82+
export function clearIndexedMessages(db: Database, sessionId: string): void {
8383
getDeleteFtsStatement(db).run(sessionId);
8484
getDeleteIndexStatement(db).run(sessionId);
8585
}

src/features/magic-context/storage-db.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export function initializeDatabase(db: Database): void {
122122
retry_count INTEGER DEFAULT 0
123123
);
124124
CREATE INDEX IF NOT EXISTS idx_dream_queue_project ON dream_queue(project_path);
125+
CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, enqueued_at);
125126
126127
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
127128
content,

src/features/magic-context/storage-meta-session.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Database } from "bun:sqlite";
2+
import { clearIndexedMessages } from "./message-index";
23
import {
34
BOOLEAN_META_KEYS,
45
ensureSessionMetaRow,
@@ -73,5 +74,6 @@ export function clearSession(db: Database, sessionId: string): void {
7374
db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
7475
db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
7576
db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
77+
clearIndexedMessages(db, sessionId);
7678
})();
7779
}

src/features/magic-context/storage-meta.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe("storage-meta", () => {
8282

8383
//#then
8484
expect(db.transaction).toHaveBeenCalledTimes(1);
85-
expect(db.prepare).toHaveBeenCalledTimes(9);
85+
expect(db.prepare).toHaveBeenCalledTimes(11);
8686
});
8787
});
8888
});

src/features/magic-context/storage.test.ts

Lines changed: 2 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
updateSessionMeta,
3030
updateTagStatus,
3131
} from "./storage";
32+
import { initializeDatabase } from "./storage-db";
3233

3334
const tempDirs: string[] = [];
3435
const originalXdgDataHome = process.env.XDG_DATA_HOME;
@@ -61,98 +62,7 @@ function resolveDbPath(dataHome: string): string {
6162

6263
function makeMemoryDatabase(): Database {
6364
const db = new Database(":memory:");
64-
db.run(`
65-
CREATE TABLE IF NOT EXISTS tags (
66-
id INTEGER PRIMARY KEY AUTOINCREMENT,
67-
session_id TEXT,
68-
message_id TEXT,
69-
type TEXT,
70-
status TEXT DEFAULT 'active',
71-
byte_size INTEGER,
72-
tag_number INTEGER,
73-
UNIQUE(session_id, tag_number)
74-
);
75-
CREATE TABLE IF NOT EXISTS pending_ops (
76-
id INTEGER PRIMARY KEY AUTOINCREMENT,
77-
session_id TEXT,
78-
tag_id INTEGER,
79-
operation TEXT,
80-
queued_at INTEGER
81-
);
82-
CREATE TABLE IF NOT EXISTS source_contents (
83-
tag_id INTEGER,
84-
session_id TEXT,
85-
content TEXT,
86-
created_at INTEGER,
87-
PRIMARY KEY(session_id, tag_id)
88-
);
89-
CREATE TABLE IF NOT EXISTS compartments (
90-
id INTEGER PRIMARY KEY AUTOINCREMENT,
91-
session_id TEXT NOT NULL,
92-
sequence INTEGER NOT NULL,
93-
start_message INTEGER NOT NULL,
94-
end_message INTEGER NOT NULL,
95-
title TEXT NOT NULL,
96-
content TEXT NOT NULL,
97-
created_at INTEGER NOT NULL,
98-
UNIQUE(session_id, sequence)
99-
);
100-
CREATE TABLE IF NOT EXISTS session_facts (
101-
id INTEGER PRIMARY KEY AUTOINCREMENT,
102-
session_id TEXT NOT NULL,
103-
category TEXT NOT NULL,
104-
content TEXT NOT NULL,
105-
created_at INTEGER NOT NULL,
106-
updated_at INTEGER NOT NULL
107-
);
108-
CREATE TABLE IF NOT EXISTS session_notes (
109-
id INTEGER PRIMARY KEY AUTOINCREMENT,
110-
session_id TEXT NOT NULL,
111-
content TEXT NOT NULL,
112-
created_at INTEGER NOT NULL
113-
);
114-
CREATE TABLE IF NOT EXISTS session_meta (
115-
session_id TEXT PRIMARY KEY,
116-
last_response_time INTEGER,
117-
cache_ttl TEXT,
118-
counter INTEGER DEFAULT 0,
119-
last_nudge_tokens INTEGER DEFAULT 0,
120-
last_nudge_band TEXT DEFAULT '',
121-
last_transform_error TEXT DEFAULT '',
122-
nudge_anchor_message_id TEXT DEFAULT '',
123-
nudge_anchor_text TEXT DEFAULT '',
124-
sticky_turn_reminder_text TEXT DEFAULT '',
125-
sticky_turn_reminder_message_id TEXT DEFAULT '',
126-
is_subagent INTEGER DEFAULT 0,
127-
last_context_percentage REAL DEFAULT 0,
128-
last_input_tokens INTEGER DEFAULT 0,
129-
times_execute_threshold_reached INTEGER DEFAULT 0,
130-
compartment_in_progress INTEGER DEFAULT 0,
131-
system_prompt_hash INTEGER DEFAULT 0
132-
);
133-
CREATE TABLE IF NOT EXISTS recomp_compartments (
134-
id INTEGER PRIMARY KEY AUTOINCREMENT,
135-
session_id TEXT NOT NULL,
136-
sequence INTEGER NOT NULL,
137-
start_message INTEGER NOT NULL,
138-
end_message INTEGER NOT NULL,
139-
start_message_id TEXT DEFAULT '',
140-
end_message_id TEXT DEFAULT '',
141-
title TEXT NOT NULL,
142-
content TEXT NOT NULL,
143-
pass_number INTEGER NOT NULL,
144-
created_at INTEGER NOT NULL,
145-
UNIQUE(session_id, sequence)
146-
);
147-
CREATE TABLE IF NOT EXISTS recomp_facts (
148-
id INTEGER PRIMARY KEY AUTOINCREMENT,
149-
session_id TEXT NOT NULL,
150-
category TEXT NOT NULL,
151-
content TEXT NOT NULL,
152-
pass_number INTEGER NOT NULL,
153-
created_at INTEGER NOT NULL
154-
);
155-
`);
65+
initializeDatabase(db);
15666
return db;
15767
}
15868

0 commit comments

Comments
 (0)