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
+10 -2
View File
@@ -1,3 +1,7 @@
import type {
BackendConnectionListener,
BackendConnectionPurpose
} from '../../shared/backend-service-contract'
import type { ClaudeUsageBreakdownKind, ClaudeUsageSnapshot } from '../../shared/claude-usage-types'
import type { CodexUsageBreakdownKind, CodexUsageSnapshot } from '../../shared/codex-usage-types'
import type {
@@ -25,10 +29,14 @@ export type UsageQueryArgs<Snapshot extends UsageProviderSnapshot> = Pick<
>
export type UsageProviderApi<Snapshot extends UsageProviderSnapshot, BreakdownKind> = {
getScanState: () => Promise<Snapshot['scanState']>
onConnectionChanged?: (listener: BackendConnectionListener) => () => void
getScanState: (purpose?: BackendConnectionPurpose) => Promise<Snapshot['scanState']>
setEnabled: (args: { enabled: boolean }) => Promise<Snapshot['scanState']>
refresh: (args?: { force?: boolean }) => Promise<Snapshot['scanState']>
getSnapshot: (args: UsageQueryArgs<Snapshot> & { limit?: number }) => Promise<Snapshot>
getSnapshot: (
args: UsageQueryArgs<Snapshot> & { limit?: number },
purpose?: BackendConnectionPurpose
) => Promise<Snapshot>
getSummary: (args: UsageQueryArgs<Snapshot>) => Promise<Snapshot['summary']>
getDaily: (args: UsageQueryArgs<Snapshot>) => Promise<Snapshot['daily']>
getBreakdown: (
+2 -2
View File
@@ -1,8 +1,8 @@
import { ipcRenderer } from 'electron'
import { backendUsageTransport } from '../backend-port-client'
import { createUsageProviderApi } from '../usage-provider-api'
import type { PreloadApi } from '../api-types'
export const claudeUsageApi = createUsageProviderApi(
ipcRenderer,
backendUsageTransport,
'claudeUsage'
) satisfies PreloadApi['claudeUsage']
+2 -2
View File
@@ -1,8 +1,8 @@
import { ipcRenderer } from 'electron'
import { backendUsageTransport } from '../backend-port-client'
import { createUsageProviderApi } from '../usage-provider-api'
import type { PreloadApi } from '../api-types'
export const codexUsageApi = createUsageProviderApi(
ipcRenderer,
backendUsageTransport,
'codexUsage'
) satisfies PreloadApi['codexUsage']
+2 -2
View File
@@ -1,8 +1,8 @@
import { ipcRenderer } from 'electron'
import { backendUsageTransport } from '../backend-port-client'
import { createUsageProviderApi } from '../usage-provider-api'
import type { PreloadApi } from '../api-types'
export const museUsageApi = createUsageProviderApi(
ipcRenderer,
backendUsageTransport,
'museUsage'
) satisfies PreloadApi['museUsage']
+2 -2
View File
@@ -1,8 +1,8 @@
import { ipcRenderer } from 'electron'
import { backendUsageTransport } from '../backend-port-client'
import { createUsageProviderApi } from '../usage-provider-api'
import type { PreloadApi } from '../api-types'
export const openCodeUsageApi = createUsageProviderApi(
ipcRenderer,
backendUsageTransport,
'openCodeUsage'
) satisfies PreloadApi['openCodeUsage']
+151
View File
@@ -0,0 +1,151 @@
import { ipcRenderer } from 'electron'
import { BackendPortRpc, type BackendPort } from '../shared/backend-port-rpc'
import {
BACKEND_CONNECTION_CHANGED,
BACKEND_PORT_DELIVERY,
BACKEND_PORT_REQUEST,
type BackendConnectionPurpose,
type BackendConnectionStatus,
type BackendConnectionListener
} from '../shared/backend-service-contract'
let connection: BackendPortRpc | undefined
let connecting: Promise<BackendPortRpc> | undefined
type PortAttempt = {
token: string
purpose: BackendConnectionPurpose
resolve(client: BackendPortRpc): void
reject(error: Error): void
}
let pendingPort: PortAttempt | undefined
const connectionListeners = new Set<BackendConnectionListener>()
let connected: boolean | undefined
let unavailable = false
function publishConnection(next: boolean): void {
if (next) {
unavailable = false
}
if (connected === next) {
return
}
connected = next
if (!next) {
const previous = connection
connection = undefined
connecting = undefined
const attempt = pendingPort
pendingPort = undefined
previous?.close()
attempt?.reject(new Error('Usage disconnected. Try again after it reconnects.'))
}
for (const listener of connectionListeners) {
listener(next)
}
}
ipcRenderer.on(BACKEND_PORT_DELIVERY, (event, data: { token?: string }) => {
const [port, ...surplus] = event.ports
for (const extra of surplus) {
extra.close()
}
if (!port || !pendingPort || pendingPort.token !== data?.token) {
port?.close()
return
}
const adapter: BackendPort = {
postMessage: (message) => port.postMessage(message),
onMessage(listener) {
const receive = (event: MessageEvent): void => listener(event.data)
port.addEventListener('message', receive)
port.start()
return () => port.removeEventListener('message', receive)
},
onClose(listener) {
port.addEventListener('close', listener)
return () => port.removeEventListener('close', listener)
},
close: () => port.close()
}
const previous = connection
connection = undefined
previous?.close()
const client = new BackendPortRpc(
adapter,
'renderer',
async () => {
throw new Error('Renderer capabilities are not available on this port.')
},
() => {
if (connection === client) {
publishConnection(false)
}
}
)
connection = client
const attempt = pendingPort
pendingPort = undefined
attempt.resolve(client)
publishConnection(true)
})
function requestPort(attempt: PortAttempt): void {
void ipcRenderer
.invoke(BACKEND_PORT_REQUEST, attempt.token, attempt.purpose)
.catch((error: Error) => {
if (pendingPort === attempt) {
pendingPort = undefined
attempt.reject(error)
}
})
}
function connect(purpose: BackendConnectionPurpose = 'demand'): Promise<BackendPortRpc> {
if (connection) {
return Promise.resolve(connection)
}
if (connecting) {
if (purpose === 'demand' && pendingPort?.purpose === 'recovery') {
pendingPort = { ...pendingPort, token: crypto.randomUUID(), purpose }
requestPort(pendingPort)
}
return connecting
}
const attemptPromise = new Promise<BackendPortRpc>((resolve, reject) => {
const attempt = { token: crypto.randomUUID(), purpose, resolve, reject }
pendingPort = attempt
requestPort(attempt)
})
const result = attemptPromise.finally(() => {
if (connecting === result) {
connecting = undefined
}
})
connecting = result
return result
}
ipcRenderer.on(BACKEND_CONNECTION_CHANGED, (_event, state: BackendConnectionStatus) => {
publishConnection(state.connected)
if (!state.connected && state.unavailable && !unavailable) {
unavailable = true
for (const listener of connectionListeners) {
listener(false, { unavailable: true })
}
}
})
export const backendUsageTransport = {
onConnectionChanged(listener: BackendConnectionListener): () => void {
connectionListeners.add(listener)
listener(connected ?? false, { initial: true, unavailable })
return () => {
connectionListeners.delete(listener)
}
},
invoke: async (
method: string,
params?: unknown,
purpose?: BackendConnectionPurpose
): Promise<unknown> => (await connect(purpose)).call(method, params)
}
+23 -7
View File
@@ -1,4 +1,7 @@
import type { IpcRenderer } from 'electron'
import type {
BackendConnectionListener,
BackendConnectionPurpose
} from '../shared/backend-service-contract'
import type { PreloadApi } from './api-types'
type UsageProviderApiKey = 'claudeUsage' | 'codexUsage' | 'openCodeUsage' | 'museUsage'
@@ -6,24 +9,37 @@ type UsageProviderApi = PreloadApi[UsageProviderApiKey]
type UsageRangeArgs = { scope: string; range: string }
export function createUsageProviderApi<Key extends UsageProviderApiKey>(
ipc: Pick<IpcRenderer, 'invoke'>,
ipc: {
invoke(channel: string, args?: unknown, purpose?: BackendConnectionPurpose): Promise<unknown>
onConnectionChanged?: (listener: BackendConnectionListener) => () => void
},
prefix: Key
): PreloadApi[Key]
export function createUsageProviderApi(
ipc: Pick<IpcRenderer, 'invoke'>,
ipc: {
invoke(channel: string, args?: unknown, purpose?: BackendConnectionPurpose): Promise<unknown>
onConnectionChanged?: (listener: BackendConnectionListener) => () => void
},
prefix: UsageProviderApiKey
): UsageProviderApi {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The prefix selects its typed service methods; only the transport boundary erases their return types.
return {
getScanState: () => ipc.invoke(`${prefix}:getScanState`),
...(ipc.onConnectionChanged ? { onConnectionChanged: ipc.onConnectionChanged } : {}),
getScanState: (purpose?: BackendConnectionPurpose) =>
purpose
? ipc.invoke(`${prefix}:getScanState`, undefined, purpose)
: ipc.invoke(`${prefix}:getScanState`),
setEnabled: (args: { enabled: boolean }) => ipc.invoke(`${prefix}:setEnabled`, args),
refresh: (args?: { force?: boolean }) => ipc.invoke(`${prefix}:refresh`, args),
getSnapshot: (args: UsageRangeArgs & { limit?: number }) =>
ipc.invoke(`${prefix}:getSnapshot`, args),
getSnapshot: (args: UsageRangeArgs & { limit?: number }, purpose?: BackendConnectionPurpose) =>
purpose
? ipc.invoke(`${prefix}:getSnapshot`, args, purpose)
: ipc.invoke(`${prefix}:getSnapshot`, args),
getSummary: (args: UsageRangeArgs) => ipc.invoke(`${prefix}:getSummary`, args),
getDaily: (args: UsageRangeArgs) => ipc.invoke(`${prefix}:getDaily`, args),
getBreakdown: (args: UsageRangeArgs & { kind: string }) =>
ipc.invoke(`${prefix}:getBreakdown`, args),
getRecentSessions: (args: UsageRangeArgs & { limit?: number }) =>
ipc.invoke(`${prefix}:getRecentSessions`, args)
}
} as UsageProviderApi
}