Skip to content

Commit babdb30

Browse files
refactor: break storage-db<->migrations cycle + remove genuine dead code
Findings from the aft_inspect/fallow dogfooding pass that apply to our own code: - Circular dependency (fallow-detected, verified at source): storage-db.ts imported runMigrations from migrations.ts while migrations.ts imported ensureColumn/ healAllNullColumns back from storage-db.ts. Function-level so it ran, but it blocks tree-shaking and risks init-order bugs. Extracted the two schema helpers (+ their private heal functions) into a leaf module storage-schema-helpers.ts that depends only on the SQLite handle; both storage-db and migrations now import from the leaf. storage-db re-exports them so existing importers/tests are unaffected. fallow now reports zero circular dependencies. - Dead code: deleted hooks/is-anthropic-provider.ts (isAnthropicProvider, zero refs anywhere) and removed isLocalEmbeddingRuntimeMissing from embedding-local.ts (zero refs; its 'used by callers/tests' docstring was stale). - Dashboard test lint: dropped an unused binding in db_mutations.rs (the only rustc warning aft_inspect surfaced). Gate: plugin 2602/0, plugin tsc + biome clean, dashboard rust build clean. Co-authored-by: Alfonso [Magic Context] <288211368+alfonso-magic-context@users.noreply.github.com>
1 parent 5d430c2 commit babdb30

6 files changed

Lines changed: 168 additions & 175 deletions

File tree

packages/dashboard/src-tauri/tests/db_mutations.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,7 @@ fn test_update_memory_category_collision() {
751751
[],
752752
)
753753
.unwrap();
754-
let id2 = conn.last_insert_rowid();
754+
// Memory 2 exists with category NAMING; we don't need its id below.
755755

756756
// Try to update Memory 1's category to NAMING. This should collide with Memory 2.
757757
let res = db::update_memory_category(&mut conn, id1, "NAMING");

packages/plugin/src/features/magic-context/memory/embedding-local.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,6 @@ async function withQuietConsole<T>(fn: () => Promise<T>): Promise<T> {
273273
// because the missing package affects the whole install, not one model.
274274
let nativeRuntimeMissing = false;
275275

276-
/** Whether local embeddings have been disabled this process due to a missing
277-
* native runtime (issue #128). Used by callers/tests to detect the degraded state. */
278-
export function isLocalEmbeddingRuntimeMissing(): boolean {
279-
return nativeRuntimeMissing;
280-
}
281-
282276
export function isNativeRuntimeMissingError(error: unknown): boolean {
283277
const message = error instanceof Error ? error.message : String(error ?? "");
284278
const lower = message.toLowerCase();

packages/plugin/src/features/magic-context/migrations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { log } from "../../shared/logger";
22
import type { Database } from "../../shared/sqlite";
3-
import { ensureColumn, healAllNullColumns } from "./storage-db";
3+
import { ensureColumn, healAllNullColumns } from "./storage-schema-helpers";
44
import { bumpEpochsForWorkspaceMemberSet } from "./workspaces";
55

66
/**

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

Lines changed: 6 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ import { Database } from "../../shared/sqlite";
1111
import { closeQuietly } from "../../shared/sqlite-helpers";
1212

1313
import { runMigrations } from "./migrations";
14+
import { ensureColumn, healAllNullColumns } from "./storage-schema-helpers";
1415
import {
1516
loadToolDefinitionMeasurements,
1617
setDatabase as setToolDefinitionDatabase,
1718
} from "./tool-definition-tokens";
1819
import { runToolOwnerBackfill } from "./tool-owner-backfill";
1920

21+
// Re-exported so existing `from "./storage-db"` importers (and tests) keep
22+
// resolving these; the definitions live in the leaf module to break the
23+
// storage-db <-> migrations import cycle.
24+
export { ensureColumn, healAllNullColumns };
25+
2026
const databases = new Map<string, Database>();
2127
const persistenceByDatabase = new WeakMap<Database, boolean>();
2228
const persistenceErrorByDatabase = new WeakMap<Database, string>();
@@ -1427,31 +1433,6 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
14271433
// cannot go here because the table doesn't exist yet on a fresh DB.
14281434
}
14291435

1430-
/**
1431-
* Heal NULL columns added via ensureColumn against pre-existing rows.
1432-
*
1433-
* SQLite does NOT backfill column defaults when ALTER TABLE ADD COLUMN runs
1434-
* on an already-populated table — old rows get NULL regardless of the
1435-
* DEFAULT clause. isSessionMetaRow used to require strict typeof === "string"
1436-
* / "number", which NULL fails, so rows with NULL columns were rejected,
1437-
* getOrCreateSessionMeta returned zeroed defaults (lastResponseTime=0,
1438-
* cacheTtl="5m"), the scheduler returned "execute" forever, and every
1439-
* execute pass mutated message content — a sustained cache-bust cascade.
1440-
*
1441-
* The validator now tolerates NULL, but we normalize the data too so every
1442-
* code path sees well-formed values. Each UPDATE is best-effort: if a column
1443-
* doesn't exist yet (migration ran on a DB older than the ensureColumn call),
1444-
* the UPDATE throws and we move on — the next schema upgrade runs ensureColumn
1445-
* first, then this heal again.
1446-
*
1447-
* Exported so migration v5 can call it. Not exported from any barrel.
1448-
*/
1449-
export function healAllNullColumns(db: Database): void {
1450-
healNullTextColumns(db);
1451-
healNullIntegerColumns(db);
1452-
healMissingMemoryBlockIds(db);
1453-
}
1454-
14551436
const CHANNEL2_CLAIM_TTL_MS = 120_000;
14561437

14571438
/**
@@ -1476,145 +1457,6 @@ function healWedgedChannel2Claims(db: Database): void {
14761457
}
14771458
}
14781459

1479-
/**
1480-
* One-shot heal for sessions upgraded from a build without memory_block_ids.
1481-
*
1482-
* Those sessions have a populated memory_block_cache but no ids — ctx_search's
1483-
* visible-memory filter then silently no-ops. Clearing the cache forces the
1484-
* next transform pass to regenerate BOTH cache + ids in one UPDATE. The
1485-
* regenerated block is byte-identical (renderMemoryBlock is deterministic
1486-
* over the same memory set in stable id order), so this does NOT cause an
1487-
* Anthropic prompt-cache bust.
1488-
*
1489-
* Best-effort — wrapped because the columns may not exist on a brand-new DB
1490-
* that hasn't finished ensureColumn yet.
1491-
*/
1492-
function healMissingMemoryBlockIds(db: Database): void {
1493-
try {
1494-
db.prepare(
1495-
"UPDATE session_meta SET memory_block_cache = '' WHERE memory_block_cache != '' AND (memory_block_ids IS NULL OR memory_block_ids = '') AND memory_block_count > 0",
1496-
).run();
1497-
} catch {
1498-
// Column missing on very fresh DBs — next startup reruns this after
1499-
// ensureColumn adds the column.
1500-
}
1501-
}
1502-
1503-
function healNullTextColumns(db: Database): void {
1504-
const columns: Array<[string, string]> = [
1505-
["cache_ttl", ""],
1506-
["last_nudge_band", ""],
1507-
["last_nudge_level", ""],
1508-
["last_transform_error", ""],
1509-
["nudge_anchor_message_id", ""],
1510-
["nudge_anchor_text", ""],
1511-
["sticky_turn_reminder_text", ""],
1512-
["sticky_turn_reminder_message_id", ""],
1513-
["note_nudge_trigger_message_id", ""],
1514-
["note_nudge_sticky_text", ""],
1515-
["note_nudge_sticky_message_id", ""],
1516-
["last_todo_state", ""],
1517-
["todo_synthetic_call_id", ""],
1518-
["todo_synthetic_anchor_message_id", ""],
1519-
["todo_synthetic_state_json", ""],
1520-
["system_prompt_hash", ""],
1521-
["stripped_placeholder_ids", ""],
1522-
["stale_reduce_stripped_ids", ""],
1523-
["processed_image_stripped_ids", ""],
1524-
["memory_block_cache", ""],
1525-
["memory_block_ids", ""],
1526-
["compaction_marker_state", ""],
1527-
["key_files", ""],
1528-
];
1529-
for (const [column, fallback] of columns) {
1530-
try {
1531-
db.prepare(`UPDATE session_meta SET ${column} = ? WHERE ${column} IS NULL`).run(
1532-
fallback,
1533-
);
1534-
} catch (_error) {
1535-
// Ignore — the column may not exist yet on a brand-new DB that
1536-
// hasn't gone through all ensureColumn calls yet. The heal runs
1537-
// again on next startup.
1538-
}
1539-
}
1540-
}
1541-
1542-
function healNullIntegerColumns(db: Database): void {
1543-
// INTEGER columns added via ensureColumn against pre-existing rows.
1544-
// SQLite does not backfill the DEFAULT on ALTER TABLE, so old rows have
1545-
// NULL. The validator tolerates null as of this release, but we still
1546-
// normalize to 0 so subsequent reads from any path (including paths
1547-
// that bypass toSessionMeta) see a well-formed row.
1548-
const columns: Array<[string, number]> = [
1549-
["times_execute_threshold_reached", 0],
1550-
["compartment_in_progress", 0],
1551-
["historian_failure_count", 0],
1552-
["cleared_reasoning_through_tag", 0],
1553-
["memory_block_count", 0],
1554-
["system_prompt_tokens", 0],
1555-
["conversation_tokens", 0],
1556-
["tool_call_tokens", 0],
1557-
["note_nudge_trigger_pending", 0],
1558-
["observed_safe_input_tokens", 0],
1559-
["cache_alert_sent", 0],
1560-
["new_work_tokens", 0],
1561-
["total_input_tokens", 0],
1562-
["last_emergency_input_sample", 0],
1563-
["channel2_nudge_claimed_at", 0],
1564-
["last_usage_context_limit", 0],
1565-
["prior_boundary_ordinal", 1],
1566-
["protected_tail_policy_version", 0],
1567-
["protected_tail_drain_window_started_at", 0],
1568-
["protected_tail_drain_tokens", 0],
1569-
["recovery_no_eligible_head_count", 0],
1570-
["force_emergency_bypass_window_start", 0],
1571-
["force_emergency_bypass_used", 0],
1572-
["emergency_drain_active", 0],
1573-
["historian_drain_failure_at", 0],
1574-
];
1575-
for (const [column, fallback] of columns) {
1576-
try {
1577-
db.prepare(`UPDATE session_meta SET ${column} = ? WHERE ${column} IS NULL`).run(
1578-
fallback,
1579-
);
1580-
} catch (_error) {
1581-
// Same rationale as the text heal — swallow missing-column errors
1582-
// on brand-new DBs; next startup reruns this.
1583-
}
1584-
}
1585-
}
1586-
1587-
// Intentional: the definition regex allows single quotes and parens because SQLite column
1588-
// defaults use them (e.g. TEXT DEFAULT '', INTEGER DEFAULT 0). All callsites pass hardcoded
1589-
// string literals — no user input reaches this function, so the regex is sufficient.
1590-
export function ensureColumn(
1591-
db: Database,
1592-
table: string,
1593-
column: string,
1594-
definition: string,
1595-
): void {
1596-
if (
1597-
!/^[a-z][a-z0-9_]*$/.test(table) ||
1598-
!/^[a-z][a-z0-9_]*$/.test(column) ||
1599-
!/^[A-Z0-9_"'(),[\]\s]+$/i.test(definition)
1600-
) {
1601-
throw new Error(`Unsafe schema identifier: ${table}.${column} ${definition}`);
1602-
}
1603-
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: string }>;
1604-
if (rows.some((row) => row.name === column)) {
1605-
return;
1606-
}
1607-
try {
1608-
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
1609-
} catch (err) {
1610-
const recheck = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: string }>;
1611-
if (recheck.some((row) => row.name === column)) {
1612-
return;
1613-
}
1614-
throw err;
1615-
}
1616-
}
1617-
16181460
/**
16191461
* Open the persistent Magic Context SQLite database.
16201462
*
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { Database } from "../../shared/sqlite";
2+
3+
/**
4+
* Schema-mutation helpers shared by storage-db (fresh-DB init) and migrations
5+
* (versioned upgrades). They live in this leaf module — depending only on the
6+
* SQLite handle — so storage-db and migrations don't import each other (storage-db
7+
* imports `runMigrations` from migrations; without this split, migrations would
8+
* import these back from storage-db and form an import cycle).
9+
*/
10+
11+
// Intentional: the definition regex allows single quotes and parens because SQLite column
12+
// defaults use them (e.g. TEXT DEFAULT '', INTEGER DEFAULT 0). All callsites pass hardcoded
13+
// string literals — no user input reaches this function, so the regex is sufficient.
14+
export function ensureColumn(
15+
db: Database,
16+
table: string,
17+
column: string,
18+
definition: string,
19+
): void {
20+
if (
21+
!/^[a-z][a-z0-9_]*$/.test(table) ||
22+
!/^[a-z][a-z0-9_]*$/.test(column) ||
23+
!/^[A-Z0-9_"'(),[\]\s]+$/i.test(definition)
24+
) {
25+
throw new Error(`Unsafe schema identifier: ${table}.${column} ${definition}`);
26+
}
27+
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: string }>;
28+
if (rows.some((row) => row.name === column)) {
29+
return;
30+
}
31+
try {
32+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
33+
} catch (err) {
34+
const recheck = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: string }>;
35+
if (recheck.some((row) => row.name === column)) {
36+
return;
37+
}
38+
throw err;
39+
}
40+
}
41+
42+
/**
43+
* Heal NULL columns added via ensureColumn against pre-existing rows.
44+
*
45+
* SQLite does NOT backfill column defaults when ALTER TABLE ADD COLUMN runs
46+
* on an already-populated table — old rows get NULL regardless of the
47+
* DEFAULT clause. isSessionMetaRow used to require strict typeof === "string"
48+
* / "number", which NULL fails, so rows with NULL columns were rejected,
49+
* getOrCreateSessionMeta returned zeroed defaults (lastResponseTime=0,
50+
* cacheTtl="5m"), the scheduler returned "execute" forever, and every
51+
* execute pass mutated message content — a sustained cache-bust cascade.
52+
*
53+
* The validator now tolerates NULL, but we normalize the data too so every
54+
* code path sees well-formed values. Each UPDATE is best-effort: if a column
55+
* doesn't exist yet (migration ran on a DB older than the ensureColumn call),
56+
* the UPDATE throws and we move on — the next schema upgrade runs ensureColumn
57+
* first, then this heal again.
58+
*
59+
* Exported so migration v5 can call it. Not exported from any barrel.
60+
*/
61+
export function healAllNullColumns(db: Database): void {
62+
healNullTextColumns(db);
63+
healNullIntegerColumns(db);
64+
healMissingMemoryBlockIds(db);
65+
}
66+
67+
function healMissingMemoryBlockIds(db: Database): void {
68+
try {
69+
db.prepare(
70+
"UPDATE session_meta SET memory_block_cache = '' WHERE memory_block_cache != '' AND (memory_block_ids IS NULL OR memory_block_ids = '') AND memory_block_count > 0",
71+
).run();
72+
} catch {
73+
// Column missing on very fresh DBs — next startup reruns this after
74+
// ensureColumn adds the column.
75+
}
76+
}
77+
78+
function healNullTextColumns(db: Database): void {
79+
const columns: Array<[string, string]> = [
80+
["cache_ttl", ""],
81+
["last_nudge_band", ""],
82+
["last_nudge_level", ""],
83+
["last_transform_error", ""],
84+
["nudge_anchor_message_id", ""],
85+
["nudge_anchor_text", ""],
86+
["sticky_turn_reminder_text", ""],
87+
["sticky_turn_reminder_message_id", ""],
88+
["note_nudge_trigger_message_id", ""],
89+
["note_nudge_sticky_text", ""],
90+
["note_nudge_sticky_message_id", ""],
91+
["last_todo_state", ""],
92+
["todo_synthetic_call_id", ""],
93+
["todo_synthetic_anchor_message_id", ""],
94+
["todo_synthetic_state_json", ""],
95+
["system_prompt_hash", ""],
96+
["stripped_placeholder_ids", ""],
97+
["stale_reduce_stripped_ids", ""],
98+
["processed_image_stripped_ids", ""],
99+
["memory_block_cache", ""],
100+
["memory_block_ids", ""],
101+
["compaction_marker_state", ""],
102+
["key_files", ""],
103+
];
104+
for (const [column, fallback] of columns) {
105+
try {
106+
db.prepare(`UPDATE session_meta SET ${column} = ? WHERE ${column} IS NULL`).run(
107+
fallback,
108+
);
109+
} catch (_error) {
110+
// Ignore — the column may not exist yet on a brand-new DB that
111+
// hasn't gone through all ensureColumn calls yet. The heal runs
112+
// again on next startup.
113+
}
114+
}
115+
}
116+
117+
function healNullIntegerColumns(db: Database): void {
118+
// INTEGER columns added via ensureColumn against pre-existing rows.
119+
// SQLite does not backfill the DEFAULT on ALTER TABLE, so old rows have
120+
// NULL. The validator tolerates null as of this release, but we still
121+
// normalize to 0 so subsequent reads from any path (including paths
122+
// that bypass toSessionMeta) see a well-formed row.
123+
const columns: Array<[string, number]> = [
124+
["times_execute_threshold_reached", 0],
125+
["compartment_in_progress", 0],
126+
["historian_failure_count", 0],
127+
["cleared_reasoning_through_tag", 0],
128+
["memory_block_count", 0],
129+
["system_prompt_tokens", 0],
130+
["conversation_tokens", 0],
131+
["tool_call_tokens", 0],
132+
["note_nudge_trigger_pending", 0],
133+
["observed_safe_input_tokens", 0],
134+
["cache_alert_sent", 0],
135+
["new_work_tokens", 0],
136+
["total_input_tokens", 0],
137+
["last_emergency_input_sample", 0],
138+
["channel2_nudge_claimed_at", 0],
139+
["last_usage_context_limit", 0],
140+
["prior_boundary_ordinal", 1],
141+
["protected_tail_policy_version", 0],
142+
["protected_tail_drain_window_started_at", 0],
143+
["protected_tail_drain_tokens", 0],
144+
["recovery_no_eligible_head_count", 0],
145+
["force_emergency_bypass_window_start", 0],
146+
["force_emergency_bypass_used", 0],
147+
["emergency_drain_active", 0],
148+
["historian_drain_failure_at", 0],
149+
];
150+
for (const [column, fallback] of columns) {
151+
try {
152+
db.prepare(`UPDATE session_meta SET ${column} = ? WHERE ${column} IS NULL`).run(
153+
fallback,
154+
);
155+
} catch (_error) {
156+
// Same rationale as the text heal — swallow missing-column errors
157+
// on brand-new DBs; next startup reruns this.
158+
}
159+
}
160+
}

packages/plugin/src/hooks/is-anthropic-provider.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)