diff --git a/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts b/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts index a326e5367e68..fb98e757179a 100644 --- a/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts +++ b/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts @@ -174,6 +174,62 @@ projectionRepositoriesLayer("Projection repositories", (it) => { }), ); + it.effect("stores SQL NULL for thread fields omitted by pre-annotation events", () => + Effect.gen(function* () { + const threads = yield* ProjectionThreadRepository; + const sql = yield* SqlClient.SqlClient; + + yield* threads.upsert({ + threadId: ThreadId.make("thread-before-annotations"), + projectId: ProjectId.make("project-1"), + title: "Pre-annotation thread", + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", + }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurnId: null, + createdAt: "2026-03-24T00:00:00.000Z", + updatedAt: "2026-03-24T00:00:00.000Z", + archivedAt: null, + settledOverride: null, + settledAt: null, + snoozedUntil: null, + snoozedAt: null, + pinnedAt: null, + latestUserMessageAt: null, + pendingApprovalCount: 0, + pendingUserInputCount: 0, + hasActionableProposedPlan: 0, + deletedAt: null, + }); + + const rows = yield* sql<{ + readonly annotation: string | null; + readonly latestUserMessageId: string | null; + }>` + SELECT + annotation_json AS annotation, + latest_user_message_id AS "latestUserMessageId" + FROM projection_threads + WHERE thread_id = 'thread-before-annotations' + `; + assert.deepStrictEqual(rows[0], { + annotation: null, + latestUserMessageId: null, + }); + + const persisted = yield* threads.getById({ + threadId: ThreadId.make("thread-before-annotations"), + }); + assert.strictEqual(Option.getOrNull(persisted)?.annotation, null); + assert.strictEqual(Option.getOrNull(persisted)?.latestUserMessageId, null); + }), + ); + it.effect("round-trips non-null settlement values through the thread row", () => Effect.gen(function* () { const threads = yield* ProjectionThreadRepository; diff --git a/apps/server/src/persistence/Layers/ProjectionThreads.ts b/apps/server/src/persistence/Layers/ProjectionThreads.ts index 6058acdfc152..3fab53b7ec25 100644 --- a/apps/server/src/persistence/Layers/ProjectionThreads.ts +++ b/apps/server/src/persistence/Layers/ProjectionThreads.ts @@ -15,6 +15,7 @@ import { ListPendingWorktreeCleanupThreadsInput, ProjectionThread, ProjectionThreadRepository, + UpsertProjectionThreadInput, type ProjectionThreadRepositoryShape, } from "../Services/ProjectionThreads.ts"; import { ModelSelection, ThreadAnnotation, ThreadWorktreeCleanup } from "@t3tools/contracts"; @@ -32,7 +33,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const upsertProjectionThreadRow = SqlSchema.void({ - Request: ProjectionThread, + Request: UpsertProjectionThreadInput, execute: (row) => sql` INSERT INTO projection_threads ( @@ -86,9 +87,9 @@ const makeProjectionThreadRepository = Effect.gen(function* () { ${row.pinOrderKey ?? null}, ${row.titleRegenerationRequestId ?? null}, ${row.titleRegenerationStartedAt ?? null}, - ${row.annotation === null ? null : JSON.stringify(row.annotation)}, + ${row.annotation == null ? null : JSON.stringify(row.annotation)}, ${row.worktreeCleanup == null ? null : JSON.stringify(row.worktreeCleanup)}, - ${row.latestUserMessageId}, + ${row.latestUserMessageId ?? null}, ${row.latestUserMessageAt}, ${row.pendingApprovalCount}, ${row.pendingUserInputCount}, diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index 4cf2d211a2e0..0ca6c53bc87e 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -54,11 +54,13 @@ import Migration0038 from "./Migrations/038_ProjectionThreadsPinOrderKey.ts"; import Migration0039 from "./Migrations/039_ProjectionProjectsDefaultThreadEnvMode.ts"; import Migration0040 from "./Migrations/040_ProjectionProjectFaviconPath.ts"; import Migration0041 from "./Migrations/041_AuthSessionClientConnection.ts"; -import Migration0042 from "./Migrations/042_ProjectionThreadAnnotation.ts"; -import Migration0043 from "./Migrations/043_UpdateDrain.ts"; -import Migration0044 from "./Migrations/044_UpdateDrainClaim.ts"; -import Migration0045 from "./Migrations/045_ProjectionTurnRequestCorrelations.ts"; -import Migration0046 from "./Migrations/046_ProjectionThreadWorktreeCleanup.ts"; +import Migration0042 from "./Migrations/042_ProjectionThreadLinkedPullRequest.ts"; +import Migration0043 from "./Migrations/043_ProjectionThreadAnnotation.ts"; +import Migration0044 from "./Migrations/044_UpdateDrain.ts"; +import Migration0045 from "./Migrations/045_UpdateDrainClaim.ts"; +import Migration0046 from "./Migrations/046_ProjectionTurnRequestCorrelations.ts"; +import Migration0047 from "./Migrations/047_ProjectionThreadWorktreeCleanup.ts"; +import Migration0048 from "./Migrations/048_ProjectionThreadLinkedPullRequest.ts"; /** * Migration loader with all migrations defined inline. @@ -112,11 +114,13 @@ export const migrationEntries = [ [39, "ProjectionProjectsDefaultThreadEnvMode", Migration0039], [40, "ProjectionProjectFaviconPath", Migration0040], [41, "AuthSessionClientConnection", Migration0041], - [42, "ProjectionThreadAnnotation", Migration0042], - [43, "UpdateDrain", Migration0043], - [44, "UpdateDrainClaim", Migration0044], - [45, "ProjectionTurnRequestCorrelations", Migration0045], - [46, "ProjectionThreadWorktreeCleanup", Migration0046], + [42, "ProjectionThreadLinkedPullRequest", Migration0042], + [43, "ProjectionThreadAnnotation", Migration0043], + [44, "UpdateDrain", Migration0044], + [45, "UpdateDrainClaim", Migration0045], + [46, "ProjectionTurnRequestCorrelations", Migration0046], + [47, "ProjectionThreadWorktreeCleanup", Migration0047], + [48, "ProjectionThreadLinkedPullRequest", Migration0048], ] as const; export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const); diff --git a/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.test.ts b/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.test.ts new file mode 100644 index 000000000000..1fe59df50729 --- /dev/null +++ b/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.test.ts @@ -0,0 +1,25 @@ +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())); + +layer("042_ProjectionThreadLinkedPullRequest", (it) => { + it.effect("adds the linked pull request column", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* runMigrations({ toMigrationInclusive: 41 }); + yield* runMigrations({ toMigrationInclusive: 42 }); + + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_threads) + `; + assert.ok(columns.some((column) => column.name === "linked_pull_request_json")); + }), + ); +}); diff --git a/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.ts b/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.ts new file mode 100644 index 000000000000..a026f39c392a --- /dev/null +++ b/apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.ts @@ -0,0 +1,16 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_threads) + `; + + if (!columns.some((column) => column.name === "linked_pull_request_json")) { + yield* sql` + ALTER TABLE projection_threads + ADD COLUMN linked_pull_request_json TEXT + `; + } +}); diff --git a/apps/server/src/persistence/Migrations/042_ProjectionThreadAnnotation.test.ts b/apps/server/src/persistence/Migrations/043_ProjectionThreadAnnotation.test.ts similarity index 96% rename from apps/server/src/persistence/Migrations/042_ProjectionThreadAnnotation.test.ts rename to apps/server/src/persistence/Migrations/043_ProjectionThreadAnnotation.test.ts index 273784b6149b..98c3922d2451 100644 --- a/apps/server/src/persistence/Migrations/042_ProjectionThreadAnnotation.test.ts +++ b/apps/server/src/persistence/Migrations/043_ProjectionThreadAnnotation.test.ts @@ -8,12 +8,12 @@ import * as NodeSqliteClient from "../NodeSqliteClient.ts"; const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); -layer("042_ProjectionThreadAnnotation", (it) => { +layer("043_ProjectionThreadAnnotation", (it) => { it.effect("adds annotation and latest user marker fields to thread projections", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; - yield* runMigrations({ toMigrationInclusive: 40 }); + yield* runMigrations({ toMigrationInclusive: 42 }); yield* sql` INSERT INTO projection_threads ( @@ -67,7 +67,7 @@ layer("042_ProjectionThreadAnnotation", (it) => { '2026-02-24T00:01:00.000Z' ) `; - yield* runMigrations({ toMigrationInclusive: 42 }); + yield* runMigrations({ toMigrationInclusive: 43 }); const columns = yield* sql<{ readonly name: string; readonly notnull: number }>` PRAGMA table_info(projection_threads) diff --git a/apps/server/src/persistence/Migrations/042_ProjectionThreadAnnotation.ts b/apps/server/src/persistence/Migrations/043_ProjectionThreadAnnotation.ts similarity index 100% rename from apps/server/src/persistence/Migrations/042_ProjectionThreadAnnotation.ts rename to apps/server/src/persistence/Migrations/043_ProjectionThreadAnnotation.ts diff --git a/apps/server/src/persistence/Migrations/043_UpdateDrain.test.ts b/apps/server/src/persistence/Migrations/044_UpdateDrain.test.ts similarity index 93% rename from apps/server/src/persistence/Migrations/043_UpdateDrain.test.ts rename to apps/server/src/persistence/Migrations/044_UpdateDrain.test.ts index e7acd7708b57..eea6f6a58d7d 100644 --- a/apps/server/src/persistence/Migrations/043_UpdateDrain.test.ts +++ b/apps/server/src/persistence/Migrations/044_UpdateDrain.test.ts @@ -8,13 +8,13 @@ import * as NodeSqliteClient from "../NodeSqliteClient.ts"; const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); -layer("043_UpdateDrain", (it) => { +layer("044_UpdateDrain", (it) => { it.effect("creates a narrow event stream and durable command receipts", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; - yield* runMigrations({ toMigrationInclusive: 42 }); yield* runMigrations({ toMigrationInclusive: 43 }); + yield* runMigrations({ toMigrationInclusive: 44 }); const eventColumns = yield* sql<{ readonly name: string }>` PRAGMA table_info(update_drain_events) diff --git a/apps/server/src/persistence/Migrations/043_UpdateDrain.ts b/apps/server/src/persistence/Migrations/044_UpdateDrain.ts similarity index 100% rename from apps/server/src/persistence/Migrations/043_UpdateDrain.ts rename to apps/server/src/persistence/Migrations/044_UpdateDrain.ts diff --git a/apps/server/src/persistence/Migrations/044_UpdateDrainClaim.test.ts b/apps/server/src/persistence/Migrations/045_UpdateDrainClaim.test.ts similarity index 94% rename from apps/server/src/persistence/Migrations/044_UpdateDrainClaim.test.ts rename to apps/server/src/persistence/Migrations/045_UpdateDrainClaim.test.ts index 2d99b2fb5d62..59e8a98ca5cd 100644 --- a/apps/server/src/persistence/Migrations/044_UpdateDrainClaim.test.ts +++ b/apps/server/src/persistence/Migrations/045_UpdateDrainClaim.test.ts @@ -8,11 +8,11 @@ import * as NodeSqliteClient from "../NodeSqliteClient.ts"; const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); -layer("044_UpdateDrainClaim", (it) => { +layer("045_UpdateDrainClaim", (it) => { it.effect("preserves drain history and accepts one claimed transition", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; - yield* runMigrations({ toMigrationInclusive: 43 }); + yield* runMigrations({ toMigrationInclusive: 44 }); yield* sql` INSERT INTO update_drain_events ( event_id, event_type, command_id, occurred_at, request_id, target_version, status @@ -21,7 +21,7 @@ layer("044_UpdateDrainClaim", (it) => { '2026-08-21T00:00:00.000Z', 'request-1', '1.2.3', 'draining' ) `; - yield* runMigrations({ toMigrationInclusive: 44 }); + yield* runMigrations({ toMigrationInclusive: 45 }); yield* sql` INSERT INTO update_drain_events ( event_id, event_type, command_id, occurred_at, request_id, target_version, status diff --git a/apps/server/src/persistence/Migrations/044_UpdateDrainClaim.ts b/apps/server/src/persistence/Migrations/045_UpdateDrainClaim.ts similarity index 100% rename from apps/server/src/persistence/Migrations/044_UpdateDrainClaim.ts rename to apps/server/src/persistence/Migrations/045_UpdateDrainClaim.ts diff --git a/apps/server/src/persistence/Migrations/045_ProjectionTurnRequestCorrelations.test.ts b/apps/server/src/persistence/Migrations/046_ProjectionTurnRequestCorrelations.test.ts similarity index 98% rename from apps/server/src/persistence/Migrations/045_ProjectionTurnRequestCorrelations.test.ts rename to apps/server/src/persistence/Migrations/046_ProjectionTurnRequestCorrelations.test.ts index b0cf8ce241dd..14ab26d4a201 100644 --- a/apps/server/src/persistence/Migrations/045_ProjectionTurnRequestCorrelations.test.ts +++ b/apps/server/src/persistence/Migrations/046_ProjectionTurnRequestCorrelations.test.ts @@ -15,7 +15,7 @@ const layer = it.layer( ), ); -layer("045_ProjectionTurnRequestCorrelations", (it) => { +layer("046_ProjectionTurnRequestCorrelations", (it) => { it.effect("inserts once, resolves once, and deletes by owning thread", () => Effect.gen(function* () { yield* runMigrations(); diff --git a/apps/server/src/persistence/Migrations/045_ProjectionTurnRequestCorrelations.ts b/apps/server/src/persistence/Migrations/046_ProjectionTurnRequestCorrelations.ts similarity index 100% rename from apps/server/src/persistence/Migrations/045_ProjectionTurnRequestCorrelations.ts rename to apps/server/src/persistence/Migrations/046_ProjectionTurnRequestCorrelations.ts diff --git a/apps/server/src/persistence/Migrations/046_ProjectionThreadWorktreeCleanup.test.ts b/apps/server/src/persistence/Migrations/047_ProjectionThreadWorktreeCleanup.test.ts similarity index 93% rename from apps/server/src/persistence/Migrations/046_ProjectionThreadWorktreeCleanup.test.ts rename to apps/server/src/persistence/Migrations/047_ProjectionThreadWorktreeCleanup.test.ts index 765a9605684b..1f6a8913f158 100644 --- a/apps/server/src/persistence/Migrations/046_ProjectionThreadWorktreeCleanup.test.ts +++ b/apps/server/src/persistence/Migrations/047_ProjectionThreadWorktreeCleanup.test.ts @@ -8,11 +8,11 @@ import * as NodeSqliteClient from "../NodeSqliteClient.ts"; const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); -layer("046_ProjectionThreadWorktreeCleanup", (it) => { +layer("047_ProjectionThreadWorktreeCleanup", (it) => { it.effect("adds nullable cleanup state without changing existing rows", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; - yield* runMigrations({ toMigrationInclusive: 44 }); + yield* runMigrations({ toMigrationInclusive: 46 }); yield* sql` INSERT INTO projection_threads ( thread_id, @@ -36,7 +36,7 @@ layer("046_ProjectionThreadWorktreeCleanup", (it) => { ) `; - yield* runMigrations({ toMigrationInclusive: 46 }); + yield* runMigrations({ toMigrationInclusive: 47 }); const columns = yield* sql<{ readonly name: string; readonly notnull: number }>` PRAGMA table_info(projection_threads) diff --git a/apps/server/src/persistence/Migrations/046_ProjectionThreadWorktreeCleanup.ts b/apps/server/src/persistence/Migrations/047_ProjectionThreadWorktreeCleanup.ts similarity index 100% rename from apps/server/src/persistence/Migrations/046_ProjectionThreadWorktreeCleanup.ts rename to apps/server/src/persistence/Migrations/047_ProjectionThreadWorktreeCleanup.ts diff --git a/apps/server/src/persistence/Migrations/048_ProjectionThreadLinkedPullRequest.test.ts b/apps/server/src/persistence/Migrations/048_ProjectionThreadLinkedPullRequest.test.ts new file mode 100644 index 000000000000..effa2ecfd68c --- /dev/null +++ b/apps/server/src/persistence/Migrations/048_ProjectionThreadLinkedPullRequest.test.ts @@ -0,0 +1,123 @@ +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"; +import Migration0043 from "./043_ProjectionThreadAnnotation.ts"; +import Migration0044 from "./044_UpdateDrain.ts"; +import Migration0045 from "./045_UpdateDrainClaim.ts"; +import Migration0046 from "./046_ProjectionTurnRequestCorrelations.ts"; +import Migration0047 from "./047_ProjectionThreadWorktreeCleanup.ts"; +import Migration0048 from "./048_ProjectionThreadLinkedPullRequest.ts"; + +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); + +layer("048_ProjectionThreadLinkedPullRequest", (it) => { + it.effect("bridges databases that recorded the previous LastCode migration numbers", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* runMigrations({ toMigrationInclusive: 41 }); + yield* Migration0043; + yield* Migration0044; + yield* Migration0045; + yield* Migration0046; + yield* Migration0047; + yield* sql` + INSERT INTO effect_sql_migrations (migration_id, name) + VALUES + (42, 'ProjectionThreadAnnotation'), + (43, 'UpdateDrain'), + (44, 'UpdateDrainClaim'), + (45, 'ProjectionTurnRequestCorrelations'), + (46, 'ProjectionThreadWorktreeCleanup') + `; + + const before = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_threads) + `; + assert.isFalse(before.some((column) => column.name === "linked_pull_request_json")); + + const executed = yield* runMigrations({ toMigrationInclusive: 48 }); + assert.deepStrictEqual(executed, [ + [47, "ProjectionThreadWorktreeCleanup"], + [48, "ProjectionThreadLinkedPullRequest"], + ]); + + yield* Migration0048; + const after = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_threads) + `; + assert.equal(after.filter((column) => column.name === "linked_pull_request_json").length, 1); + }), + ); +}); + +const partialUpgradeLayer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); + +partialUpgradeLayer("048_ProjectionThreadLinkedPullRequest partial upgrades", (it) => { + it.effect("preserves update drain data when upgrading from the previous migration 44", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* runMigrations({ toMigrationInclusive: 41 }); + yield* Migration0043; + yield* Migration0044; + yield* Migration0045; + yield* sql` + INSERT INTO update_drain_events ( + event_id, event_type, command_id, occurred_at, request_id, target_version, status + ) VALUES ( + 'event-claimed', 'update-drain.claimed', 'command-event', + '2026-08-25T00:00:00.000Z', 'request-1', '1.2.3', 'claimed' + ) + `; + yield* sql` + INSERT INTO update_drain_command_receipts ( + command_id, command_type, request_id, target_version, accepted_at, + result_sequence, status, error_reason, error + ) VALUES ( + 'command-receipt', 'update-drain.claim', 'request-1', '1.2.3', + '2026-08-25T00:00:01.000Z', 1, 'accepted', NULL, NULL + ) + `; + yield* sql` + INSERT INTO effect_sql_migrations (migration_id, name) + VALUES + (42, 'ProjectionThreadAnnotation'), + (43, 'UpdateDrain'), + (44, 'UpdateDrainClaim') + `; + + const executed = yield* runMigrations({ toMigrationInclusive: 48 }); + assert.deepStrictEqual(executed, [ + [45, "UpdateDrainClaim"], + [46, "ProjectionTurnRequestCorrelations"], + [47, "ProjectionThreadWorktreeCleanup"], + [48, "ProjectionThreadLinkedPullRequest"], + ]); + + const events = yield* sql<{ + readonly eventType: string; + readonly status: string; + }>` + SELECT event_type AS "eventType", status + FROM update_drain_events + WHERE event_id = 'event-claimed' + `; + assert.deepStrictEqual(events, [{ eventType: "update-drain.claimed", status: "claimed" }]); + + const receipts = yield* sql<{ + readonly commandType: string; + readonly status: string; + }>` + SELECT command_type AS "commandType", status + FROM update_drain_command_receipts + WHERE command_id = 'command-receipt' + `; + assert.deepStrictEqual(receipts, [{ commandType: "update-drain.claim", status: "accepted" }]); + }), + ); +}); diff --git a/apps/server/src/persistence/Migrations/048_ProjectionThreadLinkedPullRequest.ts b/apps/server/src/persistence/Migrations/048_ProjectionThreadLinkedPullRequest.ts new file mode 100644 index 000000000000..a026f39c392a --- /dev/null +++ b/apps/server/src/persistence/Migrations/048_ProjectionThreadLinkedPullRequest.ts @@ -0,0 +1,16 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_threads) + `; + + if (!columns.some((column) => column.name === "linked_pull_request_json")) { + yield* sql` + ALTER TABLE projection_threads + ADD COLUMN linked_pull_request_json TEXT + `; + } +}); diff --git a/apps/server/src/persistence/Services/ProjectionThreads.ts b/apps/server/src/persistence/Services/ProjectionThreads.ts index 5d3d9eed2d0f..052bab9c1ef2 100644 --- a/apps/server/src/persistence/Services/ProjectionThreads.ts +++ b/apps/server/src/persistence/Services/ProjectionThreads.ts @@ -59,6 +59,13 @@ export const ProjectionThread = Schema.Struct({ }); export type ProjectionThread = typeof ProjectionThread.Type; +export const UpsertProjectionThreadInput = Schema.Struct({ + ...ProjectionThread.fields, + annotation: Schema.optional(Schema.NullOr(ThreadAnnotation)), + latestUserMessageId: Schema.optional(Schema.NullOr(MessageId)), +}); +export type UpsertProjectionThreadInput = typeof UpsertProjectionThreadInput.Type; + export const GetProjectionThreadInput = Schema.Struct({ threadId: ThreadId, }); @@ -90,7 +97,9 @@ export interface ProjectionThreadRepositoryShape { * * Upserts by `threadId`. */ - readonly upsert: (thread: ProjectionThread) => Effect.Effect; + readonly upsert: ( + thread: UpsertProjectionThreadInput, + ) => Effect.Effect; /** * Read a projected thread row by id. diff --git a/docs/lastcode/nightly-workflow.md b/docs/lastcode/nightly-workflow.md index 0b79847073e3..fd37820ac0e7 100644 --- a/docs/lastcode/nightly-workflow.md +++ b/docs/lastcode/nightly-workflow.md @@ -160,6 +160,15 @@ still stop for review. Automatic continuation also stops if Git rejects or signing key fails), preserving the recovery worktree for operator action instead of retrying in a loop. +Migration numbers are part of deployed database history, not just filenames. +When upstream claims a number already used by LastCode, reserve that number for +upstream in the source manifest, shift the LastCode migrations forward, and add +a new highest-numbered idempotent bridge for any upstream schema change that an +already-upgraded LastCode database would otherwise skip. The checkpoint smoke +gate runs the bridge regression and typechecks the server so broken migration +imports, incompatible projection fixtures, and invalid upgrade paths stop before +a checkpoint tag is published. + No later nightly is checkpointed after a failure, because each failure should be understood before the sequence continues. diff --git a/scripts/lastcode-checkpoint.test.ts b/scripts/lastcode-checkpoint.test.ts index 8b50f9c7d96d..c5d2c4f43616 100644 --- a/scripts/lastcode-checkpoint.test.ts +++ b/scripts/lastcode-checkpoint.test.ts @@ -86,10 +86,11 @@ it("removes the Electron host mode from checkpoint smoke subprocesses", () => { ); }); -it("typechecks shared client commands before publishing a checkpoint", () => { +it("typechecks checkpoint automation, shared clients, and the server before publishing", () => { assert.deepStrictEqual(checkpointSmokeTypecheckCommands(), [ ["run", "--filter", "@t3tools/scripts", "typecheck"], ["run", "--filter", "@t3tools/client-runtime", "typecheck"], + ["run", "--filter", "t3", "typecheck"], ]); }); diff --git a/scripts/lastcode-checkpoint.ts b/scripts/lastcode-checkpoint.ts index e8b920b21116..1bdda81c5158 100644 --- a/scripts/lastcode-checkpoint.ts +++ b/scripts/lastcode-checkpoint.ts @@ -116,6 +116,7 @@ export function checkpointSmokeTypecheckCommands(): ReadonlyArray