From babc97f1d49754a4b5a7a879e4dd71731b9c790b Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 2 Jul 2026 15:44:22 +0200 Subject: [PATCH] fix(cli): scope session model-usage to family ids to avoid full part scan --- .changeset/fix-model-usage-full-scan.md | 5 +++ .../src/kilocode/session/model-usage.ts | 31 +++++++++---------- 2 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 .changeset/fix-model-usage-full-scan.md diff --git a/.changeset/fix-model-usage-full-scan.md b/.changeset/fix-model-usage-full-scan.md new file mode 100644 index 00000000000..66745ee2815 --- /dev/null +++ b/.changeset/fix-model-usage-full-scan.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix slow message loading when opening or switching sessions. The per-model token usage breakdown scanned the entire message history on every session open, which blocked the transcript from rendering for several seconds on large histories. diff --git a/packages/opencode/src/kilocode/session/model-usage.ts b/packages/opencode/src/kilocode/session/model-usage.ts index 109b0a36b2b..2ea53139c69 100644 --- a/packages/opencode/src/kilocode/session/model-usage.ts +++ b/packages/opencode/src/kilocode/session/model-usage.ts @@ -94,19 +94,13 @@ export namespace ModelUsage { FROM family ORDER BY id` - const USAGE_SQL = ` - WITH RECURSIVE family(id) AS ( - SELECT id - FROM session - WHERE id = ? AND project_id = ? - - UNION - - SELECT child.id - FROM session AS child - JOIN family AS parent ON child.parent_id = parent.id - WHERE child.project_id = ? - ), step AS ( + // Scope aggregation to the already-resolved family session IDs via an IN list. + // Re-deriving the family with an inline recursive CTE prevents SQLite from + // using part_session_idx and forces a full scan of the entire part table + // (seconds on large histories), which blocks the single-threaded server on + // every session open. A concrete IN list lets the planner seek the index. + const usageSql = (placeholders: string) => ` + WITH step AS ( SELECT coalesce(json_extract(part.data, '$.model.providerID'), json_extract(message.data, '$.providerID')) AS providerID, coalesce(json_extract(part.data, '$.model.modelID'), json_extract(message.data, '$.modelID')) AS modelID, @@ -116,10 +110,10 @@ export namespace ModelUsage { max(0, cast(coalesce(json_extract(part.data, '$.tokens.reasoning'), 0) AS INTEGER)) AS reasoning, max(0, cast(coalesce(json_extract(part.data, '$.tokens.cache.read'), 0) AS INTEGER)) AS cache_read, max(0, cast(coalesce(json_extract(part.data, '$.tokens.cache.write'), 0) AS INTEGER)) AS cache_write - FROM family - JOIN part ON part.session_id = family.id + FROM part JOIN message ON message.id = part.message_id AND message.session_id = part.session_id - WHERE json_extract(part.data, '$.type') = 'step-finish' + WHERE part.session_id IN (${placeholders}) + AND json_extract(part.data, '$.type') = 'step-finish' AND json_extract(message.data, '$.role') = 'assistant' ) SELECT @@ -163,7 +157,10 @@ export namespace ModelUsage { .prepare<{ id: SessionID }, [string, string, string]>(FAMILY_SQL) .all(...familyArgs) .map((item) => item.id) - const rows = db.prepare(USAGE_SQL).all(...familyArgs) + const rows = + sessionIDs.length === 0 + ? [] + : db.prepare(usageSql(sessionIDs.map(() => "?").join(","))).all(...sessionIDs) const totals = empty() const models = rows.map((row): Model => { totals.steps += row.steps