From 6ed91c0bd1a22f0ed9c5e1547e875a65f83849aa Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Sat, 15 Aug 2026 15:46:57 -0600 Subject: [PATCH] fix(server): repair installs that skipped the default-thread-env-mode column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An earlier build shipped ProjectionThreadSessionLifecycle as migration 41. This branch later settled on 41 = ProjectionProjectsDefaultThreadEnvMode, 42 = ProjectionProjectFaviconPath, 43 = session lifecycle. The runner only applies ids above the highest one a database records, so those installs report 41 as done, run 42 and 43, and never run 41 — leaving projection_projects without default_thread_env_mode. That is not a cosmetic gap: every project query then fails with `no such column`, and the server dies during startup. Reproduced against a real ~/.pylon-code database, which logged "Migrations ran successfully ['42_…','43_…']" and then took the server down. Renumbering is what caused this, so the fix cannot be another renumber — those databases would still skip anything below their high water mark. Migration 44 sits above it and re-adds the column, guarded by a table_info check so a correctly migrated database is untouched. Verified end to end: the same database that crashed now applies 44 and starts clean with its 4 projects and 25 threads intact. Also corrects two lines in the install doc that read "installs beside Pylon" and "Pylon continues using its own .t3" — both meant T3 Code, from an over-eager rename. --- apps/server/src/persistence/Migrations.ts | 5 ++ ...RepairProjectsDefaultThreadEnvMode.test.ts | 56 +++++++++++++++++++ .../044_RepairProjectsDefaultThreadEnvMode.ts | 31 ++++++++++ docs/user/install.md | 4 +- 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.test.ts create mode 100644 apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.ts diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index 9c8b22bc3..44307a32c 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -55,6 +55,7 @@ import Migration0040 from "./Migrations/040_ProjectionThreadsPinOrderKey.ts"; import Migration0041 from "./Migrations/041_ProjectionProjectsDefaultThreadEnvMode.ts"; import Migration0042 from "./Migrations/042_ProjectionProjectFaviconPath.ts"; import Migration0043 from "./Migrations/043_ProjectionThreadSessionLifecycle.ts"; +import Migration0044 from "./Migrations/044_RepairProjectsDefaultThreadEnvMode.ts"; /** * Migration loader with all migrations defined inline. * @@ -119,6 +120,10 @@ export const migrationEntries = [ // (pingdotgg/t3code#5775). [42, "ProjectionProjectFaviconPath", Migration0042], [43, "ProjectionThreadSessionLifecycle", Migration0043], + // Session lifecycle also shipped as 41 in an earlier build. Those databases + // record 41 as their high water mark, so they skip 41 above and never gain + // `default_thread_env_mode`. This re-adds it from above the mark. + [44, "RepairProjectsDefaultThreadEnvMode", Migration0044], ] as const; export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const); diff --git a/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.test.ts b/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.test.ts new file mode 100644 index 000000000..f7b02fe44 --- /dev/null +++ b/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.test.ts @@ -0,0 +1,56 @@ +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { runMigrations } from "../Migrations.ts"; +import * as NodeSqliteClient from "../NodeSqliteClient.ts"; + +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); + +const columnNames = Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_projects) + `; + return columns.map((column) => column.name); +}); + +layer("044_RepairProjectsDefaultThreadEnvMode", (it) => { + it.effect("restores the column on a database that skipped 041", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + // Reproduce a build that shipped session lifecycle as 41: stop before + // 041, then claim 41 so the runner treats everything at or below it as + // done. 042 and 043 still run; 041 never does. + yield* runMigrations({ toMigrationInclusive: 40 }); + yield* sql` + INSERT INTO effect_sql_migrations (migration_id, name, created_at) + VALUES (41, 'ProjectionThreadSessionLifecycle', CURRENT_TIMESTAMP) + `; + yield* runMigrations({ toMigrationInclusive: 43 }); + + assert.isFalse( + (yield* columnNames).includes("default_thread_env_mode"), + "expected the skip this migration exists to repair", + ); + + yield* runMigrations({ toMigrationInclusive: 44 }); + + assert.isTrue((yield* columnNames).includes("default_thread_env_mode")); + }), + ); + + it.effect("leaves a correctly migrated database alone", () => + Effect.gen(function* () { + yield* runMigrations({ toMigrationInclusive: 43 }); + const before = yield* columnNames; + assert.isTrue(before.includes("default_thread_env_mode")); + + yield* runMigrations({ toMigrationInclusive: 44 }); + + assert.deepStrictEqual(yield* columnNames, before); + }), + ); +}); diff --git a/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.ts b/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.ts new file mode 100644 index 000000000..0b2841286 --- /dev/null +++ b/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.ts @@ -0,0 +1,31 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +/** + * Repairs installs that skipped migration 041. + * + * Some builds shipped `ProjectionThreadSessionLifecycle` as id 41 before this + * branch settled on 041 = `ProjectionProjectsDefaultThreadEnvMode`, 042 = + * `ProjectionProjectFaviconPath`, 043 = session lifecycle. The runner only + * applies ids above the highest one recorded, so a database written by such a + * build reports 41 as done, runs 042 and 043, and never runs 041 — leaving + * `projection_projects` without `default_thread_env_mode`. Every project query + * then fails with `no such column`, which takes the server down at startup. + * + * Renumbering after release is what caused this, so the fix cannot be another + * renumber: those databases would still skip whatever sits below their high + * water mark. A new migration above it is the only thing they will run. + */ +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_projects) + `; + + if (!columns.some((column) => column.name === "default_thread_env_mode")) { + yield* sql` + ALTER TABLE projection_projects + ADD COLUMN default_thread_env_mode TEXT + `; + } +}); diff --git a/docs/user/install.md b/docs/user/install.md index 3c5718af1..00251ec0c 100644 --- a/docs/user/install.md +++ b/docs/user/install.md @@ -21,10 +21,10 @@ This starts the Pylon server on your machine and opens the local web app. Use ### Pylon fork -Pylon's desktop build installs beside Pylon rather than replacing it. The apps use different +Pylon's desktop build installs beside T3 Code rather than replacing it. The apps use different bundle IDs, URL handlers, Electron profiles, runtime databases, and updater metadata. On macOS the local Pylon build installs as `Pylon (Alpha).app`; its default runtime data lives under -`~/.pylon-code`, while Pylon continues using its own `.t3` and Electron data. +`~/.pylon-code`, while T3 Code continues using its own `.t3` and Electron data. From the Pylon repository, build the local macOS installer with: