Run local usage services in a supervised backend process (#40792)

* 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
This commit is contained in:
Brennan Benson
2026-11-11 16:39:52 -08:00
committed by GitHub
parent 890ad4d8aa
commit 175f10e32b
85 changed files with 4898 additions and 789 deletions
+24 -30
View File
@@ -1,10 +1,10 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, vi } from 'vitest'
import type { Mock } from 'vitest'
import { createTestUsageStoreHost } from '../usage/usage-store-host-test-fixture'
import { scanCodexUsageFilesViaWorker } from '../usage/scan-worker/usage-scan-worker-spawn'
import { CodexUsageStore, initCodexUsagePath } from './store'
import { CodexUsageStore } from './store'
import type { CodexUsagePersistedState } from './types'
export function createEmptyScanResult() {
@@ -66,39 +66,33 @@ export function createWorktreeUsageSession(worktreeId: string) {
}
export function createStoreWithState(state: Partial<CodexUsagePersistedState>): CodexUsageStore {
const store = new CodexUsageStore({
getRepos: () => [],
getAllWorktreeMeta: () => ({}),
getRememberedWorktreeDisplayNames: () => new Map(),
whenLoaded: async () => {}
})
;(store as unknown as { state: CodexUsagePersistedState }).state = {
schemaVersion: 1,
worktreeFingerprint: null,
processedFiles: [],
sessions: [],
dailyAggregates: [],
scanState: {
enabled: false,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
},
...state
}
return store
writeFileSync(
join(activeUserData, 'orca-codex-usage.json'),
JSON.stringify({
schemaVersion: 7,
worktreeFingerprint: null,
processedFiles: [],
sessions: [],
dailyAggregates: [],
scanState: {
enabled: false,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
},
...state
})
)
return new CodexUsageStore(createTestUsageStoreHost({ userDataPath: activeUserData }))
}
/** The caller owns the hoisted Electron mock. */
export function setupCodexUsageStoreEnv(getPathMock: Mock): { tempUserData: string } {
let activeUserData: string
export function setupCodexUsageStoreEnv(): { tempUserData: string } {
const env = { tempUserData: '' }
beforeEach(() => {
env.tempUserData = mkdtempSync(join(tmpdir(), 'orca-codex-usage-store-'))
getPathMock.mockReturnValue(env.tempUserData)
initCodexUsagePath()
activeUserData = env.tempUserData
vi.mocked(scanCodexUsageFilesViaWorker).mockReset()
vi.mocked(scanCodexUsageFilesViaWorker).mockResolvedValue(createEmptyScanResult())
vi.useFakeTimers()
+4 -18
View File
@@ -1,5 +1,4 @@
import { codexOpenCodeTokenSessions } from '../usage/reporting/token-usage/agent-token-usage'
import { app } from 'electron'
import { join } from 'node:path'
import type {
CodexUsageBreakdownKind,
@@ -12,7 +11,7 @@ import type {
CodexUsageSummary
} from '../../shared/codex-usage-types'
import type { AutomationRunUsage } from '../../shared/automations/history/automation-run'
import type { UsageWorktreeSource } from '../usage/worktree-attribution/usage-worktree-metadata'
import type { UsageStoreHost } from '../usage/usage-store-host'
import type { CodexUsagePersistedState } from './types'
import type { AutomationUsageLookupInput } from './codex-automation-run-attribution'
import { CODEX_USAGE_SCHEMA_VERSION, codexUsageProvider } from './codex-usage-provider'
@@ -24,8 +23,6 @@ import { splitUsageCacheFileViaWorker } from '../usage/scan-worker/usage-scan-wo
const SCHEMA_VERSION = CODEX_USAGE_SCHEMA_VERSION
let _codexUsageFile: string | null = null
function getDefaultState(): CodexUsagePersistedState {
return {
schemaVersion: SCHEMA_VERSION,
@@ -63,30 +60,19 @@ export function normalizePersistedState(state: CodexUsagePersistedState): CodexU
}
}
export function initCodexUsagePath(): void {
_codexUsageFile = join(app.getPath('userData'), 'orca-codex-usage.json')
}
function getCodexUsageFile(): string {
if (!_codexUsageFile) {
_codexUsageFile = join(app.getPath('userData'), 'orca-codex-usage.json')
}
return _codexUsageFile
}
export class CodexUsageStore extends UsageProviderStoreLifecycle<
'processedFiles',
CodexUsagePersistedState,
'hasAnyCodexData'
> {
constructor(store: UsageWorktreeSource) {
super(store, {
constructor(host: UsageStoreHost) {
super(host, {
tokenUsage: {
provider: 'codex',
selectSessions: (state) => codexOpenCodeTokenSessions(state.sessions)
},
logTag: '[codex-usage]',
resolveCacheFile: getCodexUsageFile,
resolveCacheFile: () => join(host.userDataPath, 'orca-codex-usage.json'),
createDefaultState: getDefaultState,
normalizeState: normalizePersistedState,
sourceKey: 'processedFiles',