From 715cb8583fe221964d98b8c14b8e7d7e5af1a9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Mon, 27 Apr 2026 05:29:47 +0000 Subject: [PATCH] feat(daemon): auto-persist signals to sense DB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every sense now gets a _signals table (id, payload, timestamp) created automatically. When compute() returns non-null, the signal is persisted to the sense's own SQLite DB before being sent to the Signal Bus. This makes `nerve sense query ` return signal history out of the box — no manual INSERT needed in compute functions. CLI's pickDefaultPreviewTable now prioritizes _signals over other tables. --- packages/cli/src/sense-sqlite.ts | 4 +++- .../src/__tests__/sense-runtime.test.ts | 3 ++- packages/daemon/src/sense-runtime.ts | 21 +++++++++++++++++-- packages/daemon/src/sense-worker.ts | 11 +++++++++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/sense-sqlite.ts b/packages/cli/src/sense-sqlite.ts index af82885..fc52530 100644 --- a/packages/cli/src/sense-sqlite.ts +++ b/packages/cli/src/sense-sqlite.ts @@ -42,7 +42,9 @@ export function pickDefaultPreviewTable(db: DatabaseSync): string | null { WHERE type = 'table' AND sql IS NOT NULL AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' ORDER BY - CASE WHEN name = '_migrations' THEN 1 ELSE 0 END, + CASE WHEN name = '_signals' THEN 0 + WHEN name = '_migrations' THEN 2 + ELSE 1 END, name LIMIT 1`, ) diff --git a/packages/daemon/src/__tests__/sense-runtime.test.ts b/packages/daemon/src/__tests__/sense-runtime.test.ts index 32258c0..d94e5bc 100644 --- a/packages/daemon/src/__tests__/sense-runtime.test.ts +++ b/packages/daemon/src/__tests__/sense-runtime.test.ts @@ -176,7 +176,7 @@ describe("executeCompute", () => { sqlite.exec(INIT_SQL); const db = drizzle({ client: sqlite }) as DrizzleDB; return { - runtime: { name: "test-sense", db, compute: computeFn }, + runtime: { name: "test-sense", db, compute: computeFn, persistSignal: () => {} }, sqlite, }; } @@ -287,6 +287,7 @@ describe("executeCompute", () => { await d.insert(samples).values({ ts: 1000, value: 1.23 }); return 1.23; }, + persistSignal: () => {}, }; const result = await executeCompute(runtime, {}); diff --git a/packages/daemon/src/sense-runtime.ts b/packages/daemon/src/sense-runtime.ts index 5456270..16921a8 100644 --- a/packages/daemon/src/sense-runtime.ts +++ b/packages/daemon/src/sense-runtime.ts @@ -40,6 +40,7 @@ export type SenseRuntime = { name: string; db: DrizzleDB; compute: ComputeFn; + persistSignal: (payload: unknown) => void; }; function ensureMigrationsTable(sqlite: DatabaseSync): Result { @@ -131,7 +132,7 @@ export function runMigrations(sqlite: DatabaseSync, migrationsDir: string): Resu export function openSenseDb( dbPath: string, migrationsDir: string, -): Result<{ sqlite: DatabaseSync; db: DrizzleDB }> { +): Result<{ sqlite: DatabaseSync; db: DrizzleDB; persistSignal: (payload: unknown) => void }> { let sqlite: DatabaseSync; try { @@ -146,9 +147,25 @@ export function openSenseDb( const migResult = runMigrations(sqlite, migrationsDir); if (!migResult.ok) return migResult; + // Auto-create _signals table for signal persistence (all senses get this) + sqlite.exec( + `CREATE TABLE IF NOT EXISTS _signals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + payload TEXT NOT NULL, + timestamp INTEGER NOT NULL + )`, + ); + + const insertStmt = sqlite.prepare("INSERT INTO _signals (payload, timestamp) VALUES (?, ?)"); + + function persistSignal(payload: unknown): void { + const json = JSON.stringify(payload); + insertStmt.run(json, Date.now()); + } + // Drizzle infers a schema-specific DB type; senses are schema-agnostic at this layer. const db = drizzle({ client: sqlite }) as DrizzleDB; - return ok({ sqlite, db }); + return ok({ sqlite, db, persistSignal }); } /** diff --git a/packages/daemon/src/sense-worker.ts b/packages/daemon/src/sense-worker.ts index d726c83..4c296dd 100644 --- a/packages/daemon/src/sense-worker.ts +++ b/packages/daemon/src/sense-worker.ts @@ -91,7 +91,15 @@ async function initSense( } const { db } = dbResult.value; - return { db, runtime: { name: senseName, db, compute: computeResult.value } }; + return { + db, + runtime: { + name: senseName, + db, + compute: computeResult.value, + persistSignal: dbResult.value.persistSignal, + }, + }; } function buildPeers( @@ -178,6 +186,7 @@ async function runCompute( } clearGracePeriodTimer(senseName); if (result.value !== null) { + runtime.persistSignal(result.value); sendSignal(senseName, result.value); } } catch (e: unknown) { -- 2.43.0