diff --git a/AGENTS.md b/AGENTS.md index c16f6c73..a1477ba6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,6 +141,7 @@ Threadlines is dense and flat. When building or changing any user-facing surface - Comments describe how a thing is used: concise notes above functions and classes, not line-by-line narration. When you change code, keep its comments in sync. - Be careful with destructive actions (deleting files, dropping data, rewriting history) that the developer did not explicitly request. - No continuously repainting animations; they peg the GPU on high-refresh displays. +- A migration that rewrites existing rows must be timed against a database with real history (hundreds of thousands of events) before it merges. In-memory tests cannot tell a two-second backfill from one that takes hours on a user's install. - If a rule here fights the task in front of you, say so loudly and get sign-off before breaking it. ## Reference Repos diff --git a/apps/server/src/persistence/Migrations/050_ProjectionTranscriptEventSequence.ts b/apps/server/src/persistence/Migrations/050_ProjectionTranscriptEventSequence.ts index abe0edfd..9d37eac2 100644 --- a/apps/server/src/persistence/Migrations/050_ProjectionTranscriptEventSequence.ts +++ b/apps/server/src/persistence/Migrations/050_ProjectionTranscriptEventSequence.ts @@ -9,25 +9,29 @@ const decodeActivity = Schema.decodeUnknownOption( Schema.fromJsonString(ThreadActivityAppendedPayload), ); -/** Order transcript entries by their first durable event, even after a clock correction. */ +/** + * Order transcript entries by their first durable event, even after a clock correction. + * + * Each backfill is an UPDATE ... FROM so SQLite materializes the first-event lookup once. + * A correlated subquery over the CTE re-scans the whole event log per projected row, + * which took hours on a multi-gigabyte database and stalled startup. + */ export default Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* sql`ALTER TABLE projection_thread_messages ADD COLUMN event_sequence INTEGER`; yield* sql` - WITH first_events AS ( + UPDATE projection_thread_messages + SET event_sequence = first_events.first_sequence + FROM ( SELECT stream_id, json_extract(payload_json, '$.messageId') AS item_id, MIN(sequence) AS first_sequence FROM orchestration_events WHERE event_type IN ('thread.message-sent', 'thread.follow-up-accepted') GROUP BY stream_id, json_extract(payload_json, '$.messageId') - ) - UPDATE projection_thread_messages - SET event_sequence = ( - SELECT first_sequence FROM first_events - WHERE stream_id = projection_thread_messages.thread_id - AND item_id = projection_thread_messages.message_id - ) + ) AS first_events + WHERE first_events.stream_id = projection_thread_messages.thread_id + AND first_events.item_id = projection_thread_messages.message_id `; yield* sql` CREATE INDEX idx_projection_thread_messages_event_order @@ -36,19 +40,17 @@ export default Effect.gen(function* () { yield* sql`ALTER TABLE projection_thread_proposed_plans ADD COLUMN event_sequence INTEGER`; yield* sql` - WITH first_events AS ( + UPDATE projection_thread_proposed_plans + SET event_sequence = first_events.first_sequence + FROM ( SELECT stream_id, json_extract(payload_json, '$.proposedPlan.id') AS item_id, MIN(sequence) AS first_sequence FROM orchestration_events WHERE event_type IN ('thread.proposed-plan-upserted') GROUP BY stream_id, json_extract(payload_json, '$.proposedPlan.id') - ) - UPDATE projection_thread_proposed_plans - SET event_sequence = ( - SELECT first_sequence FROM first_events - WHERE stream_id = projection_thread_proposed_plans.thread_id - AND item_id = projection_thread_proposed_plans.plan_id - ) + ) AS first_events + WHERE first_events.stream_id = projection_thread_proposed_plans.thread_id + AND first_events.item_id = projection_thread_proposed_plans.plan_id `; yield* sql` CREATE INDEX idx_projection_thread_proposed_plans_event_order @@ -57,19 +59,17 @@ export default Effect.gen(function* () { yield* sql`ALTER TABLE projection_thread_activities ADD COLUMN event_sequence INTEGER`; yield* sql` - WITH first_events AS ( + UPDATE projection_thread_activities + SET event_sequence = first_events.first_sequence + FROM ( SELECT stream_id, json_extract(payload_json, '$.activity.id') AS item_id, MIN(sequence) AS first_sequence FROM orchestration_events WHERE event_type IN ('thread.activity-appended') GROUP BY stream_id, json_extract(payload_json, '$.activity.id') - ) - UPDATE projection_thread_activities - SET event_sequence = ( - SELECT first_sequence FROM first_events - WHERE stream_id = projection_thread_activities.thread_id - AND item_id = projection_thread_activities.activity_id - ) + ) AS first_events + WHERE first_events.stream_id = projection_thread_activities.thread_id + AND first_events.item_id = projection_thread_activities.activity_id `; yield* sql` CREATE INDEX idx_projection_thread_activities_event_order