mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 09:57:48 +00:00
* Move local usage services into a supervised backend process * Own usage recovery and worker retirement lifetimes * Preserve answered usage data during background refresh * Run independent usage scans concurrently in the backend * Limit backend startup paths to the delegated usage service * Show usage failures through backend recovery * Include usage failure strings in the English catalog * Keep usage update details beside refresh failures * Resolve usage freshness failure from the final producer snapshot * Retain the forced refresh completion requirement * Keep managed backend failures out of application crash reports * Preserve usage recovery boundaries across backend reconnects * Match the backend RPC cancellation signal in the usage seam proof * Avoid serializing backend requests before port delivery * Keep receiving request validation in the boundary schemas * test: align usage transport coverage with renderer project * fix: preserve unknown usage state and renderer retirement * fix(usage): revalidate tracking before scan admission * fix(usage): retry failed backend startup on demand * test(usage): isolate stale backend ownership checks
146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
import { claudeTokenSessions } from '../usage/reporting/token-usage/agent-token-usage'
|
|
import { join } from 'node:path'
|
|
import type {
|
|
ClaudeUsageBreakdownKind,
|
|
ClaudeUsageBreakdownRow,
|
|
ClaudeUsageDailyPoint,
|
|
ClaudeUsageRange,
|
|
ClaudeUsageScope,
|
|
ClaudeUsageSessionRow,
|
|
ClaudeUsageSnapshot,
|
|
ClaudeUsageSummary
|
|
} from '../../shared/claude-usage-types'
|
|
import type { AutomationRunUsage } from '../../shared/automations/history/automation-run'
|
|
import type { UsageStoreHost } from '../usage/usage-store-host'
|
|
import type { ClaudeUsagePersistedState } from './types'
|
|
import {
|
|
scanClaudeUsageFilesViaWorker,
|
|
splitUsageCacheFileViaWorker
|
|
} from '../usage/scan-worker/usage-scan-worker-spawn'
|
|
import { UsageProviderStoreLifecycle } from '../usage/provider-lifecycle/usage-provider-store-lifecycle'
|
|
import { buildBreakdown, buildDaily, buildSummary } from './claude-usage-report-aggregation'
|
|
import { buildRecentSessions } from './claude-usage-session-rows'
|
|
import type { AutomationUsageLookupInput } from './claude-usage-automation-attribution'
|
|
import { resolveAutomationRunUsage } from './claude-usage-automation-attribution'
|
|
import { parseClaudeUsageReport, serializeClaudeUsageReport } from './persisted-usage-report'
|
|
|
|
// Pre-v6 totals lack stable fork ownership or the 1-hour cache-write split and must be rescanned.
|
|
// v7 stores protected column checkpoints on the worker and a separately protected report.
|
|
const SCHEMA_VERSION = 7
|
|
|
|
function getDefaultState(): ClaudeUsagePersistedState {
|
|
return {
|
|
schemaVersion: SCHEMA_VERSION,
|
|
worktreeFingerprint: null,
|
|
processedFiles: [],
|
|
sessions: [],
|
|
dailyAggregates: [],
|
|
scanState: {
|
|
enabled: false,
|
|
lastScanStartedAt: null,
|
|
lastScanCompletedAt: null,
|
|
lastScanError: null
|
|
}
|
|
}
|
|
}
|
|
|
|
function normalizePersistedState(state: ClaudeUsagePersistedState): ClaudeUsagePersistedState {
|
|
if (state.schemaVersion === 6 || state.schemaVersion === SCHEMA_VERSION) {
|
|
return state
|
|
}
|
|
// Scanner changes invalidate totals, but preserving enabled keeps existing tracking on.
|
|
const defaults = getDefaultState()
|
|
return {
|
|
...defaults,
|
|
scanState: {
|
|
...defaults.scanState,
|
|
enabled: state.scanState.enabled ?? defaults.scanState.enabled
|
|
}
|
|
}
|
|
}
|
|
|
|
export class ClaudeUsageStore extends UsageProviderStoreLifecycle<
|
|
'processedFiles',
|
|
ClaudeUsagePersistedState,
|
|
'hasAnyClaudeData'
|
|
> {
|
|
constructor(host: UsageStoreHost) {
|
|
super(host, {
|
|
tokenUsage: {
|
|
provider: 'claude',
|
|
selectSessions: (state) => claudeTokenSessions(state.sessions)
|
|
},
|
|
logTag: '[claude-usage]',
|
|
resolveCacheFile: () => join(host.userDataPath, 'orca-claude-usage.json'),
|
|
createDefaultState: getDefaultState,
|
|
normalizeState: normalizePersistedState,
|
|
parseReport: parseClaudeUsageReport,
|
|
serializeReport: serializeClaudeUsageReport,
|
|
providerId: 'claude',
|
|
sourceKey: 'processedFiles',
|
|
dataPresenceKey: 'hasAnyClaudeData',
|
|
scan: async (worktrees, sourceCache) => {
|
|
const profileDirs = await host.claudeProfileDirs()
|
|
host.signal.throwIfAborted()
|
|
return scanClaudeUsageFilesViaWorker(worktrees, sourceCache, profileDirs)
|
|
},
|
|
splitCacheFile: splitUsageCacheFileViaWorker
|
|
})
|
|
}
|
|
|
|
getSnapshot(
|
|
scope: ClaudeUsageScope,
|
|
range: ClaudeUsageRange,
|
|
recentSessionLimit = 10
|
|
): ClaudeUsageSnapshot {
|
|
return {
|
|
scanState: this.getScanState(),
|
|
summary: buildSummary(this.state, scope, range),
|
|
daily: buildDaily(this.state, scope, range),
|
|
modelBreakdown: buildBreakdown(this.state, scope, range, 'model'),
|
|
projectBreakdown: buildBreakdown(this.state, scope, range, 'project'),
|
|
recentSessions: buildRecentSessions(this.state, scope, range, recentSessionLimit)
|
|
}
|
|
}
|
|
|
|
async getSummary(scope: ClaudeUsageScope, range: ClaudeUsageRange): Promise<ClaudeUsageSummary> {
|
|
await this.refresh(false)
|
|
return buildSummary(this.state, scope, range)
|
|
}
|
|
|
|
async getDaily(
|
|
scope: ClaudeUsageScope,
|
|
range: ClaudeUsageRange
|
|
): Promise<ClaudeUsageDailyPoint[]> {
|
|
await this.refresh(false)
|
|
return buildDaily(this.state, scope, range)
|
|
}
|
|
|
|
async getBreakdown(
|
|
scope: ClaudeUsageScope,
|
|
range: ClaudeUsageRange,
|
|
kind: ClaudeUsageBreakdownKind
|
|
): Promise<ClaudeUsageBreakdownRow[]> {
|
|
await this.refresh(false)
|
|
return buildBreakdown(this.state, scope, range, kind)
|
|
}
|
|
|
|
async getRecentSessions(
|
|
scope: ClaudeUsageScope,
|
|
range: ClaudeUsageRange,
|
|
limit = 12
|
|
): Promise<ClaudeUsageSessionRow[]> {
|
|
await this.refresh(false)
|
|
return buildRecentSessions(this.state, scope, range, limit)
|
|
}
|
|
|
|
async getAutomationRunUsage(input: AutomationUsageLookupInput): Promise<AutomationRunUsage> {
|
|
await this.whenLoaded()
|
|
return resolveAutomationRunUsage(input, {
|
|
getState: () => this.state,
|
|
refresh: (force) => this.refresh(force),
|
|
isScanning: () => this.getScanState().isScanning
|
|
})
|
|
}
|
|
}
|