From c49560af0f94459015d3fa4e1efa23ad9b291955 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 14 Jul 2026 19:26:22 +0200 Subject: [PATCH] fix(cli): keep session database compatible with released clients (#12207) --- .changeset/share-session-database-safely.md | 5 + .../migration.sql | 3 +- .../snapshot.json | 2 +- .../snapshot.json | 2 +- .../snapshot.json | 2 +- .../snapshot.json | 2 +- .../snapshot.json | 2 +- .../snapshot.json | 4 +- .../migration.sql | 19 + .../snapshot.json | 2083 +++++++++++++++++ packages/core/src/database/migration.gen.ts | 1 + ...040000_session_message_projection_order.ts | 2 +- ...36_session-message-legacy-writer-compat.ts | 30 + packages/core/src/session.ts | 21 +- packages/core/src/session/history.ts | 15 +- packages/core/src/session/projector.ts | 23 +- packages/core/src/session/sql.ts | 2 +- packages/core/src/session/store.ts | 4 +- .../database-migration-compat.test.ts | 128 + 19 files changed, 2323 insertions(+), 27 deletions(-) create mode 100644 .changeset/share-session-database-safely.md create mode 100644 packages/core/migration/20260714141136_session-message-legacy-writer-compat/migration.sql create mode 100644 packages/core/migration/20260714141136_session-message-legacy-writer-compat/snapshot.json create mode 100644 packages/core/src/database/migration/20260714141136_session-message-legacy-writer-compat.ts create mode 100644 packages/core/test/kilocode/database-migration-compat.test.ts diff --git a/.changeset/share-session-database-safely.md b/.changeset/share-session-database-safely.md new file mode 100644 index 0000000000..2e61e02968 --- /dev/null +++ b/.changeset/share-session-database-safely.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Keep shared session databases writable by released Kilo clients after newer schema migrations run. diff --git a/packages/core/migration/20260603040000_session_message_projection_order/migration.sql b/packages/core/migration/20260603040000_session_message_projection_order/migration.sql index dbec67f277..e603be927b 100644 --- a/packages/core/migration/20260603040000_session_message_projection_order/migration.sql +++ b/packages/core/migration/20260603040000_session_message_projection_order/migration.sql @@ -1,5 +1,6 @@ DELETE FROM `session_message`;--> statement-breakpoint -ALTER TABLE `session_message` ADD `seq` integer NOT NULL;--> statement-breakpoint +-- kilocode_change +ALTER TABLE `session_message` ADD `seq` integer;--> statement-breakpoint DROP INDEX IF EXISTS `session_message_session_time_created_id_idx`;--> statement-breakpoint DROP INDEX IF EXISTS `session_message_session_type_time_created_id_idx`;--> statement-breakpoint CREATE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint diff --git a/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json b/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json index 35aac3f7b8..490f1a7182 100644 --- a/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json +++ b/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json @@ -756,7 +756,7 @@ }, { "type": "integer", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, diff --git a/packages/core/migration/20260603141458_session_input_inbox/snapshot.json b/packages/core/migration/20260603141458_session_input_inbox/snapshot.json index 7e51b1dfcf..9839a20820 100644 --- a/packages/core/migration/20260603141458_session_input_inbox/snapshot.json +++ b/packages/core/migration/20260603141458_session_input_inbox/snapshot.json @@ -830,7 +830,7 @@ }, { "type": "integer", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, diff --git a/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json b/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json index a2ec834e77..80c5ff8feb 100644 --- a/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json +++ b/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json @@ -874,7 +874,7 @@ }, { "type": "integer", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, diff --git a/packages/core/migration/20260604172448_event_sourced_session_input/snapshot.json b/packages/core/migration/20260604172448_event_sourced_session_input/snapshot.json index 4e916637ba..07d008d89e 100644 --- a/packages/core/migration/20260604172448_event_sourced_session_input/snapshot.json +++ b/packages/core/migration/20260604172448_event_sourced_session_input/snapshot.json @@ -874,7 +874,7 @@ }, { "type": "integer", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, diff --git a/packages/core/migration/20260605003541_add_session_context_snapshot/snapshot.json b/packages/core/migration/20260605003541_add_session_context_snapshot/snapshot.json index 6e1cce1361..9769539724 100644 --- a/packages/core/migration/20260605003541_add_session_context_snapshot/snapshot.json +++ b/packages/core/migration/20260605003541_add_session_context_snapshot/snapshot.json @@ -938,7 +938,7 @@ }, { "type": "integer", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, diff --git a/packages/core/migration/20260605042240_add_context_epoch_agent/snapshot.json b/packages/core/migration/20260605042240_add_context_epoch_agent/snapshot.json index ec49baca3b..5fe721e553 100644 --- a/packages/core/migration/20260605042240_add_context_epoch_agent/snapshot.json +++ b/packages/core/migration/20260605042240_add_context_epoch_agent/snapshot.json @@ -950,7 +950,7 @@ }, { "type": "integer", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, @@ -2080,4 +2080,4 @@ } ], "renames": [] -} \ No newline at end of file +} diff --git a/packages/core/migration/20260714141136_session-message-legacy-writer-compat/migration.sql b/packages/core/migration/20260714141136_session-message-legacy-writer-compat/migration.sql new file mode 100644 index 0000000000..62a665543a --- /dev/null +++ b/packages/core/migration/20260714141136_session-message-legacy-writer-compat/migration.sql @@ -0,0 +1,19 @@ +-- kilocode_change - new file +CREATE TABLE `__new_session_message` ( + `id` text PRIMARY KEY, + `session_id` text NOT NULL, + `type` text NOT NULL, + `seq` integer, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + `data` text NOT NULL, + CONSTRAINT `fk_session_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +INSERT INTO `__new_session_message`(`id`, `session_id`, `type`, `seq`, `time_created`, `time_updated`, `data`) SELECT `id`, `session_id`, `type`, `seq`, `time_created`, `time_updated`, `data` FROM `session_message`;--> statement-breakpoint +DROP TABLE `session_message`;--> statement-breakpoint +ALTER TABLE `__new_session_message` RENAME TO `session_message`;--> statement-breakpoint +CREATE UNIQUE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint +CREATE INDEX `session_message_session_type_seq_idx` ON `session_message` (`session_id`,`type`,`seq`);--> statement-breakpoint +CREATE INDEX `session_message_session_time_created_id_idx` ON `session_message` (`session_id`,`time_created`,`id`);--> statement-breakpoint +CREATE INDEX `session_message_time_created_idx` ON `session_message` (`time_created`); diff --git a/packages/core/migration/20260714141136_session-message-legacy-writer-compat/snapshot.json b/packages/core/migration/20260714141136_session-message-legacy-writer-compat/snapshot.json new file mode 100644 index 0000000000..6e404909a4 --- /dev/null +++ b/packages/core/migration/20260714141136_session-message-legacy-writer-compat/snapshot.json @@ -0,0 +1,2083 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "6c030252-8b68-4107-b18a-f64f99b76895", + "prevIds": [ + "d1bfa125-b81e-4c61-9b6e-e74abf6e488f" + ], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "project_directory", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_context_epoch", + "entityType": "tables" + }, + { + "name": "session_input", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'build'", + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "replacement_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "revision", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admitted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "promoted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_project_directory_project_id_project_id_fk", + "entityType": "fks", + "table": "project_directory" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_context_epoch_session_id_session_id_fk", + "entityType": "fks", + "table": "session_context_epoch" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_input_session_id_session_id_fk", + "entityType": "fks", + "table": "session_input" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "project_id", + "directory" + ], + "nameExplicit": false, + "name": "project_directory_pk", + "entityType": "pks", + "table": "project_directory" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_context_epoch_pk", + "table": "session_context_epoch", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_input_pk", + "table": "session_input", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_type_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + }, + { + "value": "delivery", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_input_session_pending_delivery_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_admitted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_promoted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_message_session_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_time_created_id_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index a7e9dd132e..1d6101500d 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -34,5 +34,6 @@ export const migrations = ( import("./migration/20260604172448_event_sourced_session_input"), import("./migration/20260605003541_add_session_context_snapshot"), import("./migration/20260605042240_add_context_epoch_agent"), + import("./migration/20260714141136_session-message-legacy-writer-compat"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts b/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts index 1f3a43bcce..fb4f5285cf 100644 --- a/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts +++ b/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts @@ -8,7 +8,7 @@ export default { // Pre-launch Session projections were written before durable event persistence // became unconditional, so they cannot be assigned truthful aggregate order. yield* tx.run(`DELETE FROM \`session_message\`;`) - yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer;`) // kilocode_change yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_time_created_id_idx\`;`) yield* tx.run(`CREATE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`) yield* tx.run( diff --git a/packages/core/src/database/migration/20260714141136_session-message-legacy-writer-compat.ts b/packages/core/src/database/migration/20260714141136_session-message-legacy-writer-compat.ts new file mode 100644 index 0000000000..7028d70c63 --- /dev/null +++ b/packages/core/src/database/migration/20260714141136_session-message-legacy-writer-compat.ts @@ -0,0 +1,30 @@ +// kilocode_change - new file +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260714141136_session-message-legacy-writer-compat", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`__new_session_message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`seq\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`INSERT INTO \`__new_session_message\`(\`id\`, \`session_id\`, \`type\`, \`seq\`, \`time_created\`, \`time_updated\`, \`data\`) SELECT \`id\`, \`session_id\`, \`type\`, \`seq\`, \`time_created\`, \`time_updated\`, \`data\` FROM \`session_message\`;`) + yield* tx.run(`DROP TABLE \`session_message\`;`) + yield* tx.run(`ALTER TABLE \`__new_session_message\` RENAME TO \`session_message\`;`) + yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index ebc213724c..42727f095d 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -2,7 +2,7 @@ export * as SessionV2 from "./session" export * from "./session/schema" import { Cause, Effect, Layer, Schema, Context, Stream } from "effect" -import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm" +import { and, asc, desc, eq, gt, isNotNull, like, lt, or, type SQL } from "drizzle-orm" // kilocode_change import { ProjectV2 } from "./project" import { WorkspaceV2 } from "./workspace" import { ModelV2 } from "./model" @@ -304,20 +304,25 @@ export const layer = Layer.effect( .select({ seq: SessionMessageTable.seq }) .from(SessionMessageTable) .where( - and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)), + and( + eq(SessionMessageTable.session_id, input.sessionID), + eq(SessionMessageTable.id, input.cursor.id), + isNotNull(SessionMessageTable.seq), // kilocode_change + ), ) .get() .pipe(Effect.orDie) : undefined - if (input.cursor && !anchor) return [] - const boundary = anchor + const seq = anchor?.seq + if (input.cursor && seq == null) return [] + const boundary = seq != null ? order === "asc" - ? gt(SessionMessageTable.seq, anchor.seq) - : lt(SessionMessageTable.seq, anchor.seq) + ? gt(SessionMessageTable.seq, seq) + : lt(SessionMessageTable.seq, seq) : undefined const where = boundary - ? and(eq(SessionMessageTable.session_id, input.sessionID), boundary) - : eq(SessionMessageTable.session_id, input.sessionID) + ? and(eq(SessionMessageTable.session_id, input.sessionID), isNotNull(SessionMessageTable.seq), boundary) + : and(eq(SessionMessageTable.session_id, input.sessionID), isNotNull(SessionMessageTable.seq)) // kilocode_change const query = db .select() .from(SessionMessageTable) diff --git a/packages/core/src/session/history.ts b/packages/core/src/session/history.ts index 66af533679..1f1d8d635d 100644 --- a/packages/core/src/session/history.ts +++ b/packages/core/src/session/history.ts @@ -1,4 +1,4 @@ -import { and, asc, desc, eq, gt, gte, ne, or } from "drizzle-orm" +import { and, asc, desc, eq, gt, gte, isNotNull, ne, or } from "drizzle-orm" // kilocode_change import { Effect, Schema } from "effect" import { Database } from "../database/database" import { MessageDecodeError } from "./error" @@ -11,14 +11,22 @@ type DatabaseService = Database.Interface["db"] const decode = Schema.decodeUnknownEffect(SessionMessage.Message) const latestCompaction = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) { - return yield* db + const row = yield* db .select({ seq: SessionMessageTable.seq }) .from(SessionMessageTable) - .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction"))) + .where( + and( + eq(SessionMessageTable.session_id, sessionID), + eq(SessionMessageTable.type, "compaction"), + isNotNull(SessionMessageTable.seq), // kilocode_change + ), + ) .orderBy(desc(SessionMessageTable.seq)) .limit(1) .get() .pipe(Effect.orDie) + if (!row || row.seq === null) return + return { seq: row.seq } }) const messageRows = Effect.fnUntraced(function* ( @@ -33,6 +41,7 @@ const messageRows = Effect.fnUntraced(function* ( .where( and( eq(SessionMessageTable.session_id, sessionID), + isNotNull(SessionMessageTable.seq), // kilocode_change compaction ? or( gte(SessionMessageTable.seq, compaction.seq), diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index 311fb988fa..cdce7dddc3 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -1,6 +1,6 @@ export * as SessionProjector from "./projector" -import { and, desc, eq, sql } from "drizzle-orm" +import { and, desc, eq, isNotNull, sql } from "drizzle-orm" // kilocode_change import { DateTime, Effect, Layer, Schema } from "effect" import { Database } from "../database/database" import { EventV2 } from "../event" @@ -138,7 +138,11 @@ function run(db: DatabaseService, event: SessionEvent.Event) { .select() .from(SessionMessageTable) .where( - and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "assistant")), + and( + eq(SessionMessageTable.session_id, event.data.sessionID), + eq(SessionMessageTable.type, "assistant"), + isNotNull(SessionMessageTable.seq), // kilocode_change + ), ) .orderBy(desc(SessionMessageTable.seq)) .limit(1) @@ -159,6 +163,7 @@ function run(db: DatabaseService, event: SessionEvent.Event) { eq(SessionMessageTable.id, messageID), eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "assistant"), + isNotNull(SessionMessageTable.seq), // kilocode_change ), ) .get() @@ -174,7 +179,11 @@ function run(db: DatabaseService, event: SessionEvent.Event) { .select() .from(SessionMessageTable) .where( - and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "compaction")), + and( + eq(SessionMessageTable.session_id, event.data.sessionID), + eq(SessionMessageTable.type, "compaction"), + isNotNull(SessionMessageTable.seq), // kilocode_change + ), ) .orderBy(desc(SessionMessageTable.seq)) .limit(1) @@ -190,7 +199,13 @@ function run(db: DatabaseService, event: SessionEvent.Event) { const rows = yield* db .select() .from(SessionMessageTable) - .where(and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "shell"))) + .where( + and( + eq(SessionMessageTable.session_id, event.data.sessionID), + eq(SessionMessageTable.type, "shell"), + isNotNull(SessionMessageTable.seq), // kilocode_change + ), + ) .orderBy(desc(SessionMessageTable.seq)) .all() .pipe(Effect.orDie) diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index ca3d8e1b53..e5ff558cc5 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -124,7 +124,7 @@ export const SessionMessageTable = sqliteTable( .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), type: text().$type().notNull(), - seq: integer().notNull(), + seq: integer(), // kilocode_change - allow released clients to share databases with newer schemas ...Timestamps, data: text({ mode: "json" }).notNull().$type(), }, diff --git a/packages/core/src/session/store.ts b/packages/core/src/session/store.ts index 87a05dc584..753b7debaf 100644 --- a/packages/core/src/session/store.ts +++ b/packages/core/src/session/store.ts @@ -1,6 +1,6 @@ export * as SessionStore from "./store" -import { eq } from "drizzle-orm" +import { and, eq, isNotNull } from "drizzle-orm" // kilocode_change import { Context, Effect, Layer, Schema } from "effect" import { Database } from "../database/database" import { SessionHistory } from "./history" @@ -45,7 +45,7 @@ export const layer = Layer.effect( const row = yield* db .select() .from(SessionMessageTable) - .where(eq(SessionMessageTable.id, messageID)) + .where(and(eq(SessionMessageTable.id, messageID), isNotNull(SessionMessageTable.seq))) // kilocode_change .get() .pipe(Effect.orDie) return row diff --git a/packages/core/test/kilocode/database-migration-compat.test.ts b/packages/core/test/kilocode/database-migration-compat.test.ts new file mode 100644 index 0000000000..a594464a30 --- /dev/null +++ b/packages/core/test/kilocode/database-migration-compat.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, test } from "bun:test" +import { SqliteClient } from "@effect/sql-sqlite-bun" +import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" +import { DatabaseMigration } from "@opencode-ai/core/database/migration" +import { migrations } from "@opencode-ai/core/database/migration.gen" +import legacyWriterMigration from "@opencode-ai/core/database/migration/20260714141136_session-message-legacy-writer-compat" +import { Effect } from "effect" +import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient" +import { sql } from "drizzle-orm" +import path from "path" +import { tmpdir } from "../fixture/tmpdir" + +const make = EffectDrizzleSqlite.makeWithDefaults() +const run = (effect: Effect.Effect) => + Effect.runPromise( + effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped), + ) + +describe("database migration compatibility", () => { + test("accepts released v7.4.7 session message writes after current migrations", async () => { + await run( + Effect.gen(function* () { + const db = yield* make + const split = migrations.findIndex((migration) => migration.id === "20260601010001_normalize_storage_paths") + expect(split).toBeGreaterThan(0) + yield* DatabaseMigration.applyOnly(db, migrations.slice(0, split)) + yield* db.run( + sql`INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('project', '/repo', 1, 1, '[]')`, + ) + yield* db.run( + sql`INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'project', 'session', '/repo', 'Session', '7.4.7', 1, 1)`, + ) + yield* db.run( + sql`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES ('legacy-message', 'session', 1, 1, '{}')`, + ) + yield* db.run( + sql`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES ('legacy-part', 'legacy-message', 'session', 1, 1, '{}')`, + ) + yield* db.run( + sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('legacy-projection', 'session', 'user', 1, 1, '{}')`, + ) + + yield* DatabaseMigration.applyOnly(db, migrations.slice(split)) + + // This is the projection shape written by the CLI bundled with VS Code v7.4.7. + yield* db.run( + sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('message', 'session', 'user', 1, 1, '{}')`, + ) + yield* db.run( + sql`UPDATE session_message SET data = '{"text":"updated"}' WHERE id = 'message'`, + ) + + expect(yield* db.get(sql`SELECT id, seq, data FROM session_message WHERE id = 'message'`)).toEqual({ + id: "message", + seq: null, + data: '{"text":"updated"}', + }) + expect(yield* db.get(sql`SELECT id FROM session WHERE id = 'session'`)).toEqual({ id: "session" }) + expect(yield* db.get(sql`SELECT id FROM message WHERE id = 'legacy-message'`)).toEqual({ id: "legacy-message" }) + expect(yield* db.get(sql`SELECT id FROM part WHERE id = 'legacy-part'`)).toEqual({ id: "legacy-part" }) + }), + ) + }) + + test("preserves sequenced projections when repairing an already-migrated database", async () => { + await run( + Effect.gen(function* () { + const db = yield* make + yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`) + yield* db.run( + sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, seq integer NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL, FOREIGN KEY (session_id) REFERENCES session(id) ON DELETE CASCADE)`, + ) + yield* db.run(sql`CREATE UNIQUE INDEX session_message_session_seq_idx ON session_message (session_id, seq)`) + yield* db.run(sql`INSERT INTO session (id) VALUES ('session')`) + yield* db.run( + sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('sequenced', 'session', 'user', 7, 1, 1, '{}')`, + ) + + yield* DatabaseMigration.applyOnly(db, [legacyWriterMigration]) + yield* db.run( + sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('legacy', 'session', 'user', 2, 2, '{}')`, + ) + + expect(yield* db.all(sql`SELECT id, seq FROM session_message ORDER BY id`)).toEqual([ + { id: "legacy", seq: null }, + { id: "sequenced", seq: 7 }, + ]) + }), + ) + }) + + test("repairs a WAL database while preserving foreign keys and sequence uniqueness", async () => { + await using tmp = await tmpdir() + const filename = path.join(tmp.path, "kilo.db") + await Effect.runPromise( + Effect.gen(function* () { + const db = yield* make + yield* db.run(sql`PRAGMA journal_mode = WAL`) + yield* db.run(sql`PRAGMA foreign_keys = ON`) + yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`) + yield* db.run( + sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, seq integer NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL, FOREIGN KEY (session_id) REFERENCES session(id) ON DELETE CASCADE)`, + ) + yield* db.run(sql`CREATE UNIQUE INDEX session_message_session_seq_idx ON session_message (session_id, seq)`) + yield* db.run(sql`INSERT INTO session (id) VALUES ('session')`) + yield* db.run( + sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('sequenced', 'session', 'user', 7, 1, 1, '{}')`, + ) + + yield* DatabaseMigration.applyOnly(db, [legacyWriterMigration]) + yield* db.run( + sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('legacy-1', 'session', 'user', 2, 2, '{}'), ('legacy-2', 'session', 'user', 3, 3, '{}')`, + ) + + expect(yield* db.all(sql`PRAGMA foreign_key_check`)).toEqual([]) + expect( + yield* Effect.exit( + db.run( + sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('duplicate', 'session', 'user', 7, 4, 4, '{}')`, + ), + ), + ).toMatchObject({ _tag: "Failure" }) + yield* db.run(sql`DELETE FROM session WHERE id = 'session'`) + expect(yield* db.all(sql`SELECT id FROM session_message`)).toEqual([]) + }).pipe(Effect.provide(SqliteClient.layer({ filename })), Effect.scoped), + ) + }) +})