diff --git a/migrations/sqlite-drizzle/0011_strange_green_goblin.sql b/migrations/sqlite-drizzle/0011_strange_green_goblin.sql new file mode 100644 index 00000000000..0803e0161b2 --- /dev/null +++ b/migrations/sqlite-drizzle/0011_strange_green_goblin.sql @@ -0,0 +1,169 @@ +CREATE TABLE `agents_agents` ( + `id` text PRIMARY KEY NOT NULL, + `type` text NOT NULL, + `name` text NOT NULL, + `description` text, + `accessible_paths` text, + `instructions` text, + `model` text NOT NULL, + `plan_model` text, + `small_model` text, + `mcps` text, + `allowed_tools` text, + `configuration` text, + `sort_order` integer DEFAULT 0 NOT NULL, + `created_at` text NOT NULL, + `updated_at` text NOT NULL +); +--> statement-breakpoint +CREATE INDEX `agents_agents_name_idx` ON `agents_agents` (`name`);--> statement-breakpoint +CREATE INDEX `agents_agents_type_idx` ON `agents_agents` (`type`);--> statement-breakpoint +CREATE INDEX `agents_agents_created_at_idx` ON `agents_agents` (`created_at`);--> statement-breakpoint +CREATE INDEX `agents_agents_sort_order_idx` ON `agents_agents` (`sort_order`);--> statement-breakpoint +CREATE TABLE `agents_channel_task_subscriptions` ( + `channel_id` text NOT NULL, + `task_id` text NOT NULL, + PRIMARY KEY(`channel_id`, `task_id`), + FOREIGN KEY (`channel_id`) REFERENCES `agents_channels`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`task_id`) REFERENCES `agents_tasks`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `agents_channel_task_subscriptions_channel_id_idx` ON `agents_channel_task_subscriptions` (`channel_id`);--> statement-breakpoint +CREATE INDEX `agents_channel_task_subscriptions_task_id_idx` ON `agents_channel_task_subscriptions` (`task_id`);--> statement-breakpoint +CREATE TABLE `agents_channels` ( + `id` text PRIMARY KEY NOT NULL, + `type` text NOT NULL, + `name` text NOT NULL, + `agent_id` text, + `session_id` text, + `config` text NOT NULL, + `is_active` integer DEFAULT true NOT NULL, + `active_chat_ids` text DEFAULT '[]', + `permission_mode` text, + `created_at` integer, + `updated_at` integer, + FOREIGN KEY (`agent_id`) REFERENCES `agents_agents`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`session_id`) REFERENCES `agents_sessions`(`id`) ON UPDATE no action ON DELETE set null, + CONSTRAINT "agents_channels_type_check" CHECK("agents_channels"."type" IN ('telegram', 'feishu', 'qq', 'wechat', 'discord', 'slack')), + CONSTRAINT "agents_channels_permission_mode_check" CHECK("agents_channels"."permission_mode" IS NULL OR "agents_channels"."permission_mode" IN ('default', 'acceptEdits', 'bypassPermissions', 'plan')) +); +--> statement-breakpoint +CREATE INDEX `agents_channels_agent_id_idx` ON `agents_channels` (`agent_id`);--> statement-breakpoint +CREATE INDEX `agents_channels_type_idx` ON `agents_channels` (`type`);--> statement-breakpoint +CREATE INDEX `agents_channels_session_id_idx` ON `agents_channels` (`session_id`);--> statement-breakpoint +CREATE TABLE `agents_session_messages` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `session_id` text NOT NULL, + `role` text NOT NULL, + `content` text NOT NULL, + `agent_session_id` text DEFAULT '', + `metadata` text, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + FOREIGN KEY (`session_id`) REFERENCES `agents_sessions`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `agents_session_messages_session_id_idx` ON `agents_session_messages` (`session_id`);--> statement-breakpoint +CREATE INDEX `agents_session_messages_created_at_idx` ON `agents_session_messages` (`created_at`);--> statement-breakpoint +CREATE INDEX `agents_session_messages_updated_at_idx` ON `agents_session_messages` (`updated_at`);--> statement-breakpoint +CREATE TABLE `agents_sessions` ( + `id` text PRIMARY KEY NOT NULL, + `agent_type` text NOT NULL, + `agent_id` text NOT NULL, + `name` text NOT NULL, + `description` text, + `accessible_paths` text, + `instructions` text, + `model` text NOT NULL, + `plan_model` text, + `small_model` text, + `mcps` text, + `allowed_tools` text, + `slash_commands` text, + `configuration` text, + `sort_order` integer DEFAULT 0 NOT NULL, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + FOREIGN KEY (`agent_id`) REFERENCES `agents_agents`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `agents_sessions_created_at_idx` ON `agents_sessions` (`created_at`);--> statement-breakpoint +CREATE INDEX `agents_sessions_agent_id_idx` ON `agents_sessions` (`agent_id`);--> statement-breakpoint +CREATE INDEX `agents_sessions_model_idx` ON `agents_sessions` (`model`);--> statement-breakpoint +CREATE INDEX `agents_sessions_sort_order_idx` ON `agents_sessions` (`sort_order`);--> statement-breakpoint +CREATE TABLE `agents_skills` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `description` text, + `folder_name` text NOT NULL, + `source` text NOT NULL, + `source_url` text, + `namespace` text, + `author` text, + `tags` text, + `content_hash` text NOT NULL, + `is_enabled` integer DEFAULT true NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `agents_skills_folder_name_unique` ON `agents_skills` (`folder_name`);--> statement-breakpoint +CREATE INDEX `agents_skills_source_idx` ON `agents_skills` (`source`);--> statement-breakpoint +CREATE INDEX `agents_skills_is_enabled_idx` ON `agents_skills` (`is_enabled`);--> statement-breakpoint +CREATE TABLE `agents_task_run_logs` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `task_id` text NOT NULL, + `session_id` text, + `run_at` text NOT NULL, + `duration_ms` integer NOT NULL, + `status` text NOT NULL, + `result` text, + `error` text, + FOREIGN KEY (`task_id`) REFERENCES `agents_tasks`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `agents_task_run_logs_task_id_idx` ON `agents_task_run_logs` (`task_id`);--> statement-breakpoint +CREATE TABLE `agents_tasks` ( + `id` text PRIMARY KEY NOT NULL, + `agent_id` text NOT NULL, + `name` text NOT NULL, + `prompt` text NOT NULL, + `schedule_type` text NOT NULL, + `schedule_value` text NOT NULL, + `timeout_minutes` integer DEFAULT 2 NOT NULL, + `next_run` text, + `last_run` text, + `last_result` text, + `status` text DEFAULT 'active' NOT NULL, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + FOREIGN KEY (`agent_id`) REFERENCES `agents_agents`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `agents_tasks_agent_id_idx` ON `agents_tasks` (`agent_id`);--> statement-breakpoint +CREATE INDEX `agents_tasks_next_run_idx` ON `agents_tasks` (`next_run`);--> statement-breakpoint +CREATE INDEX `agents_tasks_status_idx` ON `agents_tasks` (`status`);--> statement-breakpoint +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_knowledge_item` ( + `id` text PRIMARY KEY NOT NULL, + `base_id` text NOT NULL, + `group_id` text, + `type` text NOT NULL, + `data` text NOT NULL, + `status` text DEFAULT 'idle' NOT NULL, + `error` text, + `created_at` integer, + `updated_at` integer, + FOREIGN KEY (`base_id`) REFERENCES `knowledge_base`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`base_id`,`group_id`) REFERENCES `knowledge_item`(`base_id`,`id`) ON UPDATE no action ON DELETE cascade, + CONSTRAINT "knowledge_item_type_check" CHECK("__new_knowledge_item"."type" IN ('file', 'url', 'note', 'sitemap', 'directory')), + CONSTRAINT "knowledge_item_status_check" CHECK("__new_knowledge_item"."status" IN ('idle', 'pending', 'file_processing', 'read', 'embed', 'completed', 'failed')) +); +--> statement-breakpoint +INSERT INTO `__new_knowledge_item`("id", "base_id", "group_id", "type", "data", "status", "error", "created_at", "updated_at") SELECT "id", "base_id", "group_id", "type", "data", "status", "error", "created_at", "updated_at" FROM `knowledge_item`;--> statement-breakpoint +DROP TABLE `knowledge_item`;--> statement-breakpoint +ALTER TABLE `__new_knowledge_item` RENAME TO `knowledge_item`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE INDEX `knowledge_item_base_type_created_idx` ON `knowledge_item` (`base_id`,`type`,`created_at`);--> statement-breakpoint +CREATE INDEX `knowledge_item_base_group_created_idx` ON `knowledge_item` (`base_id`,`group_id`,`created_at`);--> statement-breakpoint +CREATE UNIQUE INDEX `knowledge_item_baseId_id_unique` ON `knowledge_item` (`base_id`,`id`); \ No newline at end of file diff --git a/migrations/sqlite-drizzle/meta/0011_snapshot.json b/migrations/sqlite-drizzle/meta/0011_snapshot.json new file mode 100644 index 00000000000..ae775625d11 --- /dev/null +++ b/migrations/sqlite-drizzle/meta/0011_snapshot.json @@ -0,0 +1,2820 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d9e34d55-adfe-47ae-a583-2118d391ba21", + "prevId": "ceca2c61-e6f2-49d1-b9c7-e7c6c94cbff0", + "tables": { + "agents_agents": { + "name": "agents_agents", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessible_paths": { + "name": "accessible_paths", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_model": { + "name": "plan_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "small_model": { + "name": "small_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcps": { + "name": "mcps", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agents_agents_name_idx": { + "name": "agents_agents_name_idx", + "columns": ["name"], + "isUnique": false + }, + "agents_agents_type_idx": { + "name": "agents_agents_type_idx", + "columns": ["type"], + "isUnique": false + }, + "agents_agents_created_at_idx": { + "name": "agents_agents_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "agents_agents_sort_order_idx": { + "name": "agents_agents_sort_order_idx", + "columns": ["sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents_channel_task_subscriptions": { + "name": "agents_channel_task_subscriptions", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agents_channel_task_subscriptions_channel_id_idx": { + "name": "agents_channel_task_subscriptions_channel_id_idx", + "columns": ["channel_id"], + "isUnique": false + }, + "agents_channel_task_subscriptions_task_id_idx": { + "name": "agents_channel_task_subscriptions_task_id_idx", + "columns": ["task_id"], + "isUnique": false + } + }, + "foreignKeys": { + "agents_channel_task_subscriptions_channel_id_agents_channels_id_fk": { + "name": "agents_channel_task_subscriptions_channel_id_agents_channels_id_fk", + "tableFrom": "agents_channel_task_subscriptions", + "tableTo": "agents_channels", + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agents_channel_task_subscriptions_task_id_agents_tasks_id_fk": { + "name": "agents_channel_task_subscriptions_task_id_agents_tasks_id_fk", + "tableFrom": "agents_channel_task_subscriptions", + "tableTo": "agents_tasks", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agents_channel_task_subscriptions_channel_id_task_id_pk": { + "columns": ["channel_id", "task_id"], + "name": "agents_channel_task_subscriptions_channel_id_task_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents_channels": { + "name": "agents_channels", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "active_chat_ids": { + "name": "active_chat_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "permission_mode": { + "name": "permission_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agents_channels_agent_id_idx": { + "name": "agents_channels_agent_id_idx", + "columns": ["agent_id"], + "isUnique": false + }, + "agents_channels_type_idx": { + "name": "agents_channels_type_idx", + "columns": ["type"], + "isUnique": false + }, + "agents_channels_session_id_idx": { + "name": "agents_channels_session_id_idx", + "columns": ["session_id"], + "isUnique": false + } + }, + "foreignKeys": { + "agents_channels_agent_id_agents_agents_id_fk": { + "name": "agents_channels_agent_id_agents_agents_id_fk", + "tableFrom": "agents_channels", + "tableTo": "agents_agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_channels_session_id_agents_sessions_id_fk": { + "name": "agents_channels_session_id_agents_sessions_id_fk", + "tableFrom": "agents_channels", + "tableTo": "agents_sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agents_channels_type_check": { + "name": "agents_channels_type_check", + "value": "\"agents_channels\".\"type\" IN ('telegram', 'feishu', 'qq', 'wechat', 'discord', 'slack')" + }, + "agents_channels_permission_mode_check": { + "name": "agents_channels_permission_mode_check", + "value": "\"agents_channels\".\"permission_mode\" IS NULL OR \"agents_channels\".\"permission_mode\" IN ('default', 'acceptEdits', 'bypassPermissions', 'plan')" + } + } + }, + "agents_session_messages": { + "name": "agents_session_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_session_id": { + "name": "agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agents_session_messages_session_id_idx": { + "name": "agents_session_messages_session_id_idx", + "columns": ["session_id"], + "isUnique": false + }, + "agents_session_messages_created_at_idx": { + "name": "agents_session_messages_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "agents_session_messages_updated_at_idx": { + "name": "agents_session_messages_updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "agents_session_messages_session_id_fk": { + "name": "agents_session_messages_session_id_fk", + "tableFrom": "agents_session_messages", + "tableTo": "agents_sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents_sessions": { + "name": "agents_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessible_paths": { + "name": "accessible_paths", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_model": { + "name": "plan_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "small_model": { + "name": "small_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcps": { + "name": "mcps", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slash_commands": { + "name": "slash_commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agents_sessions_created_at_idx": { + "name": "agents_sessions_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "agents_sessions_agent_id_idx": { + "name": "agents_sessions_agent_id_idx", + "columns": ["agent_id"], + "isUnique": false + }, + "agents_sessions_model_idx": { + "name": "agents_sessions_model_idx", + "columns": ["model"], + "isUnique": false + }, + "agents_sessions_sort_order_idx": { + "name": "agents_sessions_sort_order_idx", + "columns": ["sort_order"], + "isUnique": false + } + }, + "foreignKeys": { + "agents_sessions_agent_id_fk": { + "name": "agents_sessions_agent_id_fk", + "tableFrom": "agents_sessions", + "tableTo": "agents_agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents_skills": { + "name": "agents_skills", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_name": { + "name": "folder_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agents_skills_folder_name_unique": { + "name": "agents_skills_folder_name_unique", + "columns": ["folder_name"], + "isUnique": true + }, + "agents_skills_source_idx": { + "name": "agents_skills_source_idx", + "columns": ["source"], + "isUnique": false + }, + "agents_skills_is_enabled_idx": { + "name": "agents_skills_is_enabled_idx", + "columns": ["is_enabled"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents_task_run_logs": { + "name": "agents_task_run_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "run_at": { + "name": "run_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agents_task_run_logs_task_id_idx": { + "name": "agents_task_run_logs_task_id_idx", + "columns": ["task_id"], + "isUnique": false + } + }, + "foreignKeys": { + "agents_task_run_logs_task_id_fk": { + "name": "agents_task_run_logs_task_id_fk", + "tableFrom": "agents_task_run_logs", + "tableTo": "agents_tasks", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents_tasks": { + "name": "agents_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_value": { + "name": "schedule_value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeout_minutes": { + "name": "timeout_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "next_run": { + "name": "next_run", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run": { + "name": "last_run", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agents_tasks_agent_id_idx": { + "name": "agents_tasks_agent_id_idx", + "columns": ["agent_id"], + "isUnique": false + }, + "agents_tasks_next_run_idx": { + "name": "agents_tasks_next_run_idx", + "columns": ["next_run"], + "isUnique": false + }, + "agents_tasks_status_idx": { + "name": "agents_tasks_status_idx", + "columns": ["status"], + "isUnique": false + } + }, + "foreignKeys": { + "agents_tasks_agent_id_fk": { + "name": "agents_tasks_agent_id_fk", + "tableFrom": "agents_tasks", + "tableTo": "agents_agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_state": { + "name": "app_state", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assistant": { + "name": "assistant", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "settings": { + "name": "settings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "assistant_created_at_idx": { + "name": "assistant_created_at_idx", + "columns": ["created_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assistant_knowledge_base": { + "name": "assistant_knowledge_base", + "columns": { + "assistant_id": { + "name": "assistant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "assistant_knowledge_base_assistant_id_assistant_id_fk": { + "name": "assistant_knowledge_base_assistant_id_assistant_id_fk", + "tableFrom": "assistant_knowledge_base", + "tableTo": "assistant", + "columnsFrom": ["assistant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assistant_knowledge_base_knowledge_base_id_knowledge_base_id_fk": { + "name": "assistant_knowledge_base_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "assistant_knowledge_base", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "assistant_knowledge_base_assistant_id_knowledge_base_id_pk": { + "columns": ["assistant_id", "knowledge_base_id"], + "name": "assistant_knowledge_base_assistant_id_knowledge_base_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assistant_mcp_server": { + "name": "assistant_mcp_server", + "columns": { + "assistant_id": { + "name": "assistant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "assistant_mcp_server_assistant_id_assistant_id_fk": { + "name": "assistant_mcp_server_assistant_id_assistant_id_fk", + "tableFrom": "assistant_mcp_server", + "tableTo": "assistant", + "columnsFrom": ["assistant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assistant_mcp_server_mcp_server_id_mcp_server_id_fk": { + "name": "assistant_mcp_server_mcp_server_id_mcp_server_id_fk", + "tableFrom": "assistant_mcp_server", + "tableTo": "mcp_server", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "assistant_mcp_server_assistant_id_mcp_server_id_pk": { + "columns": ["assistant_id", "mcp_server_id"], + "name": "assistant_mcp_server_assistant_id_mcp_server_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "group_entity_sort_idx": { + "name": "group_entity_sort_idx", + "columns": ["entity_type", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "knowledge_base": { + "name": "knowledge_base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions": { + "name": "dimensions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "embedding_model_id": { + "name": "embedding_model_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rerank_model_id": { + "name": "rerank_model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_processor_id": { + "name": "file_processor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chunk_size": { + "name": "chunk_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chunk_overlap": { + "name": "chunk_overlap", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "threshold": { + "name": "threshold", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "document_count": { + "name": "document_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "search_mode": { + "name": "search_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hybrid_alpha": { + "name": "hybrid_alpha", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "knowledge_base_search_mode_check": { + "name": "knowledge_base_search_mode_check", + "value": "\"knowledge_base\".\"search_mode\" IN ('default', 'bm25', 'hybrid') OR \"knowledge_base\".\"search_mode\" IS NULL" + } + } + }, + "knowledge_item": { + "name": "knowledge_item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "base_id": { + "name": "base_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "knowledge_item_base_type_created_idx": { + "name": "knowledge_item_base_type_created_idx", + "columns": ["base_id", "type", "created_at"], + "isUnique": false + }, + "knowledge_item_base_group_created_idx": { + "name": "knowledge_item_base_group_created_idx", + "columns": ["base_id", "group_id", "created_at"], + "isUnique": false + }, + "knowledge_item_baseId_id_unique": { + "name": "knowledge_item_baseId_id_unique", + "columns": ["base_id", "id"], + "isUnique": true + } + }, + "foreignKeys": { + "knowledge_item_base_id_knowledge_base_id_fk": { + "name": "knowledge_item_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_item", + "tableTo": "knowledge_base", + "columnsFrom": ["base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_item_base_id_group_id_knowledge_item_base_id_id_fk": { + "name": "knowledge_item_base_id_group_id_knowledge_item_base_id_id_fk", + "tableFrom": "knowledge_item", + "tableTo": "knowledge_item", + "columnsFrom": ["base_id", "group_id"], + "columnsTo": ["base_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "knowledge_item_type_check": { + "name": "knowledge_item_type_check", + "value": "\"knowledge_item\".\"type\" IN ('file', 'url', 'note', 'sitemap', 'directory')" + }, + "knowledge_item_status_check": { + "name": "knowledge_item_status_check", + "value": "\"knowledge_item\".\"status\" IN ('idle', 'pending', 'file_processing', 'read', 'embed', 'completed', 'failed')" + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registry_url": { + "name": "registry_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "args": { + "name": "args", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_url": { + "name": "provider_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "long_running": { + "name": "long_running", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dxt_version": { + "name": "dxt_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dxt_path": { + "name": "dxt_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "search_key": { + "name": "search_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_sample": { + "name": "config_sample", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disabled_tools": { + "name": "disabled_tools", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disabled_auto_approve_tools": { + "name": "disabled_auto_approve_tools", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "should_config": { + "name": "should_config", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "install_source": { + "name": "install_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_trusted": { + "name": "is_trusted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trusted_at": { + "name": "trusted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_name_idx": { + "name": "mcp_server_name_idx", + "columns": ["name"], + "isUnique": false + }, + "mcp_server_is_active_idx": { + "name": "mcp_server_is_active_idx", + "columns": ["is_active"], + "isUnique": false + }, + "mcp_server_sort_order_idx": { + "name": "mcp_server_sort_order_idx", + "columns": ["sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_type_check": { + "name": "mcp_server_type_check", + "value": "\"mcp_server\".\"type\" IS NULL OR \"mcp_server\".\"type\" IN ('stdio', 'sse', 'streamableHttp', 'inMemory')" + }, + "mcp_server_install_source_check": { + "name": "mcp_server_install_source_check", + "value": "\"mcp_server\".\"install_source\" IS NULL OR \"mcp_server\".\"install_source\" IN ('builtin', 'manual', 'protocol', 'unknown')" + } + } + }, + "message": { + "name": "message", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "searchable_text": { + "name": "searchable_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "siblings_group_id": { + "name": "siblings_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_snapshot": { + "name": "model_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats": { + "name": "stats", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "message_parent_id_idx": { + "name": "message_parent_id_idx", + "columns": ["parent_id"], + "isUnique": false + }, + "message_topic_created_idx": { + "name": "message_topic_created_idx", + "columns": ["topic_id", "created_at"], + "isUnique": false + }, + "message_trace_id_idx": { + "name": "message_trace_id_idx", + "columns": ["trace_id"], + "isUnique": false + } + }, + "foreignKeys": { + "message_topic_id_topic_id_fk": { + "name": "message_topic_id_topic_id_fk", + "tableFrom": "message", + "tableTo": "topic", + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_parent_id_message_id_fk": { + "name": "message_parent_id_message_id_fk", + "tableFrom": "message", + "tableTo": "message", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "message_role_check": { + "name": "message_role_check", + "value": "\"message\".\"role\" IN ('user', 'assistant', 'system')" + }, + "message_status_check": { + "name": "message_status_check", + "value": "\"message\".\"status\" IN ('pending', 'success', 'error', 'paused')" + } + } + }, + "miniapp": { + "name": "miniapp", + "columns": { + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'custom'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'enabled'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "bordered": { + "name": "bordered", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "background": { + "name": "background", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supported_regions": { + "name": "supported_regions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_key": { + "name": "name_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "miniapp_status_sort_idx": { + "name": "miniapp_status_sort_idx", + "columns": ["status", "sort_order"], + "isUnique": false + }, + "miniapp_type_idx": { + "name": "miniapp_type_idx", + "columns": ["type"], + "isUnique": false + }, + "miniapp_status_type_idx": { + "name": "miniapp_status_type_idx", + "columns": ["status", "type"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "miniapp_status_check": { + "name": "miniapp_status_check", + "value": "\"miniapp\".\"status\" IN ('enabled', 'disabled', 'pinned')" + }, + "miniapp_type_check": { + "name": "miniapp_type_check", + "value": "\"miniapp\".\"type\" IN ('default', 'custom')" + } + } + }, + "preference": { + "name": "preference", + "columns": { + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "preference_scope_key_pk": { + "columns": ["scope", "key"], + "name": "preference_scope_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entity_tag": { + "name": "entity_tag", + "columns": { + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "entity_tag_tag_id_idx": { + "name": "entity_tag_tag_id_idx", + "columns": ["tag_id"], + "isUnique": false + } + }, + "foreignKeys": { + "entity_tag_tag_id_tag_id_fk": { + "name": "entity_tag_tag_id_tag_id_fk", + "tableFrom": "entity_tag", + "tableTo": "tag", + "columnsFrom": ["tag_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "entity_tag_entity_type_entity_id_tag_id_pk": { + "columns": ["entity_type", "entity_id", "tag_id"], + "name": "entity_tag_entity_type_entity_id_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tag": { + "name": "tag", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "tag_name_unique": { + "name": "tag_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "topic": { + "name": "topic", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_name_manually_edited": { + "name": "is_name_manually_edited", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "assistant_id": { + "name": "assistant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_node_id": { + "name": "active_node_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "is_pinned": { + "name": "is_pinned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "pinned_order": { + "name": "pinned_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "topic_group_updated_idx": { + "name": "topic_group_updated_idx", + "columns": ["group_id", "updated_at"], + "isUnique": false + }, + "topic_group_sort_idx": { + "name": "topic_group_sort_idx", + "columns": ["group_id", "sort_order"], + "isUnique": false + }, + "topic_updated_at_idx": { + "name": "topic_updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + }, + "topic_is_pinned_idx": { + "name": "topic_is_pinned_idx", + "columns": ["is_pinned", "pinned_order"], + "isUnique": false + }, + "topic_assistant_id_idx": { + "name": "topic_assistant_id_idx", + "columns": ["assistant_id"], + "isUnique": false + } + }, + "foreignKeys": { + "topic_assistant_id_assistant_id_fk": { + "name": "topic_assistant_id_assistant_id_fk", + "tableFrom": "topic", + "tableTo": "assistant", + "columnsFrom": ["assistant_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "topic_group_id_group_id_fk": { + "name": "topic_group_id_group_id_fk", + "tableFrom": "topic", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "translate_history": { + "name": "translate_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_text": { + "name": "source_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_text": { + "name": "target_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_language": { + "name": "source_language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_language": { + "name": "target_language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "star": { + "name": "star", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "translate_history_created_at_idx": { + "name": "translate_history_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "translate_history_star_created_at_idx": { + "name": "translate_history_star_created_at_idx", + "columns": ["star", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "translate_history_source_language_translate_language_lang_code_fk": { + "name": "translate_history_source_language_translate_language_lang_code_fk", + "tableFrom": "translate_history", + "tableTo": "translate_language", + "columnsFrom": ["source_language"], + "columnsTo": ["lang_code"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "translate_history_target_language_translate_language_lang_code_fk": { + "name": "translate_history_target_language_translate_language_lang_code_fk", + "tableFrom": "translate_history", + "tableTo": "translate_language", + "columnsFrom": ["target_language"], + "columnsTo": ["lang_code"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "translate_language": { + "name": "translate_language", + "columns": { + "lang_code": { + "name": "lang_code", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_model": { + "name": "user_model", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "preset_model_id": { + "name": "preset_model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group": { + "name": "group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_modalities": { + "name": "output_modalities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "endpoint_types": { + "name": "endpoint_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_endpoint_url": { + "name": "custom_endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supports_streaming": { + "name": "supports_streaming", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parameters": { + "name": "parameters", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing": { + "name": "pricing", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "is_hidden": { + "name": "is_hidden", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "is_deprecated": { + "name": "is_deprecated", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_overrides": { + "name": "user_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_model_preset_idx": { + "name": "user_model_preset_idx", + "columns": ["preset_model_id"], + "isUnique": false + }, + "user_model_provider_enabled_idx": { + "name": "user_model_provider_enabled_idx", + "columns": ["provider_id", "is_enabled"], + "isUnique": false + }, + "user_model_provider_sort_idx": { + "name": "user_model_provider_sort_idx", + "columns": ["provider_id", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_model_provider_id_model_id_pk": { + "columns": ["provider_id", "model_id"], + "name": "user_model_provider_id_model_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_provider": { + "name": "user_provider", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "preset_provider_id": { + "name": "preset_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint_configs": { + "name": "endpoint_configs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_chat_endpoint": { + "name": "default_chat_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_keys": { + "name": "api_keys", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "auth_config": { + "name": "auth_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_features": { + "name": "api_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_settings": { + "name": "provider_settings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_provider_preset_idx": { + "name": "user_provider_preset_idx", + "columns": ["preset_provider_id"], + "isUnique": false + }, + "user_provider_enabled_sort_idx": { + "name": "user_provider_enabled_sort_idx", + "columns": ["is_enabled", "sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/migrations/sqlite-drizzle/meta/_journal.json b/migrations/sqlite-drizzle/meta/_journal.json index 61df9f255be..93afcbf05be 100644 --- a/migrations/sqlite-drizzle/meta/_journal.json +++ b/migrations/sqlite-drizzle/meta/_journal.json @@ -77,6 +77,13 @@ "when": 1775820970055, "tag": "0010_pink_shaman", "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1776062004458, + "tag": "0011_strange_green_goblin", + "breakpoints": true } ], "version": "7" diff --git a/src/main/core/paths/pathRegistry.ts b/src/main/core/paths/pathRegistry.ts index 51e28f85bf5..9293a1db46a 100644 --- a/src/main/core/paths/pathRegistry.ts +++ b/src/main/core/paths/pathRegistry.ts @@ -110,6 +110,7 @@ export function buildPathRegistry() { 'feature.agents.skills.builtin': path.join(appRootResources, 'skills'), // bundled skill templates (read-only) 'feature.agents.skills': path.join(appUserDataData, 'Skills'), // installed skills storage 'feature.agents.skills.install.temp': path.join(appTemp, 'skill-install'), + 'feature.agents.db_file': path.join(appUserDataData, 'agents.db'), // legacy standalone agents SQLite used by v2 migration 'feature.agents.claude.root': path.join(appUserData, '.claude'), // Claude Code config (relocated from ~/.claude for Windows compat) 'feature.agents.claude.skills': path.join(appUserData, '.claude', 'skills'), // symlinks → feature.agents.skills 'feature.agents.channels': path.join(appUserDataData, 'Channels'), diff --git a/src/main/data/db/schemas/agentsAgents.ts b/src/main/data/db/schemas/agentsAgents.ts new file mode 100644 index 00000000000..b32f8635d60 --- /dev/null +++ b/src/main/data/db/schemas/agentsAgents.ts @@ -0,0 +1,31 @@ +import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const agentsAgentsTable = sqliteTable( + 'agents_agents', + { + id: text().primaryKey(), + type: text().notNull(), + name: text().notNull(), + description: text(), + accessible_paths: text(), + instructions: text(), + model: text().notNull(), + plan_model: text(), + small_model: text(), + mcps: text(), + allowed_tools: text(), + configuration: text(), + sort_order: integer().notNull().default(0), + created_at: text().notNull(), + updated_at: text().notNull() + }, + (t) => [ + index('agents_agents_name_idx').on(t.name), + index('agents_agents_type_idx').on(t.type), + index('agents_agents_created_at_idx').on(t.created_at), + index('agents_agents_sort_order_idx').on(t.sort_order) + ] +) + +export type AgentsAgentRow = typeof agentsAgentsTable.$inferSelect +export type InsertAgentsAgentRow = typeof agentsAgentsTable.$inferInsert diff --git a/src/main/data/db/schemas/agentsChannels.ts b/src/main/data/db/schemas/agentsChannels.ts new file mode 100644 index 00000000000..0e1a6efd8e0 --- /dev/null +++ b/src/main/data/db/schemas/agentsChannels.ts @@ -0,0 +1,55 @@ +import { sql } from 'drizzle-orm' +import { check, index, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +import { agentsAgentsTable } from './agentsAgents' +import { agentsSessionsTable } from './agentsSessions' +import { agentsTasksTable } from './agentsTasks' + +export const agentsChannelsTable = sqliteTable( + 'agents_channels', + { + id: text().primaryKey(), + type: text().notNull(), + name: text().notNull(), + agent_id: text().references(() => agentsAgentsTable.id, { onDelete: 'set null' }), + session_id: text().references(() => agentsSessionsTable.id, { onDelete: 'set null' }), + config: text({ mode: 'json' }).$type>().notNull(), + is_active: integer({ mode: 'boolean' }).notNull().default(true), + active_chat_ids: text({ mode: 'json' }).$type().default([]), + permission_mode: text(), + created_at: integer(), + updated_at: integer() + }, + (t) => [ + index('agents_channels_agent_id_idx').on(t.agent_id), + index('agents_channels_type_idx').on(t.type), + index('agents_channels_session_id_idx').on(t.session_id), + check('agents_channels_type_check', sql`${t.type} IN ('telegram', 'feishu', 'qq', 'wechat', 'discord', 'slack')`), + check( + 'agents_channels_permission_mode_check', + sql`${t.permission_mode} IS NULL OR ${t.permission_mode} IN ('default', 'acceptEdits', 'bypassPermissions', 'plan')` + ) + ] +) + +export const agentsChannelTaskSubscriptionsTable = sqliteTable( + 'agents_channel_task_subscriptions', + { + channel_id: text() + .notNull() + .references(() => agentsChannelsTable.id, { onDelete: 'cascade' }), + task_id: text() + .notNull() + .references(() => agentsTasksTable.id, { onDelete: 'cascade' }) + }, + (t) => [ + primaryKey({ columns: [t.channel_id, t.task_id] }), + index('agents_channel_task_subscriptions_channel_id_idx').on(t.channel_id), + index('agents_channel_task_subscriptions_task_id_idx').on(t.task_id) + ] +) + +export type AgentsChannelRow = typeof agentsChannelsTable.$inferSelect +export type InsertAgentsChannelRow = typeof agentsChannelsTable.$inferInsert +export type AgentsChannelTaskSubscriptionRow = typeof agentsChannelTaskSubscriptionsTable.$inferSelect +export type InsertAgentsChannelTaskSubscriptionRow = typeof agentsChannelTaskSubscriptionsTable.$inferInsert diff --git a/src/main/data/db/schemas/agentsSessionMessages.ts b/src/main/data/db/schemas/agentsSessionMessages.ts new file mode 100644 index 00000000000..a6cb58505ba --- /dev/null +++ b/src/main/data/db/schemas/agentsSessionMessages.ts @@ -0,0 +1,30 @@ +import { foreignKey, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +import { agentsSessionsTable } from './agentsSessions' + +export const agentsSessionMessagesTable = sqliteTable( + 'agents_session_messages', + { + id: integer().primaryKey({ autoIncrement: true }), + session_id: text().notNull(), + role: text().notNull(), + content: text().notNull(), + agent_session_id: text().default(''), + metadata: text(), + created_at: text().notNull(), + updated_at: text().notNull() + }, + (t) => [ + foreignKey({ + columns: [t.session_id], + foreignColumns: [agentsSessionsTable.id], + name: 'agents_session_messages_session_id_fk' + }).onDelete('cascade'), + index('agents_session_messages_session_id_idx').on(t.session_id), + index('agents_session_messages_created_at_idx').on(t.created_at), + index('agents_session_messages_updated_at_idx').on(t.updated_at) + ] +) + +export type AgentsSessionMessageRow = typeof agentsSessionMessagesTable.$inferSelect +export type InsertAgentsSessionMessageRow = typeof agentsSessionMessagesTable.$inferInsert diff --git a/src/main/data/db/schemas/agentsSessions.ts b/src/main/data/db/schemas/agentsSessions.ts new file mode 100644 index 00000000000..eee32d089a7 --- /dev/null +++ b/src/main/data/db/schemas/agentsSessions.ts @@ -0,0 +1,40 @@ +import { foreignKey, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +import { agentsAgentsTable } from './agentsAgents' + +export const agentsSessionsTable = sqliteTable( + 'agents_sessions', + { + id: text().primaryKey(), + agent_type: text().notNull(), + agent_id: text().notNull(), + name: text().notNull(), + description: text(), + accessible_paths: text(), + instructions: text(), + model: text().notNull(), + plan_model: text(), + small_model: text(), + mcps: text(), + allowed_tools: text(), + slash_commands: text(), + configuration: text(), + sort_order: integer().notNull().default(0), + created_at: text().notNull(), + updated_at: text().notNull() + }, + (t) => [ + foreignKey({ + columns: [t.agent_id], + foreignColumns: [agentsAgentsTable.id], + name: 'agents_sessions_agent_id_fk' + }).onDelete('cascade'), + index('agents_sessions_created_at_idx').on(t.created_at), + index('agents_sessions_agent_id_idx').on(t.agent_id), + index('agents_sessions_model_idx').on(t.model), + index('agents_sessions_sort_order_idx').on(t.sort_order) + ] +) + +export type AgentsSessionRow = typeof agentsSessionsTable.$inferSelect +export type InsertAgentsSessionRow = typeof agentsSessionsTable.$inferInsert diff --git a/src/main/data/db/schemas/agentsSkills.ts b/src/main/data/db/schemas/agentsSkills.ts new file mode 100644 index 00000000000..70959c07cba --- /dev/null +++ b/src/main/data/db/schemas/agentsSkills.ts @@ -0,0 +1,32 @@ +import { randomUUID } from 'node:crypto' + +import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core' + +export const agentsSkillsTable = sqliteTable( + 'agents_skills', + { + id: text() + .primaryKey() + .$defaultFn(() => randomUUID()), + name: text().notNull(), + description: text(), + folder_name: text().notNull(), + source: text().notNull(), + source_url: text(), + namespace: text(), + author: text(), + tags: text(), + content_hash: text().notNull(), + is_enabled: integer({ mode: 'boolean' }).notNull().default(true), + created_at: integer().notNull(), + updated_at: integer().notNull() + }, + (t) => [ + uniqueIndex('agents_skills_folder_name_unique').on(t.folder_name), + index('agents_skills_source_idx').on(t.source), + index('agents_skills_is_enabled_idx').on(t.is_enabled) + ] +) + +export type AgentsSkillRow = typeof agentsSkillsTable.$inferSelect +export type InsertAgentsSkillRow = typeof agentsSkillsTable.$inferInsert diff --git a/src/main/data/db/schemas/agentsTasks.ts b/src/main/data/db/schemas/agentsTasks.ts new file mode 100644 index 00000000000..ffd37882505 --- /dev/null +++ b/src/main/data/db/schemas/agentsTasks.ts @@ -0,0 +1,59 @@ +import { foreignKey, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +import { agentsAgentsTable } from './agentsAgents' + +export const agentsTasksTable = sqliteTable( + 'agents_tasks', + { + id: text().primaryKey(), + agent_id: text().notNull(), + name: text().notNull(), + prompt: text().notNull(), + schedule_type: text().notNull(), + schedule_value: text().notNull(), + timeout_minutes: integer().notNull().default(2), + next_run: text(), + last_run: text(), + last_result: text(), + status: text().notNull().default('active'), + created_at: text().notNull(), + updated_at: text().notNull() + }, + (t) => [ + foreignKey({ + columns: [t.agent_id], + foreignColumns: [agentsAgentsTable.id], + name: 'agents_tasks_agent_id_fk' + }).onDelete('cascade'), + index('agents_tasks_agent_id_idx').on(t.agent_id), + index('agents_tasks_next_run_idx').on(t.next_run), + index('agents_tasks_status_idx').on(t.status) + ] +) + +export const agentsTaskRunLogsTable = sqliteTable( + 'agents_task_run_logs', + { + id: integer().primaryKey({ autoIncrement: true }), + task_id: text().notNull(), + session_id: text(), + run_at: text().notNull(), + duration_ms: integer().notNull(), + status: text().notNull(), + result: text(), + error: text() + }, + (t) => [ + foreignKey({ + columns: [t.task_id], + foreignColumns: [agentsTasksTable.id], + name: 'agents_task_run_logs_task_id_fk' + }).onDelete('cascade'), + index('agents_task_run_logs_task_id_idx').on(t.task_id) + ] +) + +export type AgentsTaskRow = typeof agentsTasksTable.$inferSelect +export type InsertAgentsTaskRow = typeof agentsTasksTable.$inferInsert +export type AgentsTaskRunLogRow = typeof agentsTaskRunLogsTable.$inferSelect +export type InsertAgentsTaskRunLogRow = typeof agentsTaskRunLogsTable.$inferInsert diff --git a/src/main/data/migration/v2/core/MigrationEngine.ts b/src/main/data/migration/v2/core/MigrationEngine.ts index 2792492cc85..0e6cca91d4b 100644 --- a/src/main/data/migration/v2/core/MigrationEngine.ts +++ b/src/main/data/migration/v2/core/MigrationEngine.ts @@ -3,6 +3,12 @@ * Coordinates migrators, manages progress, and handles failures */ +import { agentsAgentsTable } from '@data/db/schemas/agentsAgents' +import { agentsChannelsTable, agentsChannelTaskSubscriptionsTable } from '@data/db/schemas/agentsChannels' +import { agentsSessionMessagesTable } from '@data/db/schemas/agentsSessionMessages' +import { agentsSessionsTable } from '@data/db/schemas/agentsSessions' +import { agentsSkillsTable } from '@data/db/schemas/agentsSkills' +import { agentsTaskRunLogsTable, agentsTasksTable } from '@data/db/schemas/agentsTasks' import { appStateTable } from '@data/db/schemas/appState' import { assistantTable } from '@data/db/schemas/assistant' import { assistantKnowledgeBaseTable, assistantMcpServerTable } from '@data/db/schemas/assistantRelations' @@ -33,6 +39,7 @@ import fs from 'fs/promises' import path from 'path' import type { BaseMigrator, ProgressMessage } from '../migrators/BaseMigrator' +import { LegacyAgentsDbReader } from '../utils/LegacyAgentsDbReader' import { createMigrationContext } from './MigrationContext' import { MigrationDbService } from './MigrationDbService' import type { MigrationPaths } from './MigrationPaths' @@ -44,6 +51,9 @@ import type { MigrationPaths } from './MigrationPaths' const logger = loggerService.withContext('MigrationEngine') const MIGRATION_V2_STATUS = 'migration_v2_status' +const MIGRATION_V2_AGENTS_STATUS = 'migration_v2_agents_status' +const MIGRATION_V2_TARGET_VERSION = '2.0.0' +const MIGRATION_V2_AGENTS_TARGET_VERSION = '2.1.0-agents' export class MigrationEngine { private migrators: BaseMigrator[] = [] @@ -104,18 +114,9 @@ export class MigrationEngine { */ //TODO 不能仅仅判断数据库,如果是全新安装,而不是升级上来的用户,其实并不需要迁移,但是按现在的逻辑,还是会进行迁移,这不正确 async needsMigration(): Promise { - const db = this.getDb() - const status = await db.select().from(appStateTable).where(eq(appStateTable.key, MIGRATION_V2_STATUS)).get() - - if (status?.value) { - const statusValue = status.value as MigrationStatusValue - return statusValue.status !== 'completed' - } + const plan = await this.getPendingMigrationPlan() - // No migration status record — check if this is a fresh install or an upgrade. - if (!this.hasLegacyData()) { - logger.info('Fresh install detected (no legacy data found), skipping migration') - await this.markCompleted() + if (!plan.fullMigrationNeeded && !plan.agentsMigrationNeeded) { return false } @@ -123,37 +124,77 @@ export class MigrationEngine { } /** - * Heuristic fallback for fresh-install detection. - * Version-based upgrade path validation is enforced in - * v2MigrationGate.ts (via versionPolicy.ts) BEFORE this method - * is called. This heuristic only needs to distinguish "fresh - * install" from "upgrade with legacy data". - * - * Known limitation: electron-store (config.json) may be written - * by v2, causing false positives. Prefer to over-trigger (empty- - * data migration completes safely) rather than miss a real upgrade. + * FIXME: 当前仅通过 electron-store 判断 core v2 是否有旧数据,这是临时方案。 + * electron-store (config.json) 在 v2 中也可能被写入,导致误判。 + * localStorage 和 IndexedDB 的文件系统路径不可靠(UserData 路径问题待迁移后期统一处理),暂不检测。 + * 宁可误触发迁移(空数据迁移可安全完成),也不漏掉真正的升级用户。 + * 后续引入 version history 后可用精确的版本记录替代这些启发式检测。 */ - private hasLegacyData(): boolean { + private hasCoreLegacyData(): boolean { const legacyStore = new Store({ cwd: this.paths.userData }) - const hasData = legacyStore.size > 0 + return legacyStore.size > 0 + } + + private hasLegacyAgentsData(): boolean { + return new LegacyAgentsDbReader().resolvePath() !== null + } + + private async getStatus(key: string): Promise { + const db = this.getDb() + const record = await db.select().from(appStateTable).where(eq(appStateTable.key, key)).get() + return (record?.value as MigrationStatusValue | undefined) ?? null + } + + private async getPendingMigrationPlan(): Promise<{ + fullMigrationNeeded: boolean + agentsMigrationNeeded: boolean + migrators: BaseMigrator[] + }> { + const fullStatus = await this.getStatus(MIGRATION_V2_STATUS) + const agentsStatus = await this.getStatus(MIGRATION_V2_AGENTS_STATUS) + const hasCoreLegacyData = this.hasCoreLegacyData() + const hasLegacyAgentsData = this.hasLegacyAgentsData() + + let fullMigrationNeeded = false + if (fullStatus) { + fullMigrationNeeded = fullStatus.status !== 'completed' + } else if (hasCoreLegacyData) { + fullMigrationNeeded = true + } else { + logger.info('Fresh install detected for core v2 migration, marking completed') + await this.markCompleted() + } + + let agentsMigrationNeeded = false + if (hasLegacyAgentsData) { + agentsMigrationNeeded = agentsStatus?.status !== 'completed' + } else if (!agentsStatus || agentsStatus.status !== 'completed') { + await this.markAgentsCompleted() + } + + const migrators = fullMigrationNeeded ? this.migrators : this.migrators.filter((m) => m.id === 'agents') - logger.info('Legacy data detection', { hasElectronStore: hasData }) - return hasData + return { + fullMigrationNeeded, + agentsMigrationNeeded, + migrators: agentsMigrationNeeded || fullMigrationNeeded ? migrators : [] + } } /** * Get last migration error (for UI display) */ async getLastError(): Promise { - const db = this.getDb() - const status = await db.select().from(appStateTable).where(eq(appStateTable.key, MIGRATION_V2_STATUS)).get() + const fullStatus = await this.getStatus(MIGRATION_V2_STATUS) + if (fullStatus?.status === 'failed') { + return fullStatus.error || 'Unknown error' + } - if (status?.value) { - const statusValue = status.value as MigrationStatusValue - if (statusValue.status === 'failed') { - return statusValue.error || 'Unknown error' - } + const agentsStatus = await this.getStatus(MIGRATION_V2_AGENTS_STATUS) + if (agentsStatus?.status === 'failed') { + return agentsStatus.error || 'Unknown error' } + return null } @@ -170,13 +211,19 @@ export class MigrationEngine { const startTime = Date.now() const results: MigratorResult[] = [] + const plan = await this.getPendingMigrationPlan() + const activeMigrators = plan.migrators + try { - for (const migrator of this.migrators) { + for (const migrator of activeMigrators) { migrator.reset() } - // Safety check: verify new tables status before clearing - await this.verifyAndClearNewTables() + if (plan.fullMigrationNeeded) { + await this.verifyAndClearNewTables() + } else if (plan.agentsMigrationNeeded) { + await this.verifyAndClearAgentsTables() + } // Create migration context const context = await createMigrationContext( @@ -187,18 +234,30 @@ export class MigrationEngine { localStorageExportPath ) - for (let i = 0; i < this.migrators.length; i++) { - const migrator = this.migrators[i] + for (let i = 0; i < activeMigrators.length; i++) { + const migrator = activeMigrators[i] const migratorStartTime = Date.now() logger.info(`Starting migrator: ${migrator.name}`, { id: migrator.id }) // Update progress: migrator starting - this.updateProgress('migration', this.calculateProgress(i, 0), migrator) + this.updateProgress( + 'migration', + this.calculateProgress(i, 0, activeMigrators.length), + migrator, + undefined, + activeMigrators + ) // Set up migrator progress callback migrator.setProgressCallback((progress, progressMessage) => { - this.updateProgress('migration', this.calculateProgress(i, progress), migrator, progressMessage) + this.updateProgress( + 'migration', + this.calculateProgress(i, progress, activeMigrators.length), + migrator, + progressMessage, + activeMigrators + ) }) // Phase 1: Prepare (includes dry-run validation) @@ -237,14 +296,30 @@ export class MigrationEngine { }) // Update progress: migrator completed - this.updateProgress('migration', this.calculateProgress(i + 1, 0), migrator) + if (migrator.id === 'agents') { + await this.markAgentsCompleted() + } + + this.updateProgress( + 'migration', + this.calculateProgress(i + 1, 0, activeMigrators.length), + migrator, + undefined, + activeMigrators + ) } // Verify FK integrity after all inserts (FK was off during bulk inserts) await this.verifyForeignKeys() // Mark migration completed - await this.markCompleted() + if (plan.fullMigrationNeeded) { + await this.markCompleted() + } + + if (plan.agentsMigrationNeeded && !activeMigrators.some((m) => m.id === 'agents')) { + await this.markAgentsCompleted() + } // Cleanup temporary files await this.cleanupTempFiles(dexieExportPath) @@ -269,7 +344,12 @@ export class MigrationEngine { logger.error('Migration failed', { error: errorMessage }) // Mark migration as failed with error details - await this.markFailed(errorMessage) + if (plan.fullMigrationNeeded) { + await this.markFailed(errorMessage) + } + if (plan.agentsMigrationNeeded) { + await this.markAgentsFailed(errorMessage) + } return { success: false, @@ -290,6 +370,14 @@ export class MigrationEngine { // Tables to clear - add more as they are created // Order matters: child tables must be cleared before parent tables const tables = [ + { table: agentsSessionMessagesTable, name: 'agents_session_messages' }, + { table: agentsChannelTaskSubscriptionsTable, name: 'agents_channel_task_subscriptions' }, + { table: agentsTaskRunLogsTable, name: 'agents_task_run_logs' }, + { table: agentsChannelsTable, name: 'agents_channels' }, + { table: agentsTasksTable, name: 'agents_tasks' }, + { table: agentsSessionsTable, name: 'agents_sessions' }, + { table: agentsSkillsTable, name: 'agents_skills' }, + { table: agentsAgentsTable, name: 'agents_agents' }, { table: userModelTable, name: 'user_model' }, // Must clear before user_provider { table: userProviderTable, name: 'user_provider' }, { table: messageTable, name: 'message' }, // Must clear before topic (FK reference) @@ -317,6 +405,14 @@ export class MigrationEngine { } // Clear tables in dependency order (children before parents) + await db.delete(agentsSessionMessagesTable) + await db.delete(agentsChannelTaskSubscriptionsTable) + await db.delete(agentsTaskRunLogsTable) + await db.delete(agentsChannelsTable) + await db.delete(agentsTasksTable) + await db.delete(agentsSessionsTable) + await db.delete(agentsSkillsTable) + await db.delete(agentsAgentsTable) await db.delete(userModelTable) await db.delete(userProviderTable) await db.delete(messageTable) // FK → topic @@ -335,6 +431,44 @@ export class MigrationEngine { logger.info('All new architecture tables cleared successfully') } + /** + * Verify and clear only agents-import target tables. + * Used when the core v2 migration is already complete and only legacy agents.db + * still needs to be imported into the main database. + */ + private async verifyAndClearAgentsTables(): Promise { + const db = this.getDb() + const tables = [ + { table: agentsSessionMessagesTable, name: 'agents_session_messages' }, + { table: agentsChannelTaskSubscriptionsTable, name: 'agents_channel_task_subscriptions' }, + { table: agentsTaskRunLogsTable, name: 'agents_task_run_logs' }, + { table: agentsChannelsTable, name: 'agents_channels' }, + { table: agentsTasksTable, name: 'agents_tasks' }, + { table: agentsSessionsTable, name: 'agents_sessions' }, + { table: agentsSkillsTable, name: 'agents_skills' }, + { table: agentsAgentsTable, name: 'agents_agents' } + ] + + for (const { table, name } of tables) { + const result = await db.select({ count: sql`count(*)` }).from(table).get() + const count = result?.count ?? 0 + if (count > 0) { + logger.warn(`Table '${name}' is not empty (${count} rows), clearing for agents-only migration`) + } + } + + await db.delete(agentsSessionMessagesTable) + await db.delete(agentsChannelTaskSubscriptionsTable) + await db.delete(agentsTaskRunLogsTable) + await db.delete(agentsChannelsTable) + await db.delete(agentsTasksTable) + await db.delete(agentsSessionsTable) + await db.delete(agentsSkillsTable) + await db.delete(agentsAgentsTable) + + logger.info('Agents import target tables cleared successfully') + } + /** * Verify foreign key integrity after all data has been inserted. * FK constraints were disabled during bulk inserts for performance; @@ -405,9 +539,13 @@ export class MigrationEngine { /** * Calculate overall progress based on completed migrators and current migrator progress */ - private calculateProgress(completedMigrators: number, currentMigratorProgress: number): number { - if (this.migrators.length === 0) return 0 - const migratorWeight = 100 / this.migrators.length + private calculateProgress( + completedMigrators: number, + currentMigratorProgress: number, + totalMigrators: number + ): number { + if (totalMigrators === 0) return 0 + const migratorWeight = 100 / totalMigrators return Math.round(completedMigrators * migratorWeight + (currentMigratorProgress / 100) * migratorWeight) } @@ -418,9 +556,10 @@ export class MigrationEngine { stage: MigrationStage, overallProgress: number, currentMigrator: BaseMigrator, - progressMessage?: ProgressMessage + progressMessage?: ProgressMessage, + migrators: BaseMigrator[] = this.migrators ): void { - const migratorsProgress = this.migrators.map((m) => ({ + const migratorsProgress = migrators.map((m) => ({ id: m.id, name: m.name, status: this.getMigratorStatus(m, currentMigrator) @@ -454,6 +593,7 @@ export class MigrationEngine { async skipMigration(): Promise { logger.info('Migration skipped by user (version incompatible, using defaults)') await this.markCompleted() + await this.markAgentsCompleted() } /** @@ -464,7 +604,7 @@ export class MigrationEngine { const statusValue: MigrationStatusValue = { status: 'completed', completedAt: Date.now(), - version: '2.0.0', + version: MIGRATION_V2_TARGET_VERSION, error: null } @@ -491,7 +631,7 @@ export class MigrationEngine { const statusValue: MigrationStatusValue = { status: 'failed', failedAt: Date.now(), - version: '2.0.0', + version: MIGRATION_V2_TARGET_VERSION, error: error } @@ -509,6 +649,54 @@ export class MigrationEngine { } }) } + + private async markAgentsCompleted(): Promise { + const db = this.getDb() + const statusValue: MigrationStatusValue = { + status: 'completed', + completedAt: Date.now(), + version: MIGRATION_V2_AGENTS_TARGET_VERSION, + error: null + } + + await db + .insert(appStateTable) + .values({ + key: MIGRATION_V2_AGENTS_STATUS, + value: statusValue + }) + .onConflictDoUpdate({ + target: appStateTable.key, + set: { + value: statusValue, + updatedAt: Date.now() + } + }) + } + + private async markAgentsFailed(error: string): Promise { + const db = this.getDb() + const statusValue: MigrationStatusValue = { + status: 'failed', + failedAt: Date.now(), + version: MIGRATION_V2_AGENTS_TARGET_VERSION, + error + } + + await db + .insert(appStateTable) + .values({ + key: MIGRATION_V2_AGENTS_STATUS, + value: statusValue + }) + .onConflictDoUpdate({ + target: appStateTable.key, + set: { + value: statusValue, + updatedAt: Date.now() + } + }) + } } // Export singleton instance diff --git a/src/main/data/migration/v2/core/__tests__/MigrationEngine.test.ts b/src/main/data/migration/v2/core/__tests__/MigrationEngine.test.ts index e0db3469764..6cdee30e560 100644 --- a/src/main/data/migration/v2/core/__tests__/MigrationEngine.test.ts +++ b/src/main/data/migration/v2/core/__tests__/MigrationEngine.test.ts @@ -1,3 +1,4 @@ +import type { MigrationStatusValue } from '@shared/data/migration/v2/types' import { beforeEach, describe, expect, it, vi } from 'vitest' import { MigrationEngine } from '../MigrationEngine' @@ -62,15 +63,40 @@ describe('MigrationEngine', () => { vi.spyOn(engine as any, 'verifyForeignKeys').mockResolvedValue(undefined) vi.spyOn(engine as any, 'markCompleted').mockResolvedValue(undefined) vi.spyOn(engine as any, 'markFailed').mockResolvedValue(undefined) + vi.spyOn(engine as any, 'markAgentsCompleted').mockResolvedValue(undefined) + vi.spyOn(engine as any, 'markAgentsFailed').mockResolvedValue(undefined) vi.spyOn(engine as any, 'cleanupTempFiles').mockResolvedValue(undefined) }) + function mockStatuses(fullStatus?: MigrationStatusValue, agentsStatus?: MigrationStatusValue) { + const get = vi.fn<() => Promise<{ value: MigrationStatusValue } | undefined>>().mockResolvedValue(undefined) + get.mockResolvedValueOnce(fullStatus ? { value: fullStatus } : undefined) + get.mockResolvedValueOnce(agentsStatus ? { value: agentsStatus } : undefined) + + ;(engine as any).migrationDb = { + getDb: vi.fn(() => ({ + select: vi.fn(() => ({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ get })) + })) + })) + })), + close: vi.fn() + } + return { get } + } + it('resets every migrator before each run starts', async () => { const events: string[] = [] const boot = createTestMigrator('boot', 1, events) const chat = createTestMigrator('chat', 2, events) engine.registerMigrators([chat as any, boot as any]) + vi.spyOn(engine as any, 'getPendingMigrationPlan').mockResolvedValue({ + fullMigrationNeeded: true, + agentsMigrationNeeded: false, + migrators: [boot, chat] + }) await engine.run({}, '/tmp/dexie_export', '/tmp/localstorage_export/export.json') await engine.run({}, '/tmp/dexie_export', '/tmp/localstorage_export/export.json') @@ -96,4 +122,60 @@ describe('MigrationEngine', () => { 'chat:validate' ]) }) + + it('requires migration again when core v2 is complete but agents import is still pending', async () => { + mockStatuses({ status: 'completed', version: '2.0.0', completedAt: Date.now(), error: null }, undefined) + vi.spyOn(engine as any, 'hasCoreLegacyData').mockReturnValue(false) + vi.spyOn(engine as any, 'hasLegacyAgentsData').mockReturnValue(true) + + await expect(engine.needsMigration()).resolves.toBe(true) + }) + + it('marks agents import as completed when no legacy agents db exists anymore', async () => { + mockStatuses({ status: 'completed', version: '2.0.0', completedAt: Date.now(), error: null }, undefined) + vi.spyOn(engine as any, 'hasCoreLegacyData').mockReturnValue(false) + vi.spyOn(engine as any, 'hasLegacyAgentsData').mockReturnValue(false) + + await expect(engine.needsMigration()).resolves.toBe(false) + expect((engine as any).markAgentsCompleted).toHaveBeenCalledTimes(1) + }) + + it('skips migration when both core v2 and agents import are already completed', async () => { + mockStatuses( + { status: 'completed', version: '2.0.0', completedAt: Date.now(), error: null }, + { status: 'completed', version: '2.1.0-agents', completedAt: Date.now(), error: null } + ) + vi.spyOn(engine as any, 'hasCoreLegacyData').mockReturnValue(false) + vi.spyOn(engine as any, 'hasLegacyAgentsData').mockReturnValue(false) + + await expect(engine.needsMigration()).resolves.toBe(false) + }) + + it('runs only the agents migrator and clears only agents tables for an agents-only migration', async () => { + const events: string[] = [] + const boot = createTestMigrator('boot', 1, events) + const agents = createTestMigrator('agents', 2.5, events) + + engine.registerMigrators([boot as any, agents as any]) + + vi.spyOn(engine as any, 'getPendingMigrationPlan').mockResolvedValue({ + fullMigrationNeeded: false, + agentsMigrationNeeded: true, + migrators: [agents] + }) + const clearAgentsTables = vi.spyOn(engine as any, 'verifyAndClearAgentsTables').mockResolvedValue(undefined) + + await engine.run({}, '/tmp/dexie_export', '/tmp/localstorage_export/export.json') + + expect(clearAgentsTables).toHaveBeenCalledTimes(1) + expect((engine as any).verifyAndClearNewTables).not.toHaveBeenCalled() + expect(events).toStrictEqual(['agents:reset', 'agents:prepare', 'agents:execute', 'agents:validate']) + }) + + it('marks both core and agents migration as completed when the user skips migration', async () => { + await engine.skipMigration() + + expect((engine as any).markCompleted).toHaveBeenCalledTimes(1) + expect((engine as any).markAgentsCompleted).toHaveBeenCalledTimes(1) + }) }) diff --git a/src/main/data/migration/v2/migrators/AgentsMigrator.ts b/src/main/data/migration/v2/migrators/AgentsMigrator.ts new file mode 100644 index 00000000000..0f7f6c6b04c --- /dev/null +++ b/src/main/data/migration/v2/migrators/AgentsMigrator.ts @@ -0,0 +1,148 @@ +import { loggerService } from '@logger' +import type { ExecuteResult, PrepareResult, ValidateResult, ValidationError } from '@shared/data/migration/v2/types' +import { sql } from 'drizzle-orm' + +import type { MigrationContext } from '../core/MigrationContext' +import { LegacyAgentsDbReader } from '../utils/LegacyAgentsDbReader' +import { BaseMigrator } from './BaseMigrator' +import { + AGENTS_TABLE_MIGRATION_SPECS, + type AgentsTableRowCounts, + buildAgentsImportStatements, + getTotalAgentsRowCount +} from './mappings/AgentsDbMappings' + +const logger = loggerService.withContext('AgentsMigrator') + +export class AgentsMigrator extends BaseMigrator { + readonly id = 'agents' + readonly name = 'Agents' + readonly description = 'Migrate legacy agents.db data into the main SQLite database' + readonly order = 2.5 + + private sourceCounts: AgentsTableRowCounts = this.createEmptyCounts() + + override reset(): void { + this.sourceCounts = this.createEmptyCounts() + } + + async prepare(): Promise { + const reader = new LegacyAgentsDbReader() + const dbPath = reader.resolvePath() + + if (!dbPath) { + return { + success: true, + itemCount: 0, + warnings: ['agents.db not found - no agents data to migrate'] + } + } + + this.sourceCounts = await reader.countRows() + + return { + success: true, + itemCount: getTotalAgentsRowCount(this.sourceCounts) + } + } + + async execute(ctx: MigrationContext): Promise { + const reader = new LegacyAgentsDbReader() + const dbPath = reader.resolvePath() + + if (!dbPath) { + logger.info('No legacy agents.db found, skipping agents migration') + return { success: true, processedCount: 0 } + } + + if (getTotalAgentsRowCount(this.sourceCounts) === 0) { + this.sourceCounts = await reader.countRows() + } + + const statements = buildAgentsImportStatements(dbPath) + const [attachStatement, ...remainingStatements] = statements + let isAttached = false + + try { + await ctx.db.run(sql.raw(attachStatement)) + isAttached = true + + for (const statement of remainingStatements.slice(0, -1)) { + await ctx.db.run(sql.raw(statement)) + } + } finally { + if (isAttached) { + await ctx.db.run(sql.raw('DETACH DATABASE agents_legacy')) + } + } + + return { + success: true, + processedCount: getTotalAgentsRowCount(this.sourceCounts) + } + } + + async validate(ctx: MigrationContext): Promise { + const reader = new LegacyAgentsDbReader() + const dbPath = reader.resolvePath() + + if (!dbPath) { + return { + success: true, + errors: [], + stats: { + sourceCount: 0, + targetCount: 0, + skippedCount: 0 + } + } + } + + if (getTotalAgentsRowCount(this.sourceCounts) === 0) { + this.sourceCounts = await reader.countRows() + } + + const errors: ValidationError[] = [] + let targetCount = 0 + + for (const spec of AGENTS_TABLE_MIGRATION_SPECS) { + const result = await ctx.db.get<{ count: number }>(sql.raw(`SELECT COUNT(*) AS count FROM ${spec.targetTable}`)) + const tableTargetCount = Number(result?.count ?? 0) + const tableSourceCount = this.sourceCounts[spec.sourceTable] + targetCount += tableTargetCount + + if (tableTargetCount < tableSourceCount) { + errors.push({ + key: `${spec.targetTable}_count_mismatch`, + expected: tableSourceCount, + actual: tableTargetCount, + message: `${spec.targetTable} count too low: expected ${tableSourceCount}, got ${tableTargetCount}` + }) + } + } + + return { + success: errors.length === 0, + errors, + stats: { + sourceCount: getTotalAgentsRowCount(this.sourceCounts), + targetCount, + skippedCount: 0, + mismatchReason: errors.length > 0 ? 'One or more agents_* tables were not fully imported' : undefined + } + } + } + + private createEmptyCounts(): AgentsTableRowCounts { + return { + agents: 0, + sessions: 0, + skills: 0, + scheduled_tasks: 0, + task_run_logs: 0, + channels: 0, + channel_task_subscriptions: 0, + session_messages: 0 + } + } +} diff --git a/src/main/data/migration/v2/migrators/README-AgentsMigrator.md b/src/main/data/migration/v2/migrators/README-AgentsMigrator.md new file mode 100644 index 00000000000..d174f2f56c9 --- /dev/null +++ b/src/main/data/migration/v2/migrators/README-AgentsMigrator.md @@ -0,0 +1,55 @@ +# AgentsMigrator + +## Purpose + +Migrates the legacy standalone `agents.db` SQLite database into the main `cherrystudio.sqlite` database during the v2 migration flow. + +## Source + +Legacy source database locations checked in order: + +1. `application.getPath('feature.agents.db_file')` → `{userData}/Data/agents.db` +2. `application.getPath('app.userdata', 'agents.db')` → legacy fallback `{userData}/agents.db` + +## Target tables + +- `agents_agents` +- `agents_sessions` +- `agents_skills` +- `agents_tasks` +- `agents_task_run_logs` +- `agents_channels` +- `agents_channel_task_subscriptions` +- `agents_session_messages` + +## Import strategy + +The migrator uses SQLite-native copy statements: + +1. `ATTACH DATABASE ... AS agents_legacy` +2. `INSERT INTO agents_* (...) SELECT ... FROM agents_legacy.*` +3. `DETACH DATABASE agents_legacy` + +This keeps IDs, timestamps, and JSON/text payloads intact while avoiding per-row TypeScript transforms. + +## Table mapping + +| Legacy table | Main DB table | +| --- | --- | +| `agents` | `agents_agents` | +| `sessions` | `agents_sessions` | +| `skills` | `agents_skills` | +| `scheduled_tasks` | `agents_tasks` | +| `task_run_logs` | `agents_task_run_logs` | +| `channels` | `agents_channels` | +| `channel_task_subscriptions` | `agents_channel_task_subscriptions` | +| `session_messages` | `agents_session_messages` | + +## Validation + +Validation compares source and target row counts for every migrated table. Any target table with fewer rows than its source table fails the migration. + +## Notes + +- The v2 migration target version was bumped so installs already marked as `2.0.0` will still rerun migration if legacy `agents.db` data remains. +- The import is intentionally schema-preserving to reduce cutover risk for the later agents-service refactor. diff --git a/src/main/data/migration/v2/migrators/__tests__/AgentsMigrator.test.ts b/src/main/data/migration/v2/migrators/__tests__/AgentsMigrator.test.ts new file mode 100644 index 00000000000..62331ea8b34 --- /dev/null +++ b/src/main/data/migration/v2/migrators/__tests__/AgentsMigrator.test.ts @@ -0,0 +1,107 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@logger', () => ({ + loggerService: { + withContext: vi.fn(() => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn() + })) + } +})) + +import { LegacyAgentsDbReader } from '../../utils/LegacyAgentsDbReader' +import { AgentsMigrator } from '../AgentsMigrator' + +function createCounts() { + return { + agents: 1, + sessions: 2, + skills: 3, + scheduled_tasks: 4, + task_run_logs: 5, + channels: 6, + channel_task_subscriptions: 7, + session_messages: 8 + } +} + +describe('AgentsMigrator', () => { + let migrator: AgentsMigrator + + beforeEach(() => { + migrator = new AgentsMigrator() + vi.restoreAllMocks() + }) + + it('prepare skips cleanly when no legacy agents db exists', async () => { + vi.spyOn(LegacyAgentsDbReader.prototype, 'resolvePath').mockReturnValue(null) + + const result = await migrator.prepare() + + expect(result.success).toBe(true) + expect(result.itemCount).toBe(0) + expect(result.warnings).toEqual(['agents.db not found - no agents data to migrate']) + }) + + it('prepare counts all legacy agents rows', async () => { + vi.spyOn(LegacyAgentsDbReader.prototype, 'resolvePath').mockReturnValue('/mock/feature.agents.db_file') + vi.spyOn(LegacyAgentsDbReader.prototype, 'countRows').mockResolvedValue(createCounts()) + + const result = await migrator.prepare() + + expect(result.success).toBe(true) + expect(result.itemCount).toBe(36) + }) + + it('execute attaches the legacy db and imports every table', async () => { + const run = vi.fn().mockResolvedValue(undefined) + vi.spyOn(LegacyAgentsDbReader.prototype, 'resolvePath').mockReturnValue('/mock/feature.agents.db_file') + vi.spyOn(LegacyAgentsDbReader.prototype, 'countRows').mockResolvedValue(createCounts()) + + const result = await migrator.execute({ db: { run } } as never) + + expect(result.success).toBe(true) + expect(result.processedCount).toBe(36) + expect(run).toHaveBeenCalledTimes(10) + }) + + it('detaches the legacy db when an import statement fails after attach', async () => { + const run = vi + .fn() + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('insert failed')) + .mockResolvedValueOnce(undefined) + + vi.spyOn(LegacyAgentsDbReader.prototype, 'resolvePath').mockReturnValue('/mock/feature.agents.db_file') + vi.spyOn(LegacyAgentsDbReader.prototype, 'countRows').mockResolvedValue(createCounts()) + + await expect(migrator.execute({ db: { run } } as never)).rejects.toThrow('insert failed') + expect(run).toHaveBeenLastCalledWith(expect.anything()) + expect(run).toHaveBeenCalledTimes(3) + }) + + it('validate fails when imported table counts are lower than the source counts', async () => { + vi.spyOn(LegacyAgentsDbReader.prototype, 'resolvePath').mockReturnValue('/mock/feature.agents.db_file') + vi.spyOn(LegacyAgentsDbReader.prototype, 'countRows').mockResolvedValue(createCounts()) + + const get = vi + .fn() + .mockResolvedValueOnce({ count: 1 }) + .mockResolvedValueOnce({ count: 1 }) + .mockResolvedValueOnce({ count: 3 }) + .mockResolvedValueOnce({ count: 4 }) + .mockResolvedValueOnce({ count: 5 }) + .mockResolvedValueOnce({ count: 6 }) + .mockResolvedValueOnce({ count: 7 }) + .mockResolvedValueOnce({ count: 8 }) + + const result = await migrator.validate({ db: { get } } as never) + + expect(result.success).toBe(false) + expect(result.errors[0]?.key).toBe('agents_sessions_count_mismatch') + expect(result.stats.sourceCount).toBe(36) + expect(result.stats.targetCount).toBe(35) + }) +}) diff --git a/src/main/data/migration/v2/migrators/index.ts b/src/main/data/migration/v2/migrators/index.ts index 110fc381cd4..d8c31f74cac 100644 --- a/src/main/data/migration/v2/migrators/index.ts +++ b/src/main/data/migration/v2/migrators/index.ts @@ -5,6 +5,7 @@ export { BaseMigrator } from './BaseMigrator' // Import all migrators +import { AgentsMigrator } from './AgentsMigrator' import { AssistantMigrator } from './AssistantMigrator' import { BootConfigMigrator } from './BootConfigMigrator' import { ChatMigrator } from './ChatMigrator' @@ -18,6 +19,7 @@ import { TranslateMigrator } from './TranslateMigrator' // Export migrator classes export { + AgentsMigrator, AssistantMigrator, BootConfigMigrator, ChatMigrator, @@ -41,6 +43,7 @@ export function getAllMigrators() { new McpServerMigrator(), new ProviderModelMigrator(), new AssistantMigrator(), + new AgentsMigrator(), new KnowledgeMigrator(), new KnowledgeVectorMigrator(), new ChatMigrator(), diff --git a/src/main/data/migration/v2/migrators/mappings/AgentsDbMappings.ts b/src/main/data/migration/v2/migrators/mappings/AgentsDbMappings.ts new file mode 100644 index 00000000000..300679a4174 --- /dev/null +++ b/src/main/data/migration/v2/migrators/mappings/AgentsDbMappings.ts @@ -0,0 +1,168 @@ +export type AgentsSourceTableName = + | 'agents' + | 'sessions' + | 'skills' + | 'scheduled_tasks' + | 'task_run_logs' + | 'channels' + | 'channel_task_subscriptions' + | 'session_messages' + +export type AgentsTableRowCounts = Record + +export type AgentsTableMigrationSpec = { + sourceTable: AgentsSourceTableName + targetTable: + | 'agents_agents' + | 'agents_sessions' + | 'agents_skills' + | 'agents_tasks' + | 'agents_task_run_logs' + | 'agents_channels' + | 'agents_channel_task_subscriptions' + | 'agents_session_messages' + columns: readonly string[] +} + +export const AGENTS_TABLE_MIGRATION_SPECS: readonly AgentsTableMigrationSpec[] = [ + { + sourceTable: 'agents', + targetTable: 'agents_agents', + columns: [ + 'id', + 'type', + 'name', + 'description', + 'accessible_paths', + 'instructions', + 'model', + 'plan_model', + 'small_model', + 'mcps', + 'allowed_tools', + 'configuration', + 'sort_order', + 'created_at', + 'updated_at' + ] + }, + { + sourceTable: 'sessions', + targetTable: 'agents_sessions', + columns: [ + 'id', + 'agent_type', + 'agent_id', + 'name', + 'description', + 'accessible_paths', + 'instructions', + 'model', + 'plan_model', + 'small_model', + 'mcps', + 'allowed_tools', + 'slash_commands', + 'configuration', + 'sort_order', + 'created_at', + 'updated_at' + ] + }, + { + sourceTable: 'skills', + targetTable: 'agents_skills', + columns: [ + 'id', + 'name', + 'description', + 'folder_name', + 'source', + 'source_url', + 'namespace', + 'author', + 'tags', + 'content_hash', + 'is_enabled', + 'created_at', + 'updated_at' + ] + }, + { + sourceTable: 'scheduled_tasks', + targetTable: 'agents_tasks', + columns: [ + 'id', + 'agent_id', + 'name', + 'prompt', + 'schedule_type', + 'schedule_value', + 'timeout_minutes', + 'next_run', + 'last_run', + 'last_result', + 'status', + 'created_at', + 'updated_at' + ] + }, + { + sourceTable: 'task_run_logs', + targetTable: 'agents_task_run_logs', + columns: ['id', 'task_id', 'session_id', 'run_at', 'duration_ms', 'status', 'result', 'error'] + }, + { + sourceTable: 'channels', + targetTable: 'agents_channels', + columns: [ + 'id', + 'type', + 'name', + 'agent_id', + 'session_id', + 'config', + 'is_active', + 'active_chat_ids', + 'permission_mode', + 'created_at', + 'updated_at' + ] + }, + { + sourceTable: 'channel_task_subscriptions', + targetTable: 'agents_channel_task_subscriptions', + columns: ['channel_id', 'task_id'] + }, + { + sourceTable: 'session_messages', + targetTable: 'agents_session_messages', + columns: ['id', 'session_id', 'role', 'content', 'agent_session_id', 'metadata', 'created_at', 'updated_at'] + } +] as const + +export function getAgentsSourceTableNames(): AgentsSourceTableName[] { + return AGENTS_TABLE_MIGRATION_SPECS.map((spec) => spec.sourceTable) +} + +export function getTotalAgentsRowCount(counts: Partial): number { + return getAgentsSourceTableNames().reduce((total, tableName) => total + (counts[tableName] ?? 0), 0) +} + +export function quoteSqlitePath(path: string): string { + return `'${path.replaceAll("'", "''")}'` +} + +export function buildAgentsImportStatements(dbPath: string): string[] { + const statements = [`ATTACH DATABASE ${quoteSqlitePath(dbPath)} AS agents_legacy`] + + for (const spec of AGENTS_TABLE_MIGRATION_SPECS) { + const columns = spec.columns.join(', ') + statements.push( + `INSERT INTO ${spec.targetTable} (${columns}) SELECT ${columns} FROM agents_legacy.${spec.sourceTable}` + ) + } + + statements.push('DETACH DATABASE agents_legacy') + return statements +} diff --git a/src/main/data/migration/v2/migrators/mappings/__tests__/AgentsDbMappings.test.ts b/src/main/data/migration/v2/migrators/mappings/__tests__/AgentsDbMappings.test.ts new file mode 100644 index 00000000000..7292a381502 --- /dev/null +++ b/src/main/data/migration/v2/migrators/mappings/__tests__/AgentsDbMappings.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest' + +import { + AGENTS_TABLE_MIGRATION_SPECS, + buildAgentsImportStatements, + getAgentsSourceTableNames, + getTotalAgentsRowCount, + quoteSqlitePath +} from '../AgentsDbMappings' + +describe('AgentsDbMappings', () => { + it('builds attach/import/detach statements for the legacy agents db', () => { + const statements = buildAgentsImportStatements("/tmp/agent's.db") + + expect(statements[0]).toBe("ATTACH DATABASE '/tmp/agent''s.db' AS agents_legacy") + expect(statements).toContain( + 'INSERT INTO agents_agents (id, type, name, description, accessible_paths, instructions, model, plan_model, small_model, mcps, allowed_tools, configuration, sort_order, created_at, updated_at) SELECT id, type, name, description, accessible_paths, instructions, model, plan_model, small_model, mcps, allowed_tools, configuration, sort_order, created_at, updated_at FROM agents_legacy.agents' + ) + expect(statements.at(-1)).toBe('DETACH DATABASE agents_legacy') + }) + + it('exposes all source table names in dependency order', () => { + expect(getAgentsSourceTableNames()).toEqual([ + 'agents', + 'sessions', + 'skills', + 'scheduled_tasks', + 'task_run_logs', + 'channels', + 'channel_task_subscriptions', + 'session_messages' + ]) + }) + + it('sums row counts across all tables', () => { + expect( + getTotalAgentsRowCount({ + agents: 2, + sessions: 3, + skills: 4, + scheduled_tasks: 5, + task_run_logs: 6, + channels: 7, + channel_task_subscriptions: 8, + session_messages: 9 + }) + ).toBe(44) + }) + + it('keeps the table spec list aligned with the source table names', () => { + expect(AGENTS_TABLE_MIGRATION_SPECS.map((spec) => spec.sourceTable)).toEqual(getAgentsSourceTableNames()) + }) + + it('quotes sqlite file paths safely', () => { + expect(quoteSqlitePath("/tmp/a'b/c.db")).toBe("'/tmp/a''b/c.db'") + }) +}) diff --git a/src/main/data/migration/v2/utils/LegacyAgentsDbReader.ts b/src/main/data/migration/v2/utils/LegacyAgentsDbReader.ts new file mode 100644 index 00000000000..da84d9078c7 --- /dev/null +++ b/src/main/data/migration/v2/utils/LegacyAgentsDbReader.ts @@ -0,0 +1,83 @@ +import { existsSync } from 'node:fs' + +import { createClient } from '@libsql/client' +import { application } from '@main/core/application' +import { sql } from 'drizzle-orm' +import { drizzle } from 'drizzle-orm/libsql' +import { pathToFileURL } from 'url' + +import { type AgentsTableRowCounts, getAgentsSourceTableNames } from '../migrators/mappings/AgentsDbMappings' + +export type ResolveLegacyAgentsDbPathArgs = { + canonicalPath: string + legacyPath: string + exists: (path: string) => boolean +} + +export function resolveLegacyAgentsDbPath({ + canonicalPath, + legacyPath, + exists +}: ResolveLegacyAgentsDbPathArgs): string | null { + if (exists(canonicalPath)) { + return canonicalPath + } + + if (exists(legacyPath)) { + return legacyPath + } + + return null +} + +export class LegacyAgentsDbReader { + constructor(private readonly exists = existsSync) {} + + getCanonicalPath(): string { + return application.getPath('feature.agents.db_file') + } + + getLegacyPath(): string { + return application.getPath('app.userdata', 'agents.db') + } + + resolvePath(): string | null { + return resolveLegacyAgentsDbPath({ + canonicalPath: this.getCanonicalPath(), + legacyPath: this.getLegacyPath(), + exists: this.exists + }) + } + + async countRows(): Promise { + const dbPath = this.resolvePath() + + if (!dbPath) { + return this.createEmptyCounts() + } + + const client = createClient({ + url: pathToFileURL(dbPath).href, + intMode: 'number' + }) + + const db = drizzle(client) + + try { + const counts = this.createEmptyCounts() + + for (const tableName of getAgentsSourceTableNames()) { + const result = await db.get<{ count: number }>(sql.raw(`SELECT COUNT(*) AS count FROM ${tableName}`)) + counts[tableName] = Number(result?.count ?? 0) + } + + return counts + } finally { + client.close() + } + } + + private createEmptyCounts(): AgentsTableRowCounts { + return Object.fromEntries(getAgentsSourceTableNames().map((tableName) => [tableName, 0])) as AgentsTableRowCounts + } +} diff --git a/src/main/data/migration/v2/utils/__tests__/LegacyAgentsDbReader.test.ts b/src/main/data/migration/v2/utils/__tests__/LegacyAgentsDbReader.test.ts new file mode 100644 index 00000000000..044fd2be5f6 --- /dev/null +++ b/src/main/data/migration/v2/utils/__tests__/LegacyAgentsDbReader.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from 'vitest' + +import { resolveLegacyAgentsDbPath } from '../LegacyAgentsDbReader' + +describe('LegacyAgentsDbReader', () => { + it('prefers the canonical agents db path when it exists', () => { + const exists = vi.fn((candidate: string) => candidate === '/data/agents.db') + + expect( + resolveLegacyAgentsDbPath({ + canonicalPath: '/data/agents.db', + legacyPath: '/user/agents.db', + exists + }) + ).toBe('/data/agents.db') + }) + + it('falls back to the old userData root path', () => { + const exists = vi.fn((candidate: string) => candidate === '/user/agents.db') + + expect( + resolveLegacyAgentsDbPath({ + canonicalPath: '/data/agents.db', + legacyPath: '/user/agents.db', + exists + }) + ).toBe('/user/agents.db') + }) + + it('returns null when no legacy agents db exists', () => { + expect( + resolveLegacyAgentsDbPath({ + canonicalPath: '/data/agents.db', + legacyPath: '/user/agents.db', + exists: () => false + }) + ).toBeNull() + }) +}) diff --git a/src/main/services/agents/BaseService.ts b/src/main/services/agents/BaseService.ts index 1514cc88bca..76725b4a44d 100644 --- a/src/main/services/agents/BaseService.ts +++ b/src/main/services/agents/BaseService.ts @@ -8,7 +8,6 @@ import { objectKeys } from '@types' import fs from 'fs' import path from 'path' -import { databaseManager } from './database/DatabaseManager' import { type AgentModelField, AgentModelValidationError } from './errors' import { builtinSlashCommands } from './services/claudecode/commands' import { builtinTools } from './services/claudecode/tools' @@ -144,12 +143,13 @@ export abstract class BaseService { } /** - * Get database instance - * Automatically waits for initialization to complete + * Get the consolidated v2 main database. + * + * Agents services now read/write the shared main SQLite database via + * `DbService` instead of the deprecated standalone `agents.db` manager. */ public async getDatabase() { - await databaseManager.initialize() - return databaseManager.getDatabase() + return application.get('DbService').getDb() } protected serializeJsonFields(data: any): any { diff --git a/src/main/services/agents/database/schema/agents.schema.ts b/src/main/services/agents/database/schema/agents.schema.ts index 801fc48a1e7..50b3bca6aff 100644 --- a/src/main/services/agents/database/schema/agents.schema.ts +++ b/src/main/services/agents/database/schema/agents.schema.ts @@ -1,38 +1,11 @@ /** - * Drizzle ORM schema for agents table + * Compatibility re-export for the shared agents schema. + * + * The canonical table definition now lives under src/main/data/db/schemas. */ -import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' - -export const agentsTable = sqliteTable('agents', { - id: text('id').primaryKey(), - type: text('type').notNull(), - name: text('name').notNull(), - description: text('description'), - accessible_paths: text('accessible_paths'), // JSON array of directory paths the agent can access - - instructions: text('instructions'), - - model: text('model').notNull(), // Main model ID (required) - plan_model: text('plan_model'), // Optional plan/thinking model ID - small_model: text('small_model'), // Optional small/fast model ID - - mcps: text('mcps'), // JSON array of MCP tool IDs - allowed_tools: text('allowed_tools'), // JSON array of allowed tool IDs (whitelist) - - configuration: text('configuration'), // JSON, extensible settings - - sort_order: integer('sort_order').notNull().default(0), // Manual sort order (lower = first) - - created_at: text('created_at').notNull(), - updated_at: text('updated_at').notNull() -}) - -// Indexes for agents table -export const agentsNameIdx = index('idx_agents_name').on(agentsTable.name) -export const agentsTypeIdx = index('idx_agents_type').on(agentsTable.type) -export const agentsCreatedAtIdx = index('idx_agents_created_at').on(agentsTable.created_at) -export const agentsSortOrderIdx = index('idx_agents_sort_order').on(agentsTable.sort_order) - -export type AgentRow = typeof agentsTable.$inferSelect -export type InsertAgentRow = typeof agentsTable.$inferInsert +export { + type AgentsAgentRow as AgentRow, + agentsAgentsTable as agentsTable, + type InsertAgentsAgentRow as InsertAgentRow +} from '../../../../data/db/schemas/agentsAgents' diff --git a/src/main/services/agents/database/schema/channels.schema.ts b/src/main/services/agents/database/schema/channels.schema.ts index 0ce4d5d4e95..543b5f4312b 100644 --- a/src/main/services/agents/database/schema/channels.schema.ts +++ b/src/main/services/agents/database/schema/channels.schema.ts @@ -36,7 +36,7 @@ export { ChannelConfigSchema } // ---- Channels table ---- export const channelsTable = sqliteTable( - 'channels', + 'agents_channels', { id: text('id') .primaryKey() @@ -69,7 +69,7 @@ export const channelsTable = sqliteTable( // ---- Channel ↔ Task subscriptions (many-to-many) ---- export const channelTaskSubscriptionsTable = sqliteTable( - 'channel_task_subscriptions', + 'agents_channel_task_subscriptions', { channelId: text('channel_id') .notNull() diff --git a/src/main/services/agents/database/schema/messages.schema.ts b/src/main/services/agents/database/schema/messages.schema.ts index d14c7550144..3dc85d05cc1 100644 --- a/src/main/services/agents/database/schema/messages.schema.ts +++ b/src/main/services/agents/database/schema/messages.schema.ts @@ -1,30 +1,11 @@ -import { foreignKey, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' - -import { sessionsTable } from './sessions.schema' - -// session_messages table to log all messages, thoughts, actions, observations in a session -export const sessionMessagesTable = sqliteTable('session_messages', { - id: integer('id').primaryKey({ autoIncrement: true }), - session_id: text('session_id').notNull(), - role: text('role').notNull(), // 'user', 'agent', 'system', 'tool' - content: text('content').notNull(), // JSON structured data - agent_session_id: text('agent_session_id').default(''), - metadata: text('metadata'), // JSON metadata (optional) - created_at: text('created_at').notNull(), - updated_at: text('updated_at').notNull() -}) - -// Indexes for session_messages table -export const sessionMessagesSessionIdIdx = index('idx_session_messages_session_id').on(sessionMessagesTable.session_id) -export const sessionMessagesCreatedAtIdx = index('idx_session_messages_created_at').on(sessionMessagesTable.created_at) -export const sessionMessagesUpdatedAtIdx = index('idx_session_messages_updated_at').on(sessionMessagesTable.updated_at) - -// Foreign keys for session_messages table -export const sessionMessagesFkSession = foreignKey({ - columns: [sessionMessagesTable.session_id], - foreignColumns: [sessionsTable.id], - name: 'fk_session_messages_session_id' -}).onDelete('cascade') - -export type SessionMessageRow = typeof sessionMessagesTable.$inferSelect -export type InsertSessionMessageRow = typeof sessionMessagesTable.$inferInsert +/** + * Compatibility re-export for the shared agents session messages schema. + * + * The canonical table definition now lives under src/main/data/db/schemas. + */ + +export { + type InsertAgentsSessionMessageRow as InsertSessionMessageRow, + type AgentsSessionMessageRow as SessionMessageRow, + agentsSessionMessagesTable as sessionMessagesTable +} from '../../../../data/db/schemas/agentsSessionMessages' diff --git a/src/main/services/agents/database/schema/sessions.schema.ts b/src/main/services/agents/database/schema/sessions.schema.ts index 0b5ab199ae6..3efc402a5d7 100644 --- a/src/main/services/agents/database/schema/sessions.schema.ts +++ b/src/main/services/agents/database/schema/sessions.schema.ts @@ -1,49 +1,11 @@ /** - * Drizzle ORM schema for sessions and session_logs tables + * Compatibility re-export for the shared agents sessions schema. + * + * The canonical table definition now lives under src/main/data/db/schemas. */ -import { foreignKey, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' - -import { agentsTable } from './agents.schema' - -export const sessionsTable = sqliteTable('sessions', { - id: text('id').primaryKey(), - agent_type: text('agent_type').notNull(), - agent_id: text('agent_id').notNull(), // Primary agent ID for the session - name: text('name').notNull(), - description: text('description'), - accessible_paths: text('accessible_paths'), // JSON array of directory paths the agent can access - - instructions: text('instructions'), - - model: text('model').notNull(), // Main model ID (required) - plan_model: text('plan_model'), // Optional plan/thinking model ID - small_model: text('small_model'), // Optional small/fast model ID - - mcps: text('mcps'), // JSON array of MCP tool IDs - allowed_tools: text('allowed_tools'), // JSON array of allowed tool IDs (whitelist) - slash_commands: text('slash_commands'), // JSON array of slash command objects from SDK init - - configuration: text('configuration'), // JSON, extensible settings - - sort_order: integer('sort_order').notNull().default(0), // Manual sort order (lower = first) - - created_at: text('created_at').notNull(), - updated_at: text('updated_at').notNull() -}) - -// Foreign keys for sessions table -export const sessionsFkAgent = foreignKey({ - columns: [sessionsTable.agent_id], - foreignColumns: [agentsTable.id], - name: 'fk_session_agent_id' -}).onDelete('cascade') - -// Indexes for sessions table -export const sessionsCreatedAtIdx = index('idx_sessions_created_at').on(sessionsTable.created_at) -export const sessionsMainAgentIdIdx = index('idx_sessions_agent_id').on(sessionsTable.agent_id) -export const sessionsModelIdx = index('idx_sessions_model').on(sessionsTable.model) -export const sessionsSortOrderIdx = index('idx_sessions_sort_order').on(sessionsTable.sort_order) - -export type SessionRow = typeof sessionsTable.$inferSelect -export type InsertSessionRow = typeof sessionsTable.$inferInsert +export { + type InsertAgentsSessionRow as InsertSessionRow, + type AgentsSessionRow as SessionRow, + agentsSessionsTable as sessionsTable +} from '../../../../data/db/schemas/agentsSessions' diff --git a/src/main/services/agents/database/schema/skills.schema.ts b/src/main/services/agents/database/schema/skills.schema.ts index 82bf07837a8..56276b187a0 100644 --- a/src/main/services/agents/database/schema/skills.schema.ts +++ b/src/main/services/agents/database/schema/skills.schema.ts @@ -1,54 +1,11 @@ /** - * Drizzle ORM schema for skills table + * Compatibility re-export for the shared agents skills schema. * - * Global registry for user-installed skills. - * Actual skill content (SKILL.md) lives on the filesystem; - * this table stores metadata only. + * The canonical table definition now lives under src/main/data/db/schemas. */ -import { randomUUID } from 'node:crypto' - -import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core' - -export const skillsTable = sqliteTable( - 'skills', - { - id: text('id') - .primaryKey() - .$defaultFn(() => randomUUID()), - - name: text('name').notNull(), - description: text('description'), - folder_name: text('folder_name').notNull(), - - // Source tracking - source: text('source').notNull(), // 'marketplace' | 'local' | 'zip' - source_url: text('source_url'), - namespace: text('namespace'), // e.g. "@owner/repo/skill-name" - author: text('author'), - tags: text('tags'), // JSON array of strings - - // Content tracking - content_hash: text('content_hash').notNull(), // SHA-256 of SKILL.md - - // State - is_enabled: integer('is_enabled', { mode: 'boolean' }).notNull().default(true), - - // Timestamps (integer ms, aligned with v2) - created_at: integer('created_at') - .notNull() - .$defaultFn(() => Date.now()), - updated_at: integer('updated_at') - .notNull() - .$defaultFn(() => Date.now()) - .$onUpdateFn(() => Date.now()) - }, - (t) => [ - uniqueIndex('skills_folder_name_unique').on(t.folder_name), - index('idx_skills_source').on(t.source), - index('idx_skills_is_enabled').on(t.is_enabled) - ] -) - -export type SkillRow = typeof skillsTable.$inferSelect -export type InsertSkillRow = typeof skillsTable.$inferInsert +export { + type InsertAgentsSkillRow as InsertSkillRow, + type AgentsSkillRow as SkillRow, + agentsSkillsTable as skillsTable +} from '../../../../data/db/schemas/agentsSkills' diff --git a/src/main/services/agents/database/schema/tasks.schema.ts b/src/main/services/agents/database/schema/tasks.schema.ts index 581571c77bc..1c1730eab7c 100644 --- a/src/main/services/agents/database/schema/tasks.schema.ts +++ b/src/main/services/agents/database/schema/tasks.schema.ts @@ -1,61 +1,14 @@ /** - * Drizzle ORM schema for scheduled tasks tables + * Compatibility re-export for the shared agents tasks schema. + * + * The canonical table definition now lives under src/main/data/db/schemas. */ -import { foreignKey, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' - -import { agentsTable } from './agents.schema' - -export const scheduledTasksTable = sqliteTable('scheduled_tasks', { - id: text('id').primaryKey(), - agent_id: text('agent_id').notNull(), - name: text('name').notNull(), - prompt: text('prompt').notNull(), - schedule_type: text('schedule_type').notNull(), // 'cron' | 'interval' | 'once' - schedule_value: text('schedule_value').notNull(), // cron expression, milliseconds as string, or ISO timestamp - timeout_minutes: integer('timeout_minutes').notNull().default(2), - next_run: text('next_run'), - last_run: text('last_run'), - last_result: text('last_result'), - status: text('status').notNull().default('active'), // 'active' | 'paused' | 'completed' - created_at: text('created_at').notNull(), - updated_at: text('updated_at').notNull() -}) - -export const taskRunLogsTable = sqliteTable('task_run_logs', { - id: integer('id').primaryKey({ autoIncrement: true }), - task_id: text('task_id').notNull(), - session_id: text('session_id'), - run_at: text('run_at').notNull(), - duration_ms: integer('duration_ms').notNull(), - status: text('status').notNull(), // 'success' | 'error' - result: text('result'), - error: text('error') -}) - -// Foreign keys -export const scheduledTasksFkAgent = foreignKey({ - columns: [scheduledTasksTable.agent_id], - foreignColumns: [agentsTable.id], - name: 'fk_scheduled_tasks_agent_id' -}).onDelete('cascade') - -export const taskRunLogsFkTask = foreignKey({ - columns: [taskRunLogsTable.task_id], - foreignColumns: [scheduledTasksTable.id], - name: 'fk_task_run_logs_task_id' -}).onDelete('cascade') - -// Indexes for scheduled_tasks table -export const tasksAgentIdIdx = index('idx_tasks_agent_id').on(scheduledTasksTable.agent_id) -export const tasksNextRunIdx = index('idx_tasks_next_run').on(scheduledTasksTable.next_run) -export const tasksStatusIdx = index('idx_tasks_status').on(scheduledTasksTable.status) - -// Indexes for task_run_logs table -export const taskRunLogsTaskIdIdx = index('idx_task_run_logs_task_id').on(taskRunLogsTable.task_id) - -// Type exports -export type TaskRow = typeof scheduledTasksTable.$inferSelect -export type InsertTaskRow = typeof scheduledTasksTable.$inferInsert -export type TaskRunLogRow = typeof taskRunLogsTable.$inferSelect -export type InsertTaskRunLogRow = typeof taskRunLogsTable.$inferInsert +export { + type InsertAgentsTaskRow as InsertTaskRow, + type InsertAgentsTaskRunLogRow as InsertTaskRunLogRow, + agentsTasksTable as scheduledTasksTable, + type AgentsTaskRow as TaskRow, + type AgentsTaskRunLogRow as TaskRunLogRow, + agentsTaskRunLogsTable as taskRunLogsTable +} from '../../../../data/db/schemas/agentsTasks' diff --git a/src/main/services/agents/skills/SkillRepository.ts b/src/main/services/agents/skills/SkillRepository.ts index 47c8f45cfce..f3490e40919 100644 --- a/src/main/services/agents/skills/SkillRepository.ts +++ b/src/main/services/agents/skills/SkillRepository.ts @@ -43,15 +43,14 @@ export class SkillRepository extends BaseService { async insert(row: InsertSkillRow): Promise { const db = await this.getDatabase() - await db.insert(skillsTable).values(row) + const [inserted] = await db.insert(skillsTable).values(row).returning() - const inserted = await db.select().from(skillsTable).where(eq(skillsTable.id, row.id!)).limit(1) - if (!inserted[0]) { + if (!inserted) { throw new Error(`Failed to insert skill: ${row.name}`) } - logger.info('Skill inserted', { id: row.id, name: row.name }) - return this.rowToInstalledSkill(inserted[0]) + logger.info('Skill inserted', { id: inserted.id, name: inserted.name }) + return this.rowToInstalledSkill(inserted) } async toggleEnabled(id: string, isEnabled: boolean): Promise {