You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/29 10:14:09 UTC

(maka) branch main updated: refactor(storage): remove the unused legacy Usage session scanner (#4060)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 8237a9a0d refactor(storage): remove the unused legacy Usage session scanner (#4060)
8237a9a0d is described below

commit 8237a9a0d96d88dbe4236fd1bcf7f773a59cb3d7
Author: 채희원 <68...@users.noreply.github.com>
AuthorDate: Sat Aug 29 19:14:03 2026 +0900

    refactor(storage): remove the unused legacy Usage session scanner (#4060)
    
    * refactor(storage): remove the unused legacy Usage session scanner
    
    SettingsStore.usageStats and packages/storage/src/usage-stats-store.ts have no production consumer. Desktop reads Usage through the Runtime Host client in runtime-host-usage-ipc-main.ts; the only .usageStats( call site in the repository is window.maka.settings.usageStats(range, host), which resolves through that same Host path.
    
    Refs #4059
    
    Generated-by: Claude Opus 5
    
    * refactor(storage): delete the legacy Usage session scanner
    
    Its only importer was SettingsStore.usageStats, removed in this branch.
    
    Refs #4059
    
    Generated-by: Claude Opus 5
    
    * test(storage): delete the legacy Usage reader tests
    
    These cover SettingsStore.usageStats and the legacy scanner only; both are gone.
    
    Refs #4059
    
    Generated-by: Claude Opus 5
    
    * test(storage): drop ./settings-store from the SQLite entrypoint boundary
    
    usage-stats-store.ts held the only import that pulled node:sqlite into ./settings-store's module graph. With the legacy scanner gone that entrypoint is SQLite-free, so it leaves SQLITE_BACKED_ENTRYPOINTS -- updated in the same change with the reason, as the list's own comment asks.
    
    Refs #4059
    
    Generated-by: Claude Opus 5
    
    * style(storage): collapse the settings type import biome now fits on one line
    
    Dropping UsageRange and UsageStats leaves three specifiers, which biome format prints inline. Caught by `npx biome format packages/storage`.
    
    Refs #4059
    
    Generated-by: Claude Opus 5
---
 .../src/__tests__/public-entrypoints.test.ts       |   1 -
 .../src/__tests__/settings-store-usage.test.ts     | 296 ----------------
 packages/storage/src/settings-store.ts             |  14 +-
 packages/storage/src/usage-stats-store.ts          | 391 ---------------------
 4 files changed, 1 insertion(+), 701 deletions(-)

diff --git a/packages/storage/src/__tests__/public-entrypoints.test.ts b/packages/storage/src/__tests__/public-entrypoints.test.ts
index c177133c0..d3d185cb9 100644
--- a/packages/storage/src/__tests__/public-entrypoints.test.ts
+++ b/packages/storage/src/__tests__/public-entrypoints.test.ts
@@ -66,7 +66,6 @@ const SQLITE_BACKED_ENTRYPOINTS = [
   './session-bundle-policy',
   './session-copy-cleanup',
   './session-store',
-  './settings-store',
   './shell-run-authority',
   './shell-run-store',
   './sqlite-runtime-store',
diff --git a/packages/storage/src/__tests__/settings-store-usage.test.ts b/packages/storage/src/__tests__/settings-store-usage.test.ts
deleted file mode 100644
index 0d8f43a10..000000000
--- a/packages/storage/src/__tests__/settings-store-usage.test.ts
+++ /dev/null
@@ -1,296 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { strict as assert } from 'node:assert';
-import { mkdtemp, rm } from 'node:fs/promises';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
-import { DatabaseSync } from 'node:sqlite';
-import { describe, it } from 'node:test';
-import type { StoredMessage } from '@maka/core/session';
-import { createSessionStore } from '../session-store.js';
-import { createSettingsStore } from '../settings-store.js';
-
-async function seedSession(workspaceRoot: string, messages: StoredMessage[]): Promise<string> {
-  const sessions = createSessionStore(workspaceRoot);
-  try {
-    const header = await sessions.create({
-      cwd: '/tmp/maka-workspace',
-      llmConnectionSlug: 'anthropic',
-      model: 'claude-sonnet-4',
-      permissionMode: 'ask',
-      name: 'Usage fixture',
-    });
-    await sessions.appendMessages(header.id, messages);
-    return header.id;
-  } finally {
-    await sessions.close?.();
-  }
-}
-
-describe('SettingsStore.usageStats request logs', () => {
-  it('does not migrate session metadata when operational state is rejected', async () => {
-    const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-settings-usage-rejected-'));
-    const databasePath = join(workspaceRoot, 'runtime.sqlite');
-    try {
-      const sessions = createSessionStore(workspaceRoot);
-      await sessions.close?.();
-
-      const database = new DatabaseSync(databasePath);
-      database
-        .prepare(`UPDATE session_metadata_schema SET version = 21 WHERE scope = 'session_metadata'`)
-        .run();
-      database
-        .prepare(
-          `INSERT INTO operational_schema_migrations(scope, version, applied_at)
-           VALUES ('future_scope', 1, 0)`,
-        )
-        .run();
-      database.close();
-
-      await assert.rejects(
-        () => createSettingsStore(workspaceRoot).usageStats('all'),
-        /Operational schema future_scope is unknown to this Maka build/,
-      );
-
-      const preserved = new DatabaseSync(databasePath, { readOnly: true });
-      try {
-        assert.equal(
-          (
-            preserved
-              .prepare(
-                `SELECT version FROM session_metadata_schema WHERE scope = 'session_metadata'`,
-              )
-              .get() as { version: number }
-          ).version,
-          21,
-        );
-      } finally {
-        preserved.close();
-      }
-    } finally {
-      await rm(workspaceRoot, { recursive: true, force: true });
-    }
-  });
-
-  it('uses canonical SQLite metadata for transcript-marker sessions', async () => {
-    const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-settings-usage-sqlite-'));
-    const sessions = createSessionStore(workspaceRoot);
-    try {
-      const header = await sessions.create({
-        cwd: '/tmp/maka-workspace',
-        llmConnectionSlug: 'sqlite-provider',
-        model: 'sqlite-default-model',
-        permissionMode: 'ask',
-      });
-      await sessions.appendMessages(header.id, [
-        {
-          type: 'assistant',
-          id: 'assistant-1',
-          turnId: 'turn-1',
-          ts: 10,
-          text: 'tracked',
-          modelId: 'sqlite-runtime-model',
-        },
-        {
-          type: 'token_usage',
-          id: 'usage-1',
-          turnId: 'turn-1',
-          ts: 20,
-          input: 8,
-          output: 2,
-        },
-      ]);
-
-      const stats = await createSettingsStore(workspaceRoot).usageStats('all');
-
-      assert.equal(stats.summary.totalTokens, 10);
-      assert.equal(stats.logs[0]?.provider, 'sqlite-provider');
-      assert.equal(stats.logs[0]?.model, 'sqlite-runtime-model');
-    } finally {
-      await sessions.close?.();
-      await rm(workspaceRoot, { recursive: true, force: true });
-    }
-  });
-
-  it('includes tool invocation rows without inflating model usage totals', async () => {
-    const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-settings-usage-'));
-    try {
-      const sessionId = await seedSession(workspaceRoot, [
-        {
-          type: 'assistant',
-          id: 'assistant-1',
-          turnId: 'turn-1',
-          ts: 10,
-          text: 'I will inspect it.',
-          modelId: 'claude-sonnet-4-runtime',
-        },
-        {
-          type: 'tool_call',
-          id: 'tool-1',
-          turnId: 'turn-1',
-          ts: 11,
-          toolName: 'Bash',
-          displayName: '终端',
-          args: { cmd: 'pwd' },
-        },
-        {
-          type: 'tool_result',
-          id: 'tool-result-1',
-          turnId: 'turn-1',
-          ts: 15,
-          toolUseId: 'tool-1',
-          isError: true,
-          durationMs: 37,
-          content: { kind: 'text', text: 'failed' },
-        },
-        {
-          type: 'token_usage',
-          id: 'usage-1',
-          turnId: 'turn-1',
-          ts: 20,
-          input: 120,
-          output: 30,
-          cacheMissInput: 105,
-          cacheRead: 10,
-          cacheCreation: 5,
-          reasoning: 4,
-          costUsd: 0.01,
-        },
-      ]);
-
-      const stats = await createSettingsStore(workspaceRoot).usageStats('all');
-
-      assert.equal(stats.summary.totalRequests, 1, 'summary counts model requests only');
-      assert.equal(stats.summary.totalTokens, 150);
-      assert.equal(stats.summary.totalCostUsd, 0.01);
-      assert.equal(stats.summary.cacheMiss, 105);
-      assert.equal(stats.summary.cacheRead, 10);
-      assert.equal(stats.summary.cacheCreation, 5);
-      assert.equal(stats.summary.reasoning, 4);
-      assert.equal(stats.byProvider.length, 1, 'provider aggregates remain model-only');
-      assert.equal(stats.byModel.length, 1, 'model aggregates remain model-only');
-
-      const modelLog = stats.logs.find((log) => log.kind === 'model');
-      assert.ok(modelLog);
-      assert.equal(modelLog.sessionId, sessionId);
-      assert.equal(modelLog.turnId, 'turn-1');
-      assert.equal(modelLog.model, 'claude-sonnet-4-runtime');
-      assert.equal(modelLog.inputTokens, 120);
-      assert.equal(modelLog.outputTokens, 30);
-      assert.equal(modelLog.cacheMiss, 105);
-      assert.equal(modelLog.cacheRead, 10);
-      assert.equal(modelLog.cacheCreation, 5);
-      assert.equal(modelLog.reasoning, 4);
-
-      const toolLog = stats.logs.find((log) => log.kind === 'tool');
-      assert.ok(toolLog);
-      assert.equal(toolLog.id, 'tool:tool-1');
-      assert.equal(toolLog.sessionId, sessionId);
-      assert.equal(toolLog.turnId, 'turn-1');
-      assert.equal(toolLog.provider, 'anthropic');
-      assert.equal(toolLog.model, 'claude-sonnet-4');
-      assert.equal(toolLog.toolName, '终端');
-      assert.equal(toolLog.inputTokens, 0);
-      assert.equal(toolLog.outputTokens, 0);
-      assert.equal(toolLog.latencyMs, 37);
-      assert.equal(toolLog.status, 'error');
-    } finally {
-      await rm(workspaceRoot, { recursive: true, force: true });
-    }
-  });
-
-  it('merges tool stats by tool name across sessions instead of one row per session', async () => {
-    const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-settings-usage-bytool-'));
-    try {
-      // Two sessions each call Bash; the second also calls Read. Tool-call ids
-      // are only unique within a session, so both sessions reuse `bash`/`read`
-      // ids to prove result matching stays session-scoped after the merge.
-      const bashTurn = (sessionId: string, error: boolean, duration: number): StoredMessage[] => [
-        {
-          type: 'tool_call',
-          id: 'bash',
-          turnId: 't1',
-          ts: 10,
-          toolName: 'Bash',
-          displayName: '终端',
-          args: {},
-        },
-        {
-          type: 'tool_result',
-          id: 'bash-result',
-          turnId: 't1',
-          ts: 10 + duration,
-          toolUseId: 'bash',
-          isError: error,
-          durationMs: duration,
-          content: { kind: 'text', text: error ? 'failed' : 'ok' },
-        },
-      ];
-
-      await seedSession(workspaceRoot, [
-        ...bashTurn('session-a', false, 20),
-        {
-          type: 'tool_call',
-          id: 'read',
-          turnId: 't1',
-          ts: 12,
-          toolName: 'Read',
-          displayName: '读取',
-          args: {},
-        },
-        {
-          type: 'tool_result',
-          id: 'read-result',
-          turnId: 't1',
-          ts: 40,
-          toolUseId: 'read',
-          isError: false,
-          durationMs: 28,
-          content: { kind: 'text', text: 'ok' },
-        },
-      ]);
-      await seedSession(workspaceRoot, [...bashTurn('session-b', true, 40)]);
-
-      const stats = await createSettingsStore(workspaceRoot).usageStats('all');
-
-      // One row per tool name — never a duplicate Bash row per session.
-      assert.equal(stats.byTool.length, 2, 'byTool must have one row per unique tool');
-      const bash = stats.byTool.find((row) => row.tool === 'Bash');
-      assert.ok(bash, 'a single merged Bash row must exist');
-      assert.equal(bash.calls, 2, 'Bash calls merge across sessions');
-      assert.equal(bash.success, 1, 'the successful Bash call is counted');
-      assert.equal(bash.errors, 1, 'the failed Bash call is counted');
-      assert.equal(bash.avgDurationMs, 30, 'Bash duration averages (20 + 40) / 2 across sessions');
-
-      const read = stats.byTool.find((row) => row.tool === 'Read');
-      assert.ok(read);
-      assert.equal(read.calls, 1);
-      assert.equal(read.avgDurationMs, 28);
-
-      // Rows are ordered by call count desc so the busiest tool leads.
-      assert.deepEqual(
-        stats.byTool.map((row) => row.tool),
-        ['Bash', 'Read'],
-      );
-    } finally {
-      await rm(workspaceRoot, { recursive: true, force: true });
-    }
-  });
-});
diff --git a/packages/storage/src/settings-store.ts b/packages/storage/src/settings-store.ts
index 20f369374..39b0377d1 100644
--- a/packages/storage/src/settings-store.ts
+++ b/packages/storage/src/settings-store.ts
@@ -19,17 +19,10 @@
 
 import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
 import { dirname, join } from 'node:path';
-import type {
-  AppSettings,
-  SettingsTestResult,
-  UpdateAppSettingsInput,
-  UsageRange,
-  UsageStats,
-} from '@maka/core/settings';
+import type { AppSettings, SettingsTestResult, UpdateAppSettingsInput } from '@maka/core/settings';
 import type { OnboardingMilestone, OnboardingMilestoneId } from '@maka/core/onboarding';
 import { createDefaultSettings, mergeSettings, normalizeSettings } from '@maka/core/settings';
 import { sanitizeOnboardingMilestones } from '@maka/core/onboarding';
-import { readUsageStats } from './usage-stats-store.js';
 
 /**
  * A conditional write's patch, either fixed or derived from the state the
@@ -53,7 +46,6 @@ export interface SettingsStore {
     patch: ConditionalSettingsPatch,
   ): Promise<{ applied: boolean; settings: AppSettings }>;
   testNetworkProxy(): Promise<SettingsTestResult>;
-  usageStats(range?: UsageRange): Promise<UsageStats>;
   /**
    * PR110b: upsert a single onboarding milestone. Caller passes the
    * desired terminal status; the store stamps `Date.now()` so the
@@ -214,10 +206,6 @@ class FileSettingsStore implements SettingsStore {
     };
   }
 
-  async usageStats(range: UsageRange = '24h'): Promise<UsageStats> {
-    return readUsageStats(this.workspaceRoot, range);
-  }
-
   private async write(settings: AppSettings): Promise<void> {
     await mkdir(dirname(this.settingsPath), { recursive: true });
     const tempPath = `${this.settingsPath}.${process.pid}.${Date.now()}.tmp`;
diff --git a/packages/storage/src/usage-stats-store.ts b/packages/storage/src/usage-stats-store.ts
deleted file mode 100644
index ecd34981e..000000000
--- a/packages/storage/src/usage-stats-store.ts
+++ /dev/null
@@ -1,391 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { join } from 'node:path';
-import type { UsageRange, UsageStats } from '@maka/core/settings';
-import { legacyUsageProvenance } from '@maka/core/usage-ledger-merge';
-import type { SessionHeader } from '@maka/core/session';
-import {
-  acquireOperationalStateDatabase,
-  OPERATIONAL_STATE_DATABASE_NAME,
-} from './operational-state-store.js';
-import { createSqliteSessionMetadataStore } from './sqlite-session-metadata-store.js';
-
-type UsageSessionHeader = Pick<SessionHeader, 'id' | 'name' | 'llmConnectionSlug' | 'model'>;
-
-type UsageAssistantMessage = {
-  type: 'assistant';
-  turnId: string;
-  modelId: string;
-};
-
-type UsageTokenMessage = {
-  type: 'token_usage';
-  id: string;
-  turnId: string;
-  ts: number;
-  input: number;
-  output: number;
-  cacheMissInput?: number;
-  cacheRead?: number;
-  cacheCreation?: number;
-  reasoning?: number;
-  costUsd?: number;
-};
-
-type UsageToolCallMessage = {
-  type: 'tool_call';
-  id: string;
-  turnId: string;
-  ts: number;
-  toolName: string;
-  displayName?: string;
-};
-
-type UsageToolResultMessage = {
-  type: 'tool_result';
-  turnId: string;
-  ts: number;
-  toolUseId: string;
-  isError: boolean;
-  durationMs?: number;
-};
-
-type UsageMessage =
-  | UsageAssistantMessage
-  | UsageTokenMessage
-  | UsageToolCallMessage
-  | UsageToolResultMessage;
-
-export async function readUsageStats(
-  workspaceRoot: string,
-  range: UsageRange,
-): Promise<UsageStats> {
-  const since = rangeToSince(range);
-  const sessions = await readStoredSessions(workspaceRoot);
-  const modelLogs = sessions.flatMap(({ header, messages }) => {
-    const assistantByTurn = new Map(
-      messages
-        .filter((message) => message.type === 'assistant')
-        .map((message) => [message.turnId, message.modelId]),
-    );
-    return messages
-      .filter((message): message is UsageTokenMessage => message.type === 'token_usage')
-      .filter((message) => !since || message.ts >= since)
-      .map((message) => ({
-        id: message.id,
-        ts: message.ts,
-        kind: 'model' as const,
-        sessionId: header.id,
-        sessionName: header.name,
-        turnId: message.turnId,
-        provider: header.llmConnectionSlug,
-        model: assistantByTurn.get(message.turnId) ?? header.model,
-        inputTokens: message.input,
-        outputTokens: message.output,
-        cacheMiss: message.cacheMissInput,
-        cacheRead: message.cacheRead,
-        cacheCreation: message.cacheCreation,
-        reasoning: message.reasoning,
-        costUsd: message.costUsd,
-        status: 'success' as const,
-      }));
-  });
-
-  const toolRows = aggregateToolStats(sessions, since);
-  const toolLogs = sessions.flatMap(({ header, messages }) =>
-    toolLogRowsFromMessages(header, messages, since),
-  );
-  const logs = [...modelLogs, ...toolLogs].sort((a, b) => b.ts - a.ts);
-  const totalInput = sum(modelLogs.map((log) => log.inputTokens));
-  const totalOutput = sum(modelLogs.map((log) => log.outputTokens));
-  const cacheMiss = sum(modelLogs.map((log) => log.cacheMiss ?? 0));
-  const cacheRead = sum(modelLogs.map((log) => log.cacheRead ?? 0));
-  const cacheCreation = sum(modelLogs.map((log) => log.cacheCreation ?? 0));
-  const reasoning = sum(modelLogs.map((log) => log.reasoning ?? 0));
-  return {
-    summary: {
-      totalRequests: modelLogs.length,
-      totalCostUsd: sum(modelLogs.map((log) => log.costUsd ?? 0)),
-      totalTokens: totalInput + totalOutput,
-      inputTokens: totalInput,
-      outputTokens: totalOutput,
-      cacheTokens: cacheRead + cacheCreation,
-      cacheMiss,
-      cacheRead,
-      cacheCreation,
-      reasoning,
-    },
-    logs,
-    byProvider: aggregateBy(modelLogs, 'provider'),
-    byModel: aggregateBy(modelLogs, 'model'),
-    byTool: toolRows,
-    pricing: [],
-    // This store reads the frozen pre-canonical session records, so its rows are
-    // all legacy: cost is present but was never qualified with a cost basis.
-    provenance: legacyUsageProvenance(modelLogs.length),
-  };
-}
-
-async function readStoredSessions(
-  workspaceRoot: string,
-): Promise<Array<{ header: UsageSessionHeader; messages: UsageMessage[] }>> {
-  const databaseLease = acquireOperationalStateDatabase(workspaceRoot);
-  const metadata = createSqliteSessionMetadataStore(
-    join(workspaceRoot, OPERATIONAL_STATE_DATABASE_NAME),
-    { databaseLease },
-  );
-  try {
-    const sessions: Array<{ header: UsageSessionHeader; messages: UsageMessage[] }> = [];
-    // Usage is a durable cost aggregate, so it counts every Session that spent
-    // tokens, including reserved roles and rows a catalog would hide.
-    for (const { header } of await metadata.list(undefined, 'all')) {
-      const messages = (await metadata.readMessages(header.id)).flatMap((value) => {
-        const message = normalizeUsageMessage(value);
-        return message ? [message] : [];
-      });
-      sessions.push({
-        header: {
-          id: header.id,
-          name: header.name,
-          llmConnectionSlug: header.llmConnectionSlug,
-          model: header.model,
-        },
-        messages,
-      });
-    }
-    return sessions;
-  } catch {
-    return [];
-  } finally {
-    metadata.close();
-  }
-}
-
-function normalizeUsageSessionHeader(value: unknown, sessionId: string): UsageSessionHeader | null {
-  if (!isRecord(value)) return null;
-  if (value.id !== sessionId) return null;
-  if (typeof value.name !== 'string') return null;
-  if (typeof value.llmConnectionSlug !== 'string') return null;
-  if (typeof value.model !== 'string') return null;
-  return {
-    id: value.id,
-    name: value.name,
-    llmConnectionSlug: value.llmConnectionSlug,
-    model: value.model,
-  };
-}
-
-function normalizeUsageMessage(value: unknown): UsageMessage | null {
-  if (!isRecord(value)) return null;
-  switch (value.type) {
-    case 'assistant':
-      if (typeof value.turnId !== 'string') return null;
-      if (typeof value.modelId !== 'string') return null;
-      return { type: 'assistant', turnId: value.turnId, modelId: value.modelId };
-    case 'token_usage':
-      if (typeof value.id !== 'string') return null;
-      if (typeof value.turnId !== 'string') return null;
-      if (!isFiniteNumber(value.ts)) return null;
-      if (!isFiniteNumber(value.input)) return null;
-      if (!isFiniteNumber(value.output)) return null;
-      if (!isOptionalFiniteNumber(value.cacheMissInput)) return null;
-      if (!isOptionalFiniteNumber(value.cacheRead)) return null;
-      if (!isOptionalFiniteNumber(value.cacheCreation)) return null;
-      if (!isOptionalFiniteNumber(value.reasoning)) return null;
-      if (!isOptionalFiniteNumber(value.costUsd)) return null;
-      return {
-        type: 'token_usage',
-        id: value.id,
-        turnId: value.turnId,
-        ts: value.ts,
-        input: value.input,
-        output: value.output,
-        cacheMissInput: value.cacheMissInput,
-        cacheRead: value.cacheRead,
-        cacheCreation: value.cacheCreation,
-        reasoning: value.reasoning,
-        costUsd: value.costUsd,
-      };
-    case 'tool_call':
-      if (typeof value.id !== 'string') return null;
-      if (typeof value.turnId !== 'string') return null;
-      if (!isFiniteNumber(value.ts)) return null;
-      if (typeof value.toolName !== 'string') return null;
-      if (value.displayName !== undefined && typeof value.displayName !== 'string') return null;
-      return {
-        type: 'tool_call',
-        id: value.id,
-        turnId: value.turnId,
-        ts: value.ts,
-        toolName: value.toolName,
-        displayName: value.displayName,
-      };
-    case 'tool_result':
-      if (typeof value.turnId !== 'string') return null;
-      if (!isFiniteNumber(value.ts)) return null;
-      if (typeof value.toolUseId !== 'string') return null;
-      if (typeof value.isError !== 'boolean') return null;
-      if (!isOptionalFiniteNumber(value.durationMs)) return null;
-      return {
-        type: 'tool_result',
-        turnId: value.turnId,
-        ts: value.ts,
-        toolUseId: value.toolUseId,
-        isError: value.isError,
-        durationMs: value.durationMs,
-      };
-    default:
-      return null;
-  }
-}
-
-function isRecord(value: unknown): value is Record<string, unknown> {
-  return typeof value === 'object' && value !== null && !Array.isArray(value);
-}
-
-function isFiniteNumber(value: unknown): value is number {
-  return typeof value === 'number' && Number.isFinite(value);
-}
-
-function isOptionalFiniteNumber(value: unknown): value is number | undefined {
-  return value === undefined || isFiniteNumber(value);
-}
-
-function rangeToSince(range: UsageRange): number | null {
-  const now = Date.now();
-  switch (range) {
-    case '24h':
-      return now - 24 * 60 * 60 * 1000;
-    case '7d':
-      return now - 7 * 24 * 60 * 60 * 1000;
-    case '30d':
-      return now - 30 * 24 * 60 * 60 * 1000;
-    case 'all':
-      return null;
-  }
-}
-
-function aggregateBy(logs: UsageStats['logs'], key: 'provider' | 'model') {
-  const rows = new Map<string, { requests: number; tokens: number; costUsd: number }>();
-  for (const log of logs) {
-    const id = log[key];
-    const current = rows.get(id) ?? { requests: 0, tokens: 0, costUsd: 0 };
-    current.requests += 1;
-    current.tokens += log.inputTokens + log.outputTokens;
-    current.costUsd += log.costUsd ?? 0;
-    rows.set(id, current);
-  }
-  return [...rows.entries()]
-    .map(([id, row]) => ({ [key]: id, ...row }))
-    .sort((a, b) => b.requests - a.requests) as never;
-}
-
-// Aggregate tool usage by tool name across EVERY session so 工具统计 shows one row
-// per tool (not one row per tool-per-session, which repeated the same tool name).
-// tool_call.id ↔ tool_result.toolUseId matching stays scoped to each session's
-// messages — ids are only unique within a session — while the counts, failures,
-// and durations merge into a single global row keyed by tool name.
-function aggregateToolStats(
-  sessions: Array<{ messages: UsageMessage[] }>,
-  since: number | null,
-): UsageStats['byTool'] {
-  const rows = new Map<
-    string,
-    { calls: number; success: number; errors: number; totalDuration: number; durationCount: number }
-  >();
-  for (const { messages } of sessions) {
-    const results = new Map(
-      messages
-        .filter((message): message is UsageToolResultMessage => message.type === 'tool_result')
-        .map((message) => [message.toolUseId, message]),
-    );
-    const calls = messages.filter(
-      (message): message is UsageToolCallMessage => message.type === 'tool_call',
-    );
-    for (const call of calls) {
-      if (since && call.ts < since) continue;
-      const result = results.get(call.id);
-      const current = rows.get(call.toolName) ?? {
-        calls: 0,
-        success: 0,
-        errors: 0,
-        totalDuration: 0,
-        durationCount: 0,
-      };
-      current.calls += 1;
-      if (result?.isError) current.errors += 1;
-      else current.success += 1;
-      if (result?.durationMs !== undefined) {
-        current.totalDuration += result.durationMs;
-        current.durationCount += 1;
-      }
-      rows.set(call.toolName, current);
-    }
-  }
-  return [...rows.entries()]
-    .map(([tool, row]) => ({
-      tool,
-      calls: row.calls,
-      success: row.success,
-      errors: row.errors,
-      avgDurationMs: row.durationCount ? Math.round(row.totalDuration / row.durationCount) : 0,
-    }))
-    .sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool));
-}
-
-function toolLogRowsFromMessages(
-  header: UsageSessionHeader,
-  messages: UsageMessage[],
-  since: number | null,
-): UsageStats['logs'] {
-  const calls = messages.filter(
-    (message): message is UsageToolCallMessage => message.type === 'tool_call',
-  );
-  const results = new Map(
-    messages
-      .filter((message): message is UsageToolResultMessage => message.type === 'tool_result')
-      .map((message) => [message.toolUseId, message]),
-  );
-  return calls
-    .filter((call) => !since || call.ts >= since)
-    .map((call) => {
-      const result = results.get(call.id);
-      const ts = result?.ts ?? call.ts;
-      return {
-        id: `tool:${call.id}`,
-        ts,
-        kind: 'tool' as const,
-        sessionId: header.id,
-        sessionName: header.name,
-        turnId: call.turnId,
-        provider: header.llmConnectionSlug,
-        model: header.model,
-        toolName: call.displayName ?? call.toolName,
-        inputTokens: 0,
-        outputTokens: 0,
-        latencyMs: result?.durationMs,
-        status: result?.isError ? ('error' as const) : ('success' as const),
-      };
-    });
-}
-
-function sum(values: number[]): number {
-  return values.reduce((total, value) => total + value, 0);
-}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org