From 46c6b7a59a1d334013ce35ab2fb2c1ea16739efa Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Tue, 5 May 2026 11:31:21 -0700 Subject: [PATCH 1/4] fix(insights): add ensureInsightRunsSchemaCompatibility for missing lifecycle column Databases already at schema v59+ when the lifecycle column was added never re-ran the v59 migration, causing "no column named lifecycle" errors on insight generation. Adds an unconditional compatibility check following the existing ensureRoutinesSchemaCompatibility pattern. Fixes #42 Co-Authored-By: Claude Opus 4.6 --- packages/core/src/db.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/core/src/db.ts b/packages/core/src/db.ts index 61dff7895..aa3f9d781 100644 --- a/packages/core/src/db.ts +++ b/packages/core/src/db.ts @@ -1003,6 +1003,7 @@ export class Database { // Compatibility backfills that must run even when schemaVersion is current. this.ensureRoutinesSchemaCompatibility(); + this.ensureInsightRunsSchemaCompatibility(); // Seed config row idempotently with default settings const configNow = new Date().toISOString(); @@ -1057,6 +1058,25 @@ export class Database { this.db.exec("CREATE INDEX IF NOT EXISTS idxRoutinesScope ON routines(scope)"); } + /** + * Applies idempotent compatibility fixes for the project_insight_runs table. + * + * The `lifecycle` and `cancelledAt` columns were added to SCHEMA_SQL and + * retroactively inserted into migration v33's CREATE TABLE, with a safety-net + * in migration v59. However, databases that were already at v59+ when the + * commit landed never re-run v59, leaving the columns missing. Running this + * unconditionally on every init guarantees the columns exist. + */ + private ensureInsightRunsSchemaCompatibility(): void { + if (!this.hasTable("project_insight_runs")) { + return; + } + + this.addColumnIfMissing("project_insight_runs", "lifecycle", "TEXT"); + this.addColumnIfMissing("project_insight_runs", "cancelledAt", "TEXT"); + this.db.exec(`CREATE INDEX IF NOT EXISTS idxInsightRunsProjectTriggerStatus ON project_insight_runs(projectId, trigger, status)`); + } + private migrate(): void { const version = this.getSchemaVersion() || 1; From b0c10849d2fe76d63953dc264a61c4d88a9212ab Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Tue, 5 May 2026 12:51:32 -0700 Subject: [PATCH 2/4] test(insights): add regression test for ensureInsightRunsSchemaCompatibility Simulates a database where project_insight_runs exists without lifecycle and cancelledAt columns (the state that caused the original bug), then verifies that re-opening the database adds the columns and creating a run succeeds. Co-Authored-By: Claude Opus 4.6 --- .../core/src/__tests__/insight-store.test.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/packages/core/src/__tests__/insight-store.test.ts b/packages/core/src/__tests__/insight-store.test.ts index b801176c4..aec09ec10 100644 --- a/packages/core/src/__tests__/insight-store.test.ts +++ b/packages/core/src/__tests__/insight-store.test.ts @@ -963,4 +963,56 @@ describe("Migration: pre-33 DB upgrade", () => { rmSync(testDir, { recursive: true, force: true }); } }); + + it("ensureInsightRunsSchemaCompatibility adds lifecycle column to legacy table", () => { + const compatDir = mkdtempSync(join(tmpdir(), "fn-insight-compat-")); + + try { + // Step 1: Create a fresh DB and run migrations + const db1 = createDatabase(compatDir); + db1.init(); + expect(db1.getSchemaVersion()).toBe(61); + + // Step 2: Strip lifecycle and cancelledAt columns by recreating the + // table without them. This simulates a DB that was created before the + // lifecycle columns were added and already past v59 when they landed. + db1.exec(` + CREATE TABLE project_insight_runs_legacy AS + SELECT id, projectId, trigger, status, summary, error, + insightsCreated, insightsUpdated, inputMetadata, outputMetadata, + createdAt, startedAt, completedAt + FROM project_insight_runs + `); + db1.exec("DROP TABLE project_insight_runs"); + db1.exec("ALTER TABLE project_insight_runs_legacy RENAME TO project_insight_runs"); + + // Verify lifecycle column is gone + const colsBefore = db1.prepare("PRAGMA table_info(project_insight_runs)").all() as Array<{ name: string }>; + const colNamesBefore = colsBefore.map((c) => c.name); + expect(colNamesBefore).not.toContain("lifecycle"); + expect(colNamesBefore).not.toContain("cancelledAt"); + db1.close(); + + // Step 3: Re-open — ensureInsightRunsSchemaCompatibility should add the + // missing columns unconditionally. + const db2 = createDatabase(compatDir); + db2.init(); + + const colsAfter = db2.prepare("PRAGMA table_info(project_insight_runs)").all() as Array<{ name: string }>; + const colNamesAfter = colsAfter.map((c) => c.name); + expect(colNamesAfter).toContain("lifecycle"); + expect(colNamesAfter).toContain("cancelledAt"); + + // Step 4: Creating a run must not throw — proves the INSERT path works + // with the restored columns. + const s = new InsightStore(db2); + const run = s.createRun("proj", { trigger: "manual" }); + expect(run.id).toBeTruthy(); + expect(run.lifecycle).toBeDefined(); + + db2.close(); + } finally { + rmSync(compatDir, { recursive: true, force: true }); + } + }); }); From 06cbb744ff2cf0d58738c4bf367dbe6bbd044d92 Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Tue, 5 May 2026 12:53:45 -0700 Subject: [PATCH 3/4] fix(lint): suppress no-explicit-any in chat.ts mock assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-existing lint error on main — the `any` cast in __setCreateFnAgent mirrors the eslint-disable already on the adjacent variable declarations. Co-Authored-By: Claude Opus 4.6 --- packages/dashboard/src/chat.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dashboard/src/chat.ts b/packages/dashboard/src/chat.ts index a6988c53f..c6a94c24f 100644 --- a/packages/dashboard/src/chat.ts +++ b/packages/dashboard/src/chat.ts @@ -1231,6 +1231,7 @@ export function __setCreateFnAgent(mock: typeof createFnAgent): void { // hit the real engine. Mirror the same fake into the resolved-session slot // so existing test setups that only call `__setCreateFnAgent` continue to // work. + // eslint-disable-next-line @typescript-eslint/no-explicit-any createResolvedAgentSession = (async (options: any) => mock(options)) as typeof createResolvedAgentSession; } From ca432feaea1a3980cb646a9622c7254143d50052 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 5 May 2026 14:31:53 -0700 Subject: [PATCH 4/4] Update packages/core/src/__tests__/insight-store.test.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- packages/core/src/__tests__/insight-store.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/__tests__/insight-store.test.ts b/packages/core/src/__tests__/insight-store.test.ts index c8f7b95b0..bda50e27b 100644 --- a/packages/core/src/__tests__/insight-store.test.ts +++ b/packages/core/src/__tests__/insight-store.test.ts @@ -971,7 +971,7 @@ describe("Migration: pre-33 DB upgrade", () => { // Step 1: Create a fresh DB and run migrations const db1 = createDatabase(compatDir); db1.init(); - expect(db1.getSchemaVersion()).toBe(61); + expect(db1.getSchemaVersion()).toBe(62); // Step 2: Strip lifecycle and cancelledAt columns by recreating the // table without them. This simulates a DB that was created before the