fix: isolate uok message bus db per project

This commit is contained in:
Mikael Hugo 2026-05-07 06:09:32 +02:00
parent 95cb13c08d
commit 2178aa8803
4 changed files with 72 additions and 4 deletions

View file

@ -78,7 +78,7 @@ function openRawDb(path) {
loadProvider();
return new DatabaseSync(path);
}
const SCHEMA_VERSION = 38;
const SCHEMA_VERSION = 39;
function indexExists(db, name) {
return !!db
.prepare(
@ -576,6 +576,16 @@ function initSchema(db, fileBacked) {
tags TEXT NOT NULL DEFAULT '[]'
)
`);
// content_hash is queried on every insert for deduplication; without an
// index the lookup becomes a full table scan as ingestion volume grows.
db.exec(
"CREATE INDEX IF NOT EXISTS idx_memory_sources_content_hash ON memory_sources(content_hash)",
);
// Category GROUP BY queries (e.g. /sf memory stats) need a covering
// index that filters active memories and groups by category.
db.exec(
"CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(superseded_by, category)",
);
db.exec(`
CREATE TABLE IF NOT EXISTS milestones (
id TEXT PRIMARY KEY,
@ -2046,6 +2056,20 @@ function migrateSchema(db) {
":applied_at": new Date().toISOString(),
});
}
if (currentVersion < 39) {
db.exec(
"CREATE INDEX IF NOT EXISTS idx_memory_sources_content_hash ON memory_sources(content_hash)",
);
db.exec(
"CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(superseded_by, category)",
);
db.prepare(
"INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
).run({
":version": 39,
":applied_at": new Date().toISOString(),
});
}
db.exec("COMMIT");
} catch (err) {
db.exec("ROLLBACK");

View file

@ -201,7 +201,7 @@ test("openDatabase_migrates_v27_tasks_without_created_at_through_spec_backfill",
const version = db
.prepare("SELECT MAX(version) AS version FROM schema_version")
.get();
assert.equal(version.version, 38);
assert.equal(version.version, 39);
const taskSpec = db
.prepare(
"SELECT milestone_id, slice_id, task_id, verify FROM task_specs WHERE task_id = 'T01'",
@ -285,3 +285,26 @@ test("openDatabase_memories_table_has_tags_column", () => {
assert.equal(tagsCol.type, "TEXT");
assert.equal(tagsCol.dflt_value, "'[]'");
});
test("openDatabase_memory_indexes_exist", () => {
assert.equal(openDatabase(":memory:"), true);
const db = getDatabase();
const indexes = db
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name IN ('memories', 'memory_sources')",
)
.all();
const names = indexes.map((r) => r.name);
assert.ok(
names.includes("idx_memories_active"),
"should have idx_memories_active",
);
assert.ok(
names.includes("idx_memories_category"),
"should have idx_memories_category",
);
assert.ok(
names.includes("idx_memory_sources_content_hash"),
"should have idx_memory_sources_content_hash",
);
});

View file

@ -6,7 +6,9 @@ import { afterEach, test } from "vitest";
import {
closeDatabase,
getUokMessageBusMetrics,
getUokMessagesForAgent,
insertUokMessage,
openDatabase,
} from "../sf-db.js";
import { AgentInbox, MessageBus } from "../uok/message-bus.js";
@ -274,3 +276,24 @@ test("getUokMessageBusMetrics_ignores_reads_by_non_recipient_for_unread_count",
assert.equal(m.totalMessages, 1);
assert.equal(m.totalUnread, 1);
});
test("messageBus_send_when_switching_projects_uses_current_project_db", () => {
const first = makeProject();
const second = makeProject();
new MessageBus(first).send("agent-a", "agent-b", "first");
new MessageBus(second).send("agent-a", "agent-b", "second");
closeDatabase();
openDatabase(join(first, ".sf", "sf.db"));
assert.deepEqual(
getUokMessagesForAgent("agent-b").map((message) => message.body),
["first"],
);
closeDatabase();
openDatabase(join(second, ".sf", "sf.db"));
assert.deepEqual(
getUokMessagesForAgent("agent-b").map((message) => message.body),
["second"],
);
});

View file

@ -20,7 +20,6 @@ import {
getUokMessageReadIds,
getUokMessagesForAgent,
insertUokMessage,
isDbAvailable,
markUokMessageRead,
openDatabase,
} from "../sf-db.js";
@ -34,7 +33,6 @@ function deterministicMessageId(key) {
}
function ensureDb(basePath) {
if (isDbAvailable()) return;
const dir = sfRoot(basePath);
mkdirSync(dir, { recursive: true });
openDatabase(join(dir, "sf.db"));