fix(usage): bound terminal event persistence

Add end-to-end terminal admission, bounded database fallback, and observable overload handling. Preserve first-byte lifecycle state across asynchronous runtime and frontend updates.
This commit is contained in:
elky
2026-07-16 13:12:30 +08:00
parent c6d373e6aa
commit 9a47267545
10 changed files with 1952 additions and 447 deletions
@@ -1,7 +1,10 @@
import { describe, expect, it } from 'vitest'
import type { UsageRecord } from '../../types'
import { syncUsageRecordStreamResolution } from '../recordSync'
import {
mergeUsageRecordFirstByteTimeMs,
syncUsageRecordStreamResolution,
} from '../recordSync'
function buildUsageRecord(overrides: Partial<UsageRecord> = {}): UsageRecord {
return {
@@ -59,3 +62,16 @@ describe('syncUsageRecordStreamResolution', () => {
expect(nextRecords).toBe(records)
})
})
describe('mergeUsageRecordFirstByteTimeMs', () => {
it('does not let a stale active update clear or reduce a resolved first-byte time', () => {
expect(mergeUsageRecordFirstByteTimeMs(500, null)).toBe(500)
expect(mergeUsageRecordFirstByteTimeMs(500, 320)).toBe(500)
expect(mergeUsageRecordFirstByteTimeMs(500, 640)).toBe(640)
expect(mergeUsageRecordFirstByteTimeMs(undefined, 320)).toBe(320)
expect(mergeUsageRecordFirstByteTimeMs(undefined, 0)).toBe(0)
expect(mergeUsageRecordFirstByteTimeMs(0, null)).toBe(0)
expect(mergeUsageRecordFirstByteTimeMs(null, -1)).toBeNull()
expect(mergeUsageRecordFirstByteTimeMs(-1, null)).toBeUndefined()
})
})
@@ -78,6 +78,11 @@ describe('usage status helpers', () => {
status: 'streaming',
first_byte_time_ms: 320,
}))).toBe('streaming')
expect(resolveDisplayRequestStatus(buildUsageRecord({
status: 'streaming',
first_byte_time_ms: 0,
}))).toBe('streaming')
})
it('treats active lifecycle records with failure signals as failed for display', () => {
@@ -5,6 +5,30 @@ export type UsageRecordStreamResolution = Pick<
'id' | 'is_stream' | 'upstream_is_stream' | 'client_requested_stream' | 'client_is_stream'
>
export function mergeUsageRecordFirstByteTimeMs(
existingValue: number | null | undefined,
nextValue: number | null | undefined
): number | null | undefined {
// Millisecond timing is floored, so 0 still means the first byte was observed.
const existingIsResolved = typeof existingValue === 'number' &&
Number.isFinite(existingValue) &&
existingValue >= 0
const nextIsResolved = typeof nextValue === 'number' &&
Number.isFinite(nextValue) &&
nextValue >= 0
if (existingIsResolved && nextIsResolved) {
return Math.max(existingValue, nextValue)
}
if (existingIsResolved) {
return existingValue
}
if (nextIsResolved) {
return nextValue
}
return existingValue == null ? existingValue : undefined
}
export function syncUsageRecordStreamResolution(
records: UsageRecord[],
resolved: UsageRecordStreamResolution