mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-01 23:41:40 +00:00
The 5-minute reconciliation read the multi-GB OpenCode store on the hub's event loop with bun:sqlite; a cold scan froze every request for ~88s and stalled each machine's runner the same way. - scan in a spawned `hapi usage-scan` child process (stdin/stdout JSON) - refresh on demand from the usage page (debounced) + a daily fallback - a runner whose hub is localhost skips the store the hub already owns - machine scans run every 30min and read the message table sequentially in one pass instead of per-session indexed lookups
63 lines
2.6 KiB
TypeScript
63 lines
2.6 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import type { CommandDefinition } from './types'
|
|
|
|
/**
|
|
* Internal command: aggregate OpenCode token usage.
|
|
*
|
|
* The hub and the runner spawn `hapi usage-scan` as a child process so the
|
|
* synchronous bun:sqlite reads over the multi-GB OpenCode store never block
|
|
* their event loops (a cold scan used to freeze the hub for a minute or
|
|
* more). The request arrives as JSON on stdin, the result leaves as JSON on
|
|
* stdout: `{ dbPath, sessionIds }` for known sessions, or
|
|
* `{ dbPath, listAll: true }` to scan every session found in the store.
|
|
*/
|
|
|
|
export type UsageScanRequest = {
|
|
dbPath: string
|
|
sessionIds?: string[]
|
|
listAll?: boolean
|
|
}
|
|
|
|
function readStdin(): string {
|
|
// fd 0 works for both pipe and file-backed stdin; Bun's async iteration
|
|
// over a file-backed stdin yields nothing.
|
|
return readFileSync(0, 'utf8')
|
|
}
|
|
|
|
export function parseUsageScanRequest(raw: string): UsageScanRequest {
|
|
const parsed = JSON.parse(raw) as { dbPath?: unknown; sessionIds?: unknown; listAll?: unknown }
|
|
if (typeof parsed.dbPath !== 'string') {
|
|
throw new Error('usage-scan expects { dbPath: string, sessionIds?: string[], listAll?: boolean } on stdin')
|
|
}
|
|
if (parsed.listAll === true) {
|
|
return { dbPath: parsed.dbPath, listAll: true }
|
|
}
|
|
if (!Array.isArray(parsed.sessionIds)) {
|
|
throw new Error('usage-scan expects sessionIds or listAll: true on stdin')
|
|
}
|
|
const sessionIds = parsed.sessionIds.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
return { dbPath: parsed.dbPath, sessionIds }
|
|
}
|
|
|
|
/** Payload shape the hub/runner parse (see hub/src/sync/usageReconciliation.ts). */
|
|
export async function runUsageScan(request: UsageScanRequest): Promise<{ results: Record<string, { rows: unknown[]; messages: number }> }> {
|
|
const { readOpencodeUsageForAllSessions, readOpencodeUsageForSessions } = await import('@hapi/protocol/opencodeUsage')
|
|
const usage = request.listAll === true
|
|
? await readOpencodeUsageForAllSessions(request.dbPath)
|
|
: await readOpencodeUsageForSessions(request.dbPath, request.sessionIds ?? [])
|
|
const results: Record<string, { rows: unknown[]; messages: number }> = {}
|
|
for (const [sessionId, entry] of usage) {
|
|
results[sessionId] = { rows: entry.rows, messages: entry.messages }
|
|
}
|
|
return { results }
|
|
}
|
|
|
|
export const usageScanCommand: CommandDefinition = {
|
|
name: 'usage-scan',
|
|
requiresRuntimeAssets: false,
|
|
run: async () => {
|
|
const request = parseUsageScanRequest(readStdin())
|
|
process.stdout.write(JSON.stringify(await runUsageScan(request)))
|
|
}
|
|
}
|