Merge pull request #839 from getagentseal/phase8/stateful-antigravity

refactor(core): antigravity decode into core, stitching and caches host-side (phase 8, stateful tier)
This commit is contained in:
Resham Joshi 2026-07-27 01:09:08 -07:00 committed by GitHub
commit be00eed4e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 2792 additions and 624 deletions

View file

@ -8,6 +8,24 @@ import https from 'https'
import { isSqliteAvailable, isSqliteBusyError, openDatabase } from '../sqlite.js'
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
import type { DecodeContext } from '@codeburn/core'
import {
decodeAntigravityGenMetadata,
decodeAntigravityGeneratorMetadata,
decodeAntigravityStatusLine,
parseAntigravityStatusLinePayload,
extractAntigravityModelMap,
extractAntigravityGeneratorMetadata,
} from '@codeburn/core/providers/antigravity'
import type {
AntigravityDecodedCall,
AntigravityGeneratorMetadata,
AntigravityModelMap,
} from '@codeburn/core/providers/antigravity'
// Moved into @codeburn/core (the decoders use them). Re-exported so existing
// importers keep resolving them from this module.
export { extractAntigravityModelMap, extractAntigravityGeneratorMetadata }
type AntigravityConversationRoot = {
dir: string
@ -61,74 +79,7 @@ type ServerCandidate = ServerInfo & {
appDataDir?: 'antigravity' | 'antigravity-cli' | 'antigravity-ide'
}
type ModelMap = Record<string, string>
type UsageEntry = {
model: string
inputTokens: string
outputTokens: string
thinkingOutputTokens?: string
responseOutputTokens?: string
apiProvider: string
responseId?: string
}
export type GeneratorMetadata = {
stepIndices?: number[]
chatModel?: {
model: string
usage: UsageEntry
chatStartMetadata?: {
createdAt?: string
}
}
}
type ModelMapResponse = {
models?: Record<string, { model?: string; displayName?: string }>
response?: {
models?: Record<string, { model?: string; displayName?: string }>
}
}
type GeneratorMetadataResponse = {
generatorMetadata?: GeneratorMetadata[]
response?: {
generatorMetadata?: GeneratorMetadata[]
}
}
type StatusLineCurrentUsage = {
input_tokens?: number
output_tokens?: number
cache_creation_input_tokens?: number
cache_read_input_tokens?: number
}
type StatusLinePayload = {
conversation_id?: string
session_id?: string
model?: string | {
id?: string
display_name?: string
}
context_window?: {
current_usage?: StatusLineCurrentUsage | null
}
}
type StatusLineEvent = {
at: string
conversationId: string
sessionId?: string
model: string
usage: {
inputTokens: number
outputTokens: number
cacheCreationInputTokens: number
cacheReadInputTokens: number
}
}
type ModelMap = AntigravityModelMap
type CachedCascade = {
mtimeMs: number
@ -141,29 +92,11 @@ type AntigravityCache = {
cascades: Record<string, CachedCascade>
}
type ProtoField = {
number: number
wireType: number
value?: bigint
bytes?: Uint8Array
}
type ProtoVarint = {
value: bigint
offset: number
}
type AntigravityGenMetadataRow = {
idx: number
data: Uint8Array | string
}
const cachedServers = new Map<string, ServerInfo | null>()
const cachedModelMaps = new Map<string, ModelMap>()
let memCache: AntigravityCache | null = null
let cacheDirty = false
let httpsAgent: https.Agent | undefined
const protoTextDecoder = new TextDecoder('utf-8', { fatal: false })
const SERVER_PORT_FLAGS = ['https_server_port', 'extension_server_port', 'https-server-port', 'extension-server-port']
const CSRF_TOKEN_FLAGS = ['csrf_token', 'extension_server_csrf_token', 'csrf-token', 'extension-server-csrf-token']
@ -259,69 +192,6 @@ function parseAntigravityServerCandidates(lines: string[]): ServerCandidate[] {
.filter((server): server is ServerCandidate => server !== null)
}
// Antigravity's own model-map config sometimes hasn't caught up with a new
// model yet, so both the config key and displayName can still be the raw
// placeholder id (e.g. "MODEL_PLACEHOLDER_M26"). Falling through to that
// value as the "canonical" model would leak an internal placeholder as a
// model name; 'unknown' is what this file already uses when no model can be
// resolved at all (see antigravitySqliteModel).
const MODEL_PLACEHOLDER_PATTERN = /^MODEL_PLACEHOLDER_/
function dropPlaceholderModelId(model: string): string {
return MODEL_PLACEHOLDER_PATTERN.test(model) ? 'unknown' : model
}
function getCanonicalModelId(key: string, displayName?: string): string {
if (displayName) {
const lower = displayName.toLowerCase()
if (lower.includes('3.5 flash')) {
if (lower.includes('high')) return 'gemini-3.5-flash-high'
if (lower.includes('medium')) return 'gemini-3.5-flash-medium'
if (lower.includes('low')) return 'gemini-3.5-flash-low'
return 'gemini-3.5-flash'
}
if (lower.includes('3.1 pro')) {
if (lower.includes('high')) return 'gemini-3.1-pro-high'
if (lower.includes('low')) return 'gemini-3.1-pro-low'
return 'gemini-3.1-pro'
}
if (lower.includes('3.1 flash')) {
if (lower.includes('image')) return 'gemini-3.1-flash-image'
if (lower.includes('lite')) return 'gemini-3.1-flash-lite'
return 'gemini-3.1-flash'
}
if (lower.includes('3 flash')) {
return 'gemini-3-flash'
}
if (lower.includes('3 pro')) {
return 'gemini-3-pro'
}
}
return dropPlaceholderModelId(key)
}
export function extractAntigravityModelMap(resp: unknown): ModelMap {
if (!resp || typeof resp !== 'object') return {}
const data = resp as ModelMapResponse
const models = data.response?.models ?? data.models
const map = new Map<string, string>()
if (!models) return {}
for (const [key, info] of Object.entries(models)) {
if (info && typeof info === 'object' && typeof info.model === 'string') {
const canonicalKey = getCanonicalModelId(key, info.displayName)
map.set(info.model, canonicalKey)
}
}
return Object.fromEntries(map)
}
export function extractAntigravityGeneratorMetadata(resp: unknown): GeneratorMetadata[] {
if (!resp || typeof resp !== 'object') return []
const data = resp as GeneratorMetadataResponse
const metadata = data.response?.generatorMetadata ?? data.generatorMetadata
return Array.isArray(metadata) ? metadata : []
}
async function loadCache(): Promise<AntigravityCache> {
if (memCache) return memCache
try {
@ -578,234 +448,40 @@ function normalizePricingModel(model: string): string {
return PRICING_ALIASES[stripped] ?? stripped
}
function readProtoVarint(data: Uint8Array, startOffset: number): ProtoVarint | null {
let value = 0n
let shift = 0n
let offset = startOffset
while (offset < data.length) {
const byte = BigInt(data[offset]!)
offset += 1
value |= (byte & 0x7fn) << shift
if ((byte & 0x80n) === 0n) return { value, offset }
shift += 7n
if (shift > 70n) return null
}
return null
function decodeContext(sourceRef: string): DecodeContext {
return { privacyKey: '', providerId: 'antigravity', sourceRef }
}
function parseProtoFields(data: Uint8Array): ProtoField[] {
const fields: ProtoField[] = []
let offset = 0
while (offset < data.length) {
const key = readProtoVarint(data, offset)
if (!key) break
offset = key.offset
const fieldNumber = Number(key.value >> 3n)
const wireType = Number(key.value & 0x7n)
if (!Number.isSafeInteger(fieldNumber) || fieldNumber <= 0) break
if (wireType === 0) {
const value = readProtoVarint(data, offset)
if (!value) break
fields.push({ number: fieldNumber, wireType, value: value.value })
offset = value.offset
continue
}
if (wireType === 1) {
if (offset + 8 > data.length) break
fields.push({ number: fieldNumber, wireType, bytes: data.subarray(offset, offset + 8) })
offset += 8
continue
}
if (wireType === 2) {
const length = readProtoVarint(data, offset)
if (!length) break
offset = length.offset
const byteLength = Number(length.value)
if (!Number.isSafeInteger(byteLength) || byteLength < 0 || offset + byteLength > data.length) break
fields.push({ number: fieldNumber, wireType, bytes: data.subarray(offset, offset + byteLength) })
offset += byteLength
continue
}
if (wireType === 5) {
if (offset + 4 > data.length) break
fields.push({ number: fieldNumber, wireType, bytes: data.subarray(offset, offset + 4) })
offset += 4
continue
}
break
}
return fields
}
function firstProtoField(fields: readonly ProtoField[], fieldNumber: number): ProtoField | undefined {
return fields.find(field => field.number === fieldNumber)
}
function protoFieldText(field: ProtoField | undefined): string | undefined {
if (!field?.bytes || field.bytes.length === 0) return undefined
const text = protoTextDecoder.decode(field.bytes)
if (!text || /[\u0000-\u0008\u000E-\u001F\u007F\uFFFD]/.test(text)) return undefined
return text
}
function protoFieldPositiveInteger(field: ProtoField | undefined): number {
if (field?.value === undefined) return 0
const value = Number(field.value)
return Number.isSafeInteger(value) && value > 0 ? value : 0
}
function protoFieldBytes(field: ProtoField | undefined): Uint8Array | undefined {
return field?.bytes
}
function isAntigravityResponseId(value: string): boolean {
return /^[^\s]+$/.test(value)
}
function antigravitySqliteResponseId(usageFields: readonly ProtoField[], fallback: string): string {
const responseId = protoFieldText(firstProtoField(usageFields, 11))
return responseId && isAntigravityResponseId(responseId) ? responseId : fallback
}
function genMetadataDataBytes(value: Uint8Array | string): Uint8Array {
return typeof value === 'string'
? new TextEncoder().encode(value)
: value
}
function antigravitySqliteMetadataAttributes(chatFields: readonly ProtoField[]): Map<string, string> {
const attributes = new Map<string, string>()
for (const field of chatFields) {
if (field.number !== 20) continue
const pairFields = parseProtoFields(protoFieldBytes(field) ?? new Uint8Array())
const key = protoFieldText(firstProtoField(pairFields, 1))
const value = protoFieldText(firstProtoField(pairFields, 2))
if (key && value) attributes.set(key, value)
}
return attributes
}
function antigravitySqliteModel(chatFields: readonly ProtoField[]): string {
const attributes = antigravitySqliteMetadataAttributes(chatFields)
const displayName = protoFieldText(firstProtoField(chatFields, 21))
const rawModel = protoFieldText(firstProtoField(chatFields, 19))
?? attributes.get('model_enum')
?? displayName
?? 'unknown'
return getCanonicalModelId(rawModel, displayName)
}
// Decode a proto field that carries a time into an ISO-8601 string. Antigravity
// may encode ChatStartMetadata.created_at as an ISO string, a Timestamp
// submessage (seconds in field 1), or a bare unix varint. Returns '' when the
// field is absent or unparseable so the caller can fall back.
function protoTimestampToIso(field: ProtoField | undefined): string {
if (!field) return ''
const text = protoFieldText(field)
if (text && !Number.isNaN(Date.parse(text))) return new Date(text).toISOString()
if (field.bytes) {
// google.protobuf.Timestamp submessage: seconds (#1), nanos (#2).
const tsFields = parseProtoFields(field.bytes)
const seconds = firstProtoField(tsFields, 1)?.value
if (seconds !== undefined) {
const nanos = firstProtoField(tsFields, 2)?.value ?? 0n
const ms = Number(seconds) * 1000 + Math.floor(Number(nanos) / 1e6)
if (Number.isSafeInteger(ms) && ms > 0) return new Date(ms).toISOString()
}
}
if (field.value !== undefined) {
const raw = Number(field.value)
const ms = raw < 1e12 ? raw * 1000 : raw
if (Number.isSafeInteger(ms) && ms > 0) return new Date(ms).toISOString()
}
return ''
}
// ChatStartMetadata lives at chatModel(#1).#9; its created_at is #4. Not every
// gen_metadata row carries it, so this returns '' when missing.
function antigravitySqliteCreatedAt(chatFields: readonly ProtoField[]): string {
const metadataBytes = protoFieldBytes(firstProtoField(chatFields, 9))
if (!metadataBytes) return ''
return protoTimestampToIso(firstProtoField(parseProtoFields(metadataBytes), 4))
}
function buildCallFromSqliteGenMetadataRow(cascadeId: string, row: AntigravityGenMetadataRow): ParsedProviderCall | null {
const rootFields = parseProtoFields(genMetadataDataBytes(row.data))
const chatFields = parseProtoFields(protoFieldBytes(firstProtoField(rootFields, 1)) ?? new Uint8Array())
const usageFields = parseProtoFields(protoFieldBytes(firstProtoField(chatFields, 4)) ?? new Uint8Array())
if (usageFields.length === 0) return null
const inputTokens = protoFieldPositiveInteger(firstProtoField(usageFields, 2))
|| protoFieldPositiveInteger(firstProtoField(usageFields, 1))
const totalOutputTokens = protoFieldPositiveInteger(firstProtoField(usageFields, 3))
let responseTokens = protoFieldPositiveInteger(firstProtoField(usageFields, 9))
let thinkingTokens = protoFieldPositiveInteger(firstProtoField(usageFields, 10))
if (responseTokens === 0 && thinkingTokens === 0) {
responseTokens = totalOutputTokens
} else if (totalOutputTokens > 0 && responseTokens + thinkingTokens !== totalOutputTokens) {
const adjustedResponseTokens = totalOutputTokens - thinkingTokens
if (adjustedResponseTokens >= 0) responseTokens = adjustedResponseTokens
}
if (inputTokens === 0 && totalOutputTokens === 0) return null
const responseId = antigravitySqliteResponseId(usageFields, String(row.idx))
const model = antigravitySqliteModel(chatFields)
const pricingModel = normalizePricingModel(model)
// Exported so the RPC-arm golden can assert the exact emitted shape without a
// live language server; every emit arm goes through this one mapper.
export function toProviderCall(rich: AntigravityDecodedCall, project?: string): ParsedProviderCall {
return {
provider: 'antigravity',
model,
inputTokens,
outputTokens: responseTokens,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: thinkingTokens,
webSearchRequests: 0,
model: rich.model,
inputTokens: rich.inputTokens,
outputTokens: rich.outputTokens,
cacheCreationInputTokens: rich.cacheCreationInputTokens,
cacheReadInputTokens: rich.cacheReadInputTokens,
cachedInputTokens: rich.cachedInputTokens,
reasoningTokens: rich.reasoningTokens,
webSearchRequests: rich.webSearchRequests,
// Whitelisted for cost persistence: the pass prices `pricingModel` (which
// strips suffixes / applies aliases) and the result is stored verbatim, so
// the display `model` never reaches the price table. reasoning is billed at
// the output rate (outputTokens + reasoningTokens), matching the lift.
costBasis: 'estimated',
pricingModel,
pricingModel: normalizePricingModel(rich.model),
tools: [],
bashCommands: [],
timestamp: antigravitySqliteCreatedAt(chatFields),
speed: 'standard',
deduplicationKey: `antigravity:${cascadeId}:${responseId}`,
timestamp: rich.timestamp,
speed: rich.speed,
deduplicationKey: rich.deduplicationKey,
userMessage: '',
sessionId: cascadeId,
sessionId: rich.sessionId,
...(project !== undefined ? { project } : {}),
}
}
function buildCallsFromSqliteGenMetadata(cascadeId: string, rows: AntigravityGenMetadataRow[]): ParsedProviderCall[] {
const calls: ParsedProviderCall[] = []
const seenResponseIds = new Set<string>()
for (const row of rows) {
const call = buildCallFromSqliteGenMetadataRow(cascadeId, row)
if (!call) continue
if (seenResponseIds.has(call.deduplicationKey)) continue
seenResponseIds.add(call.deduplicationKey)
calls.push(call)
}
return calls
}
async function parseSqliteGenMetadataCalls(filePath: string, cascadeId: string): Promise<ParsedProviderCall[]> {
if (!filePath.toLowerCase().endsWith('.db')) return []
if (!isSqliteAvailable()) return []
@ -813,8 +489,9 @@ async function parseSqliteGenMetadataCalls(filePath: string, cascadeId: string):
let db: ReturnType<typeof openDatabase> | null = null
try {
db = openDatabase(filePath)
const rows = db.query<AntigravityGenMetadataRow>('SELECT idx, data FROM gen_metadata ORDER BY idx')
return buildCallsFromSqliteGenMetadata(cascadeId, rows)
const rows = db.query<{ idx: number; data: Uint8Array | string }>('SELECT idx, data FROM gen_metadata ORDER BY idx')
const { calls } = decodeAntigravityGenMetadata({ records: rows, context: decodeContext(filePath), cascadeId })
return calls.map(c => toProviderCall(c))
} catch (err) {
// Let a transient lock propagate so the run retries this file on the next
// refresh instead of treating it as empty (see parser.ts busy handling).
@ -825,107 +502,10 @@ async function parseSqliteGenMetadataCalls(filePath: string, cascadeId: string):
}
}
function parseFiniteToken(value: unknown): number {
return typeof value === 'number' && Number.isFinite(value) && value > 0
? Math.floor(value)
: 0
}
function usageSignature(event: StatusLineEvent): string {
const u = event.usage
return [
event.model,
u.inputTokens,
u.outputTokens,
u.cacheCreationInputTokens,
u.cacheReadInputTokens,
].join(':')
}
function usageHasTokens(usage: StatusLineEvent['usage']): boolean {
return (
usage.inputTokens > 0 ||
usage.outputTokens > 0 ||
usage.cacheCreationInputTokens > 0 ||
usage.cacheReadInputTokens > 0
)
}
function usageIsMonotonic(current: StatusLineEvent['usage'], previous: StatusLineEvent['usage']): boolean {
return (
current.inputTokens >= previous.inputTokens &&
current.outputTokens >= previous.outputTokens &&
current.cacheCreationInputTokens >= previous.cacheCreationInputTokens &&
current.cacheReadInputTokens >= previous.cacheReadInputTokens
)
}
function usageDelta(current: StatusLineEvent['usage'], previous: StatusLineEvent['usage']): StatusLineEvent['usage'] {
return {
inputTokens: current.inputTokens - previous.inputTokens,
outputTokens: current.outputTokens - previous.outputTokens,
cacheCreationInputTokens: current.cacheCreationInputTokens - previous.cacheCreationInputTokens,
cacheReadInputTokens: current.cacheReadInputTokens - previous.cacheReadInputTokens,
}
}
export function antigravityCascadeIdFromPath(path: string): string {
return basename(path).replace(/\.(pb|db)$/i, '')
}
function buildCallsFromGeneratorMetadata(
cascadeId: string,
metadata: GeneratorMetadata[],
modelMap: ModelMap,
): ParsedProviderCall[] {
const results: ParsedProviderCall[] = []
for (let i = 0; i < metadata.length; i++) {
const entry = metadata[i]!
const usage = entry.chatModel?.usage
if (!usage) continue
const inputTokens = parseInt(usage.inputTokens ?? '0', 10)
const outputTokens = parseInt(usage.outputTokens ?? '0', 10)
const thinkingTokens = parseInt(usage.thinkingOutputTokens ?? '0', 10)
const responseTokens = parseInt(usage.responseOutputTokens ?? '0', 10)
if (inputTokens === 0 && outputTokens === 0) continue
const responseId = usage.responseId || String(i)
const dedupKey = `antigravity:${cascadeId}:${responseId}`
const model = dropPlaceholderModelId(modelMap[usage.model] ?? usage.model)
const pricingModel = normalizePricingModel(model)
const timestamp = entry.chatModel?.chatStartMetadata?.createdAt ?? ''
results.push({
provider: 'antigravity',
model,
inputTokens,
outputTokens: responseTokens,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: thinkingTokens,
webSearchRequests: 0,
// See buildCallFromSqliteGenMetadataRow: pricing pass prices `pricingModel`
// and the whitelist persists the result verbatim.
costBasis: 'estimated',
pricingModel,
tools: [],
bashCommands: [],
timestamp,
speed: 'standard',
deduplicationKey: dedupKey,
userMessage: '',
sessionId: cascadeId,
})
}
return results
}
function isConversationFile(file: string, extensions: readonly string[]): boolean {
const lowerFile = file.toLowerCase()
return extensions.some(ext => lowerFile.endsWith(ext))
@ -979,38 +559,8 @@ export async function discoverAntigravitySessionSources(
return sources
}
function parseStatusLinePayload(input: unknown): StatusLineEvent | null {
if (!input || typeof input !== 'object') return null
const payload = input as StatusLinePayload
if (typeof payload.conversation_id !== 'string' || payload.conversation_id.length === 0) return null
const usage = payload.context_window?.current_usage
if (!usage) return null
const event: StatusLineEvent = {
at: new Date().toISOString(),
conversationId: payload.conversation_id,
sessionId: typeof payload.session_id === 'string' ? payload.session_id : undefined,
model: typeof payload.model === 'string'
? payload.model
: payload.model?.id ?? payload.model?.display_name ?? 'unknown',
usage: {
inputTokens: parseFiniteToken(usage.input_tokens),
outputTokens: parseFiniteToken(usage.output_tokens),
cacheCreationInputTokens: parseFiniteToken(usage.cache_creation_input_tokens),
cacheReadInputTokens: parseFiniteToken(usage.cache_read_input_tokens),
},
}
const u = event.usage
if (u.inputTokens === 0 && u.outputTokens === 0 && u.cacheCreationInputTokens === 0 && u.cacheReadInputTokens === 0) {
return null
}
if (event.model === 'unknown') return null
return event
}
export async function recordAntigravityStatusLinePayload(input: unknown): Promise<boolean> {
const event = parseStatusLinePayload(input)
const event = parseAntigravityStatusLinePayload(input, new Date().toISOString())
if (!event) return false
const path = getAntigravityStatusLineEventsPath()
@ -1024,128 +574,14 @@ export async function recordAntigravityStatusLinePayload(input: unknown): Promis
return true
}
function parseStatusLineEvent(input: unknown): StatusLineEvent | null {
if (!input || typeof input !== 'object') return null
const event = input as StatusLineEvent
if (typeof event.at !== 'string' || Number.isNaN(new Date(event.at).getTime())) return null
if (typeof event.conversationId !== 'string' || event.conversationId.length === 0) return null
if (typeof event.model !== 'string' || event.model.length === 0) return null
if (!event.usage || typeof event.usage !== 'object') return null
const usage = {
inputTokens: parseFiniteToken(event.usage.inputTokens),
outputTokens: parseFiniteToken(event.usage.outputTokens),
cacheCreationInputTokens: parseFiniteToken(event.usage.cacheCreationInputTokens),
cacheReadInputTokens: parseFiniteToken(event.usage.cacheReadInputTokens),
}
if (
usage.inputTokens === 0 &&
usage.outputTokens === 0 &&
usage.cacheCreationInputTokens === 0 &&
usage.cacheReadInputTokens === 0
) return null
return {
at: event.at,
conversationId: event.conversationId,
sessionId: typeof event.sessionId === 'string' ? event.sessionId : undefined,
model: event.model,
usage,
}
}
function hasRpcCacheForConversation(seenKeys: Set<string>, conversationId: string): boolean {
const prefix = `antigravity:${conversationId}:`
for (const key of seenKeys) {
if (key.startsWith(prefix)) return true
}
return false
}
async function parseStatusLineCalls(source: SessionSource, seenKeys: Set<string>): Promise<ParsedProviderCall[]> {
const raw = await readFile(source.path, 'utf-8').catch(() => '')
const runsByConversation = new Map<string, Array<{ event: StatusLineEvent; signature: string; count: number }>>()
for (const line of raw.split(/\r?\n/)) {
if (!line.trim()) continue
let parsed: unknown
try {
parsed = JSON.parse(line)
} catch {
continue
}
const event = parseStatusLineEvent(parsed)
if (!event) continue
if (hasRpcCacheForConversation(seenKeys, event.conversationId)) continue
const signature = usageSignature(event)
const runs = runsByConversation.get(event.conversationId) ?? []
const lastRun = runs.at(-1)
if (lastRun?.signature === signature) {
lastRun.count += 1
lastRun.event = event
} else {
runs.push({ event, signature, count: 1 })
runsByConversation.set(event.conversationId, runs)
}
}
const results: ParsedProviderCall[] = []
for (const runs of runsByConversation.values()) {
let turnIndex = 0
let previousSnapshotUsage: StatusLineEvent['usage'] | null = null
for (let i = 0; i < runs.length; i++) {
const run = runs[i]!
const isLastRun = i === runs.length - 1
if (run.count === 1 && !isLastRun) continue
const event = run.event
const signature = run.signature
const billableUsage = previousSnapshotUsage && usageIsMonotonic(event.usage, previousSnapshotUsage)
? usageDelta(event.usage, previousSnapshotUsage)
: event.usage
previousSnapshotUsage = event.usage
if (!usageHasTokens(billableUsage)) continue
const dedupKey = `antigravity-statusline:${event.conversationId}:${turnIndex}:${signature}`
turnIndex += 1
if (seenKeys.has(dedupKey)) continue
const u = billableUsage
results.push({
provider: 'antigravity',
model: event.model,
inputTokens: u.inputTokens,
outputTokens: u.outputTokens,
cacheCreationInputTokens: u.cacheCreationInputTokens,
cacheReadInputTokens: u.cacheReadInputTokens,
cachedInputTokens: 0,
// StatusLine current_usage exposes aggregate output tokens, not a
// separate thinking/response split. Preserve the exact total instead
// of inventing a breakdown.
reasoningTokens: 0,
webSearchRequests: 0,
// Pass prices `pricingModel` (reasoningTokens is 0 here, so the output
// basis is exactly u.outputTokens); whitelist persists the result.
costBasis: 'estimated',
pricingModel: normalizePricingModel(event.model),
tools: [],
bashCommands: [],
timestamp: event.at,
speed: 'standard',
deduplicationKey: dedupKey,
userMessage: '',
sessionId: event.conversationId,
project: source.project,
})
}
}
return results
const { calls } = decodeAntigravityStatusLine({
records: raw.split(/\r?\n/),
context: decodeContext(source.path),
seenKeys,
})
return calls.map(c => toProviderCall(c, source.project))
}
export function shouldReparseAntigravitySource(path: string, cachedTurnCount: number): boolean {
@ -1163,7 +599,7 @@ async function findCascadeSource(cascadeId: string): Promise<SessionSource | nul
}
export async function snapshotAntigravityStatusLinePayload(input: unknown): Promise<boolean> {
const event = parseStatusLinePayload(input)
const event = parseAntigravityStatusLinePayload(input, new Date().toISOString())
if (!event) return false
const cascadeId = event.conversationId
@ -1182,13 +618,18 @@ export async function snapshotAntigravityStatusLinePayload(input: unknown): Prom
const server = await detectServer(antigravityAppDataDirFromSourcePath(source.path))
if (!server) return false
let metadata: GeneratorMetadata[]
let metadata: AntigravityGeneratorMetadata[]
try {
const modelMap = await getModelMap(server)
metadata = extractAntigravityGeneratorMetadata(
await rpc(server, 'GetCascadeTrajectoryGeneratorMetadata', { cascadeId }),
)
const snapshotCalls = buildCallsFromGeneratorMetadata(cascadeId, metadata, modelMap)
const snapshotCalls = decodeAntigravityGeneratorMetadata({
records: metadata,
context: decodeContext(source.path),
cascadeId,
modelMap,
}).calls.map(c => toProviderCall(c))
assignStableTimestamps(snapshotCalls, cached?.calls, new Date(s.mtimeMs).toISOString())
cache.cascades[cascadeId] = {
mtimeMs: s.mtimeMs,
@ -1253,13 +694,13 @@ function applyAntigravityProject(call: ParsedProviderCall, source: SessionSource
call.project = source.project
}
// gen_metadata rows and RPC entries without a real ChatStartMetadata.created_at
// carry no per-call timestamp. Left empty, those calls are dropped by the
// date-range filters in parser.ts (`if (!callTs) continue`), so each needs a
// fallback. The fallback must be *stable* across file rewrites: the generic
// session-cache persists whatever timestamp is emitted, and a non-durable
// source is cleared and reparsed whenever its mtime changes, so stamping the
// current mtime on every reparse would retro-date the whole session forward.
// Sqlite rows and RPC entries without a real created_at timestamp carry no
// per-call timestamp. Left empty, those calls are dropped by the date-range
// filters in parser.ts (`if (!callTs) continue`), so each needs a fallback.
// The fallback must be *stable* across file rewrites: the generic session-cache
// persists whatever timestamp is emitted, and a non-durable source is cleared
// and reparsed whenever its mtime changes, so stamping the current mtime on
// every reparse would retro-date the whole session forward.
//
// assignStableTimestamps carries forward the timestamp already recorded for a
// dedup key (its first-seen time, held in the durable Antigravity cache) and
@ -1356,7 +797,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
const modelMap = await getModelMap(server)
let metadata: GeneratorMetadata[]
let metadata: AntigravityGeneratorMetadata[]
try {
metadata = extractAntigravityGeneratorMetadata(
await rpc(server, 'GetCascadeTrajectoryGeneratorMetadata', { cascadeId }),
@ -1373,7 +814,12 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
return
}
const results = buildCallsFromGeneratorMetadata(cascadeId, metadata, modelMap)
const results = decodeAntigravityGeneratorMetadata({
records: metadata,
context: decodeContext(source.path),
cascadeId,
modelMap,
}).calls.map(c => toProviderCall(c))
assignStableTimestamps(results, cached?.calls, fallbackTimestamp)
for (const call of results) {
applyAntigravityProject(call, source, projectPath)

File diff suppressed because it is too large Load diff

View file

@ -154,6 +154,10 @@
"./providers/opencode-session": {
"types": "./dist/providers/opencode-session/index.d.ts",
"import": "./dist/providers/opencode-session/index.js"
},
"./providers/antigravity": {
"types": "./dist/providers/antigravity/index.d.ts",
"import": "./dist/providers/antigravity/index.js"
}
},
"files": [

View file

@ -0,0 +1,583 @@
// @codeburn/core Antigravity decoder: pure decode over three record shapes the
// host hands it: sqlite gen_metadata rows, RPC generatorMetadata entries, and
// statusline JSONL events. No fs / env / clock — the host owns discovery,
// durable caching, project attribution, and pricing.
import type { DecodeContext } from '../../contracts.js'
import type { RecordDiagnostic } from '../../diagnostics.js'
import type {
AntigravityDecodedCall,
AntigravityGeneratorMetadata,
AntigravityGeneratorMetadataResponse,
AntigravityGenMetadataRow,
AntigravityModelMap,
AntigravityModelMapResponse,
AntigravityStatusLineEvent,
AntigravityStatusLinePayload,
ProtoField,
ProtoVarint,
} from './types.js'
export type AntigravityGenMetadataDecodeInput = {
records: unknown[]
context: DecodeContext
cascadeId: string
}
export type AntigravityDecodeResult = {
calls: AntigravityDecodedCall[]
diagnostics: RecordDiagnostic[]
}
export type AntigravityGeneratorMetadataDecodeInput = {
records: unknown[]
context: DecodeContext
cascadeId: string
modelMap: AntigravityModelMap
}
export type AntigravityStatusLineDecodeInput = {
records: unknown[]
context: DecodeContext
seenKeys: ReadonlySet<string>
}
const protoTextDecoder = new TextDecoder('utf-8', { fatal: false })
function readProtoVarint(data: Uint8Array, startOffset: number): ProtoVarint | null {
let value = 0n
let shift = 0n
let offset = startOffset
while (offset < data.length) {
const byte = BigInt(data[offset]!)
offset += 1
value |= (byte & 0x7fn) << shift
if ((byte & 0x80n) === 0n) return { value, offset }
shift += 7n
if (shift > 70n) return null
}
return null
}
function parseProtoFields(data: Uint8Array): ProtoField[] {
const fields: ProtoField[] = []
let offset = 0
while (offset < data.length) {
const key = readProtoVarint(data, offset)
if (!key) break
offset = key.offset
const fieldNumber = Number(key.value >> 3n)
const wireType = Number(key.value & 0x7n)
if (!Number.isSafeInteger(fieldNumber) || fieldNumber <= 0) break
if (wireType === 0) {
const value = readProtoVarint(data, offset)
if (!value) break
fields.push({ number: fieldNumber, wireType, value: value.value })
offset = value.offset
continue
}
if (wireType === 1) {
if (offset + 8 > data.length) break
fields.push({ number: fieldNumber, wireType, bytes: data.subarray(offset, offset + 8) })
offset += 8
continue
}
if (wireType === 2) {
const length = readProtoVarint(data, offset)
if (!length) break
offset = length.offset
const byteLength = Number(length.value)
if (!Number.isSafeInteger(byteLength) || byteLength < 0 || offset + byteLength > data.length) break
fields.push({ number: fieldNumber, wireType, bytes: data.subarray(offset, offset + byteLength) })
offset += byteLength
continue
}
if (wireType === 5) {
if (offset + 4 > data.length) break
fields.push({ number: fieldNumber, wireType, bytes: data.subarray(offset, offset + 4) })
offset += 4
continue
}
break
}
return fields
}
function firstProtoField(fields: readonly ProtoField[], fieldNumber: number): ProtoField | undefined {
return fields.find(field => field.number === fieldNumber)
}
function protoFieldText(field: ProtoField | undefined): string | undefined {
if (!field?.bytes || field.bytes.length === 0) return undefined
const text = protoTextDecoder.decode(field.bytes)
if (!text || /[\u0000-\u0008\u000E-\u001F\u007F\uFFFD]/.test(text)) return undefined
return text
}
function protoFieldPositiveInteger(field: ProtoField | undefined): number {
if (field?.value === undefined) return 0
const value = Number(field.value)
return Number.isSafeInteger(value) && value > 0 ? value : 0
}
function protoFieldBytes(field: ProtoField | undefined): Uint8Array | undefined {
return field?.bytes
}
// Antigravity's own model-map config sometimes hasn't caught up with a new
// model yet, so both the config key and displayName can still be the raw
// placeholder id (e.g. "MODEL_PLACEHOLDER_M26"). Falling through to that
// value as the "canonical" model would leak an internal placeholder as a
// model name; 'unknown' is what the CLI already uses when no model can be
// resolved at all.
const MODEL_PLACEHOLDER_PATTERN = /^MODEL_PLACEHOLDER_/
function dropPlaceholderModelId(model: string): string {
return MODEL_PLACEHOLDER_PATTERN.test(model) ? 'unknown' : model
}
function getCanonicalModelId(key: string, displayName?: string): string {
if (displayName) {
const lower = displayName.toLowerCase()
if (lower.includes('3.5 flash')) {
if (lower.includes('high')) return 'gemini-3.5-flash-high'
if (lower.includes('medium')) return 'gemini-3.5-flash-medium'
if (lower.includes('low')) return 'gemini-3.5-flash-low'
return 'gemini-3.5-flash'
}
if (lower.includes('3.1 pro')) {
if (lower.includes('high')) return 'gemini-3.1-pro-high'
if (lower.includes('low')) return 'gemini-3.1-pro-low'
return 'gemini-3.1-pro'
}
if (lower.includes('3.1 flash')) {
if (lower.includes('image')) return 'gemini-3.1-flash-image'
if (lower.includes('lite')) return 'gemini-3.1-flash-lite'
return 'gemini-3.1-flash'
}
if (lower.includes('3 flash')) {
return 'gemini-3-flash'
}
if (lower.includes('3 pro')) {
return 'gemini-3-pro'
}
}
return dropPlaceholderModelId(key)
}
function isAntigravityResponseId(value: string): boolean {
return /^[^\s]+$/.test(value)
}
function antigravitySqliteResponseId(usageFields: readonly ProtoField[], fallback: string): string {
const responseId = protoFieldText(firstProtoField(usageFields, 11))
return responseId && isAntigravityResponseId(responseId) ? responseId : fallback
}
function genMetadataDataBytes(value: Uint8Array | string): Uint8Array {
return typeof value === 'string'
? new TextEncoder().encode(value)
: value
}
function antigravitySqliteMetadataAttributes(chatFields: readonly ProtoField[]): Map<string, string> {
const attributes = new Map<string, string>()
for (const field of chatFields) {
if (field.number !== 20) continue
const pairFields = parseProtoFields(protoFieldBytes(field) ?? new Uint8Array())
const key = protoFieldText(firstProtoField(pairFields, 1))
const value = protoFieldText(firstProtoField(pairFields, 2))
if (key && value) attributes.set(key, value)
}
return attributes
}
function antigravitySqliteModel(chatFields: readonly ProtoField[]): string {
const attributes = antigravitySqliteMetadataAttributes(chatFields)
const displayName = protoFieldText(firstProtoField(chatFields, 21))
const rawModel = protoFieldText(firstProtoField(chatFields, 19))
?? attributes.get('model_enum')
?? displayName
?? 'unknown'
return getCanonicalModelId(rawModel, displayName)
}
// Decode a proto field that carries a time into an ISO-8601 string. Antigravity
// may encode ChatStartMetadata.created_at as an ISO string, a Timestamp
// submessage (seconds in field 1), or a bare unix varint. Returns '' when the
// field is absent or unparseable so the caller can fall back.
function protoTimestampToIso(field: ProtoField | undefined): string {
if (!field) return ''
const text = protoFieldText(field)
if (text && !Number.isNaN(Date.parse(text))) return new Date(text).toISOString()
if (field.bytes) {
// google.protobuf.Timestamp submessage: seconds (#1), nanos (#2).
const tsFields = parseProtoFields(field.bytes)
const seconds = firstProtoField(tsFields, 1)?.value
if (seconds !== undefined) {
const nanos = firstProtoField(tsFields, 2)?.value ?? 0n
const ms = Number(seconds) * 1000 + Math.floor(Number(nanos) / 1e6)
if (Number.isSafeInteger(ms) && ms > 0) return new Date(ms).toISOString()
}
}
if (field.value !== undefined) {
const raw = Number(field.value)
const ms = raw < 1e12 ? raw * 1000 : raw
if (Number.isSafeInteger(ms) && ms > 0) return new Date(ms).toISOString()
}
return ''
}
// ChatStartMetadata lives at chatModel(#1).#9; its created_at is #4. Not every
// gen_metadata row carries it, so this returns '' when missing.
function antigravitySqliteCreatedAt(chatFields: readonly ProtoField[]): string {
const metadataBytes = protoFieldBytes(firstProtoField(chatFields, 9))
if (!metadataBytes) return ''
return protoTimestampToIso(firstProtoField(parseProtoFields(metadataBytes), 4))
}
function decodeAntigravityGenMetadataRow(
cascadeId: string,
row: AntigravityGenMetadataRow,
): AntigravityDecodedCall | null {
const rootFields = parseProtoFields(genMetadataDataBytes(row.data))
const chatFields = parseProtoFields(protoFieldBytes(firstProtoField(rootFields, 1)) ?? new Uint8Array())
const usageFields = parseProtoFields(protoFieldBytes(firstProtoField(chatFields, 4)) ?? new Uint8Array())
if (usageFields.length === 0) return null
const inputTokens = protoFieldPositiveInteger(firstProtoField(usageFields, 2))
|| protoFieldPositiveInteger(firstProtoField(usageFields, 1))
const totalOutputTokens = protoFieldPositiveInteger(firstProtoField(usageFields, 3))
let responseTokens = protoFieldPositiveInteger(firstProtoField(usageFields, 9))
let thinkingTokens = protoFieldPositiveInteger(firstProtoField(usageFields, 10))
if (responseTokens === 0 && thinkingTokens === 0) {
responseTokens = totalOutputTokens
} else if (totalOutputTokens > 0 && responseTokens + thinkingTokens !== totalOutputTokens) {
const adjustedResponseTokens = totalOutputTokens - thinkingTokens
if (adjustedResponseTokens >= 0) responseTokens = adjustedResponseTokens
}
if (inputTokens === 0 && totalOutputTokens === 0) return null
const responseId = antigravitySqliteResponseId(usageFields, String(row.idx))
const model = antigravitySqliteModel(chatFields)
return {
provider: 'antigravity',
model,
inputTokens,
outputTokens: responseTokens,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: thinkingTokens,
webSearchRequests: 0,
timestamp: antigravitySqliteCreatedAt(chatFields),
speed: 'standard',
deduplicationKey: `antigravity:${cascadeId}:${responseId}`,
sessionId: cascadeId,
}
}
/** gen_metadata rows -> calls. Per-cascade dedup on deduplicationKey (host-side
* cross-file dedup is applied later by the caller, exactly as before). */
export function decodeAntigravityGenMetadata(
input: AntigravityGenMetadataDecodeInput,
): AntigravityDecodeResult {
const { records, cascadeId } = input
const calls: AntigravityDecodedCall[] = []
const seenResponseIds = new Set<string>()
for (const row of records as AntigravityGenMetadataRow[]) {
const call = decodeAntigravityGenMetadataRow(cascadeId, row)
if (!call) continue
if (seenResponseIds.has(call.deduplicationKey)) continue
seenResponseIds.add(call.deduplicationKey)
calls.push(call)
}
return { calls, diagnostics: [] }
}
export function extractAntigravityModelMap(resp: unknown): AntigravityModelMap {
if (!resp || typeof resp !== 'object') return {}
const data = resp as AntigravityModelMapResponse
const models = data.response?.models ?? data.models
const map = new Map<string, string>()
if (!models) return {}
for (const [key, info] of Object.entries(models)) {
if (info && typeof info === 'object' && typeof info.model === 'string') {
const canonicalKey = getCanonicalModelId(key, info.displayName)
map.set(info.model, canonicalKey)
}
}
return Object.fromEntries(map)
}
export function extractAntigravityGeneratorMetadata(resp: unknown): AntigravityGeneratorMetadata[] {
if (!resp || typeof resp !== 'object') return []
const data = resp as AntigravityGeneratorMetadataResponse
const metadata = data.response?.generatorMetadata ?? data.generatorMetadata
return Array.isArray(metadata) ? metadata : []
}
function parseFiniteToken(value: unknown): number {
return typeof value === 'number' && Number.isFinite(value) && value > 0
? Math.floor(value)
: 0
}
function usageSignature(event: AntigravityStatusLineEvent): string {
const u = event.usage
return [
event.model,
u.inputTokens,
u.outputTokens,
u.cacheCreationInputTokens,
u.cacheReadInputTokens,
].join(':')
}
function usageHasTokens(usage: AntigravityStatusLineEvent['usage']): boolean {
return (
usage.inputTokens > 0 ||
usage.outputTokens > 0 ||
usage.cacheCreationInputTokens > 0 ||
usage.cacheReadInputTokens > 0
)
}
function usageIsMonotonic(
current: AntigravityStatusLineEvent['usage'],
previous: AntigravityStatusLineEvent['usage'],
): boolean {
return (
current.inputTokens >= previous.inputTokens &&
current.outputTokens >= previous.outputTokens &&
current.cacheCreationInputTokens >= previous.cacheCreationInputTokens &&
current.cacheReadInputTokens >= previous.cacheReadInputTokens
)
}
function usageDelta(
current: AntigravityStatusLineEvent['usage'],
previous: AntigravityStatusLineEvent['usage'],
): AntigravityStatusLineEvent['usage'] {
return {
inputTokens: current.inputTokens - previous.inputTokens,
outputTokens: current.outputTokens - previous.outputTokens,
cacheCreationInputTokens: current.cacheCreationInputTokens - previous.cacheCreationInputTokens,
cacheReadInputTokens: current.cacheReadInputTokens - previous.cacheReadInputTokens,
}
}
/** Hook-capture record parse. The wall clock is INJECTED (`at`) so the decoder
* stays pure; the host passes a freshly-generated ISO-8601 timestamp. */
export function parseAntigravityStatusLinePayload(
input: unknown,
at: string,
): AntigravityStatusLineEvent | null {
if (!input || typeof input !== 'object') return null
const payload = input as AntigravityStatusLinePayload
if (typeof payload.conversation_id !== 'string' || payload.conversation_id.length === 0) return null
const usage = payload.context_window?.current_usage
if (!usage) return null
const event: AntigravityStatusLineEvent = {
at,
conversationId: payload.conversation_id,
sessionId: typeof payload.session_id === 'string' ? payload.session_id : undefined,
model: typeof payload.model === 'string'
? payload.model
: payload.model?.id ?? payload.model?.display_name ?? 'unknown',
usage: {
inputTokens: parseFiniteToken(usage.input_tokens),
outputTokens: parseFiniteToken(usage.output_tokens),
cacheCreationInputTokens: parseFiniteToken(usage.cache_creation_input_tokens),
cacheReadInputTokens: parseFiniteToken(usage.cache_read_input_tokens),
},
}
const u = event.usage
if (u.inputTokens === 0 && u.outputTokens === 0 && u.cacheCreationInputTokens === 0 && u.cacheReadInputTokens === 0) {
return null
}
if (event.model === 'unknown') return null
return event
}
function parseStatusLineEvent(input: unknown): AntigravityStatusLineEvent | null {
if (!input || typeof input !== 'object') return null
const event = input as AntigravityStatusLineEvent
if (typeof event.at !== 'string' || Number.isNaN(new Date(event.at).getTime())) return null
if (typeof event.conversationId !== 'string' || event.conversationId.length === 0) return null
if (typeof event.model !== 'string' || event.model.length === 0) return null
if (!event.usage || typeof event.usage !== 'object') return null
const usage = {
inputTokens: parseFiniteToken(event.usage.inputTokens),
outputTokens: parseFiniteToken(event.usage.outputTokens),
cacheCreationInputTokens: parseFiniteToken(event.usage.cacheCreationInputTokens),
cacheReadInputTokens: parseFiniteToken(event.usage.cacheReadInputTokens),
}
if (
usage.inputTokens === 0 &&
usage.outputTokens === 0 &&
usage.cacheCreationInputTokens === 0 &&
usage.cacheReadInputTokens === 0
) return null
return {
at: event.at,
conversationId: event.conversationId,
sessionId: typeof event.sessionId === 'string' ? event.sessionId : undefined,
model: event.model,
usage,
}
}
function hasRpcCacheForConversation(seenKeys: ReadonlySet<string>, conversationId: string): boolean {
const prefix = `antigravity:${conversationId}:`
for (const key of seenKeys) {
if (key.startsWith(prefix)) return true
}
return false
}
/** statusline jsonl -> calls (run collapse + monotonic deltas). */
export function decodeAntigravityStatusLine(
input: AntigravityStatusLineDecodeInput,
): AntigravityDecodeResult {
const { records, seenKeys } = input
const runsByConversation = new Map<string, Array<{ event: AntigravityStatusLineEvent; signature: string; count: number }>>()
for (const line of records as string[]) {
if (!line.trim()) continue
let parsed: unknown
try {
parsed = JSON.parse(line)
} catch {
continue
}
const event = parseStatusLineEvent(parsed)
if (!event) continue
if (hasRpcCacheForConversation(seenKeys, event.conversationId)) continue
const signature = usageSignature(event)
const runs = runsByConversation.get(event.conversationId) ?? []
const lastRun = runs.at(-1)
if (lastRun?.signature === signature) {
lastRun.count += 1
lastRun.event = event
} else {
runs.push({ event, signature, count: 1 })
runsByConversation.set(event.conversationId, runs)
}
}
const calls: AntigravityDecodedCall[] = []
for (const runs of runsByConversation.values()) {
let turnIndex = 0
let previousSnapshotUsage: AntigravityStatusLineEvent['usage'] | null = null
for (let i = 0; i < runs.length; i++) {
const run = runs[i]!
const isLastRun = i === runs.length - 1
if (run.count === 1 && !isLastRun) continue
const event = run.event
const signature = run.signature
const billableUsage = previousSnapshotUsage && usageIsMonotonic(event.usage, previousSnapshotUsage)
? usageDelta(event.usage, previousSnapshotUsage)
: event.usage
previousSnapshotUsage = event.usage
if (!usageHasTokens(billableUsage)) continue
const dedupKey = `antigravity-statusline:${event.conversationId}:${turnIndex}:${signature}`
turnIndex += 1
if (seenKeys.has(dedupKey)) continue
const u = billableUsage
calls.push({
provider: 'antigravity',
model: event.model,
inputTokens: u.inputTokens,
outputTokens: u.outputTokens,
cacheCreationInputTokens: u.cacheCreationInputTokens,
cacheReadInputTokens: u.cacheReadInputTokens,
cachedInputTokens: 0,
// StatusLine current_usage exposes aggregate output tokens, not a
// separate thinking/response split. Preserve the exact total instead
// of inventing a breakdown.
reasoningTokens: 0,
webSearchRequests: 0,
timestamp: event.at,
speed: 'standard',
deduplicationKey: dedupKey,
sessionId: event.conversationId,
})
}
}
return { calls, diagnostics: [] }
}
/** RPC generatorMetadata entries -> calls. */
export function decodeAntigravityGeneratorMetadata(
input: AntigravityGeneratorMetadataDecodeInput,
): AntigravityDecodeResult {
const { records, cascadeId, modelMap } = input
const calls: AntigravityDecodedCall[] = []
for (let i = 0; i < records.length; i++) {
const entry = records[i] as AntigravityGeneratorMetadata
const usage = entry.chatModel?.usage
if (!usage) continue
const inputTokens = parseInt(usage.inputTokens ?? '0', 10)
const outputTokens = parseInt(usage.outputTokens ?? '0', 10)
const thinkingTokens = parseInt(usage.thinkingOutputTokens ?? '0', 10)
const responseTokens = parseInt(usage.responseOutputTokens ?? '0', 10)
if (inputTokens === 0 && outputTokens === 0) continue
const responseId = usage.responseId || String(i)
const dedupKey = `antigravity:${cascadeId}:${responseId}`
const model = dropPlaceholderModelId(modelMap[usage.model] ?? usage.model)
const timestamp = entry.chatModel?.chatStartMetadata?.createdAt ?? ''
calls.push({
provider: 'antigravity',
model,
inputTokens,
outputTokens: responseTokens,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: thinkingTokens,
webSearchRequests: 0,
timestamp,
speed: 'standard',
deduplicationKey: dedupKey,
sessionId: cascadeId,
})
}
return { calls, diagnostics: [] }
}

View file

@ -0,0 +1,44 @@
// @codeburn/core Antigravity provider.
//
// Two layers:
// - Rich pure decode (`decodeAntigravityGenMetadata`,
// `decodeAntigravityGeneratorMetadata`, `decodeAntigravityStatusLine`,
// `parseAntigravityStatusLinePayload`): host-facing, NOT part of the stable
// minimized surface. Pure over supplied records; carries token buckets but no
// pricing (cost leaves the decoder).
// - Minimizing transform (`toObservations`): maps the rich decode into the
// strict observation envelope; the content-smuggling guarantees bind here.
export {
decodeAntigravityGenMetadata,
decodeAntigravityGeneratorMetadata,
decodeAntigravityStatusLine,
parseAntigravityStatusLinePayload,
extractAntigravityModelMap,
extractAntigravityGeneratorMetadata,
type AntigravityGenMetadataDecodeInput,
type AntigravityGeneratorMetadataDecodeInput,
type AntigravityStatusLineDecodeInput,
type AntigravityDecodeResult,
} from './decode.js'
export {
toObservations,
type RichAntigravitySessionDecode,
type AntigravityToObservationsContext,
} from './observations.js'
export type {
AntigravityDecodedCall,
AntigravityGeneratorMetadata,
AntigravityGeneratorMetadataResponse,
AntigravityGenMetadataRow,
AntigravityModelMap,
AntigravityModelMapResponse,
AntigravityStatusLineCurrentUsage,
AntigravityStatusLineEvent,
AntigravityStatusLinePayload,
AntigravityUsageEntry,
ProtoField,
ProtoVarint,
} from './types.js'

View file

@ -0,0 +1,85 @@
// Minimizing transform: rich Antigravity decode -> the strict observation
// envelope. Only opaque ids, fingerprints, enums, numbers, timestamps, and
// canonical tool names cross into the output. Antigravity never records file
// paths and has no tool calls, so projectPath is fingerprinted and toolNames is
// always empty.
import { projectRef, sessionRef } from '../../fingerprint.js'
import type { RecordDiagnostic } from '../../diagnostics.js'
import type { CallObservation, SessionObservation } from '../../observations.js'
import type { AntigravityDecodedCall } from './types.js'
/** One Antigravity cascade's rich decode, as the host holds it before minimization. */
export interface RichAntigravitySessionDecode {
sessionId: string
/** Absolute project path; fingerprinted, never emitted raw. */
projectPath: string
calls: AntigravityDecodedCall[]
}
export interface AntigravityToObservationsContext {
/** HMAC key that scopes every fingerprint. */
privacyKey: string
/** Provider id stamped onto sessions/calls and folded into sessionRef. */
provider?: string
}
function toCallObservation(call: AntigravityDecodedCall, turnIndex: number): CallObservation {
return {
provider: call.provider,
model: call.model,
tokens: {
input: call.inputTokens,
output: call.outputTokens,
reasoning: call.reasoningTokens,
cacheRead: call.cacheReadInputTokens,
cacheCreate: call.cacheCreationInputTokens,
},
webSearchRequests: call.webSearchRequests,
speed: call.speed,
costBasis: 'estimated',
timestamp: call.timestamp,
dedupKey: call.deduplicationKey,
// Antigravity records carry no tool calls at all, so there is nothing to
// filter through a canonical-name gate here.
toolNames: [],
turnIndex,
}
}
function toSessionObservation(
decode: RichAntigravitySessionDecode,
ctx: AntigravityToObservationsContext,
): SessionObservation {
const provider = ctx.provider ?? 'antigravity'
const calls: CallObservation[] = decode.calls.map((call, i) => toCallObservation(call, i))
const timestamps = calls.map(c => c.timestamp).filter(t => t.length > 0).sort()
const startedAt = timestamps[0] ?? ''
const endedAt = timestamps.length > 0 ? timestamps[timestamps.length - 1]! : ''
const session: SessionObservation = {
sessionRef: sessionRef(ctx.privacyKey, provider, decode.sessionId),
projectRef: projectRef(ctx.privacyKey, decode.projectPath),
providerId: provider,
startedAt,
...(endedAt ? { endedAt } : {}),
calls,
turnCount: calls.length,
}
return session
}
/**
* Map a rich Antigravity decode (one or many cascades) into the minimized
* observation layer. Returns the `sessions` array plus any per-record
* `diagnostics`.
*/
export function toObservations(
decode: RichAntigravitySessionDecode | RichAntigravitySessionDecode[],
ctx: AntigravityToObservationsContext,
): { sessions: SessionObservation[]; diagnostics: RecordDiagnostic[] } {
const decodes = Array.isArray(decode) ? decode : [decode]
const sessions = decodes.map(d => toSessionObservation(d, ctx))
return { sessions, diagnostics: [] }
}

View file

@ -0,0 +1,113 @@
// Raw record + rich-decode types for the Antigravity provider.
//
// Antigravity is a Category D stateful multi-store provider. The host keeps all
// I/O, discovery, durable caching, and project attribution; core owns only the
// pure decode of the three record shapes the host hands it: sqlite gen_metadata
// rows, RPC generatorMetadata entries, and statusline JSONL events.
export type AntigravityUsageEntry = {
model: string
inputTokens: string
outputTokens: string
thinkingOutputTokens?: string
responseOutputTokens?: string
apiProvider: string
responseId?: string
}
export type AntigravityGeneratorMetadata = {
stepIndices?: number[]
chatModel?: {
model: string
usage: AntigravityUsageEntry
chatStartMetadata?: {
createdAt?: string
}
}
}
export type AntigravityModelMapResponse = {
models?: Record<string, { model?: string; displayName?: string }>
response?: {
models?: Record<string, { model?: string; displayName?: string }>
}
}
export type AntigravityGeneratorMetadataResponse = {
generatorMetadata?: AntigravityGeneratorMetadata[]
response?: {
generatorMetadata?: AntigravityGeneratorMetadata[]
}
}
export type AntigravityModelMap = Record<string, string>
export type AntigravityStatusLineCurrentUsage = {
input_tokens?: number
output_tokens?: number
cache_creation_input_tokens?: number
cache_read_input_tokens?: number
}
export type AntigravityStatusLinePayload = {
conversation_id?: string
session_id?: string
model?: string | {
id?: string
display_name?: string
}
context_window?: {
current_usage?: AntigravityStatusLineCurrentUsage | null
}
}
export type AntigravityStatusLineEvent = {
at: string
conversationId: string
sessionId?: string
model: string
usage: {
inputTokens: number
outputTokens: number
cacheCreationInputTokens: number
cacheReadInputTokens: number
}
}
export type AntigravityGenMetadataRow = {
idx: number
data: Uint8Array | string
}
export type ProtoField = {
number: number
wireType: number
value?: bigint
bytes?: Uint8Array
}
export type ProtoVarint = {
value: bigint
offset: number
}
// The rich decode of one Antigravity call, pre-pricing. Mirrors the host's
// ParsedProviderCall minus everything the host owns: cost (costUSD/costBasis/
// pricingModel), project attribution, and the always-empty tools/bashCommands
// plus the host-supplied empty text field. Antigravity records carry no tool
// calls and no user text at all.
export type AntigravityDecodedCall = {
provider: 'antigravity'
model: string
inputTokens: number
outputTokens: number
cacheCreationInputTokens: number
cacheReadInputTokens: number
cachedInputTokens: number
reasoningTokens: number
webSearchRequests: number
timestamp: string
speed: 'standard'
deduplicationKey: string
sessionId: string
}

View file

@ -38,6 +38,12 @@ import { decodeCopilot, toObservations as toCopilotObservations } from '../src/p
import { decodeVscodeCline, toObservations as toVscodeClineObservations } from '../src/providers/vscode-cline/index.js'
import { decodeOpenCodeSession, toObservations as toOpenCodeSessionObservations } from '../src/providers/opencode-session/index.js'
import { decodeMistralVibe, toObservations as toMistralVibeObservations } from '../src/providers/mistral-vibe/index.js'
import {
decodeAntigravityGenMetadata,
decodeAntigravityGeneratorMetadata,
decodeAntigravityStatusLine,
toObservations as toAntigravityObservations,
} from '../src/providers/antigravity/index.js'
import type { DecodeContext } from '../src/contracts.js'
import type { ZedThreadRow } from '../src/providers/zed/index.js'
@ -1571,3 +1577,172 @@ describe('content-smuggling guardrail: real mistral-vibe decode -> toObservation
expect(allToolNames).not.toContain(SECRETS.commandLine)
})
})
describe('content-smuggling guardrail: real antigravity decode -> toObservations is secret-free', () => {
// A hostile Antigravity cascade planting secrets in every free-text field the
// decode reads but never emits: proto attribute values other than model_enum,
// ignored proto fields, the statusline envelope's cwd/session_id, and the RPC
// usage.apiProvider. Only the canonicalized model and the responseId (inside
// the dedupKey) are emitted by design — they are the provider's own machine
// identifiers, exactly like every other provider's model and dedupKey.
const antigravityContext: DecodeContext = {
privacyKey: 'test-privacy-key',
providerId: 'antigravity',
sourceRef: 'ref',
}
function varint(n: number): number[] {
const out: number[] = []
let v = n
while (v > 0x7f) {
out.push((v & 0x7f) | 0x80)
v = Math.floor(v / 128)
}
out.push(v)
return out
}
function tag(field: number, wire: number): number[] {
return varint(field * 8 + wire)
}
function varintField(field: number, n: number): number[] {
return [...tag(field, 0), ...varint(n)]
}
function lenField(field: number, bytes: number[]): number[] {
return [...tag(field, 2), ...varint(bytes.length), ...bytes]
}
function textField(field: number, text: string): number[] {
return lenField(field, [...new TextEncoder().encode(text)])
}
function attrField(key: string, value: string): number[] {
return lenField(20, [...textField(1, key), ...textField(2, value)])
}
function buildHostileGenMetadataRow(): { idx: number; data: Uint8Array } {
const chatStartMetadata = textField(4, '2026-07-17T10:00:00.000Z')
const usage = lenField(4, [
...varintField(2, 100),
...varintField(3, 50),
...varintField(6, 999),
])
const chatModel = [
...usage,
...lenField(9, chatStartMetadata),
...textField(19, 'gemini-3-pro'),
...attrField('trajectory_id', SECRETS.commandLine),
...attrField('used_claude', SECRETS.apiKey),
...attrField('last_step_index', SECRETS.prompt),
]
const root = [
...lenField(2, [...new TextEncoder().encode(SECRETS.fileContent)]),
...lenField(4, [...new TextEncoder().encode(SECRETS.absPath)]),
...lenField(1, chatModel),
]
return { idx: 0, data: Buffer.from(root) }
}
function decodeAndMinimize() {
const genMetadataCalls = decodeAntigravityGenMetadata({
records: [buildHostileGenMetadataRow()],
context: antigravityContext,
cascadeId: 'sess-hostile',
}).calls
// The statusline decoder consumes RECORDED events (camelCase, already
// normalized by parseAntigravityStatusLinePayload), not the raw hook
// payload. `sessionId` and any stray envelope field such as `cwd` are read
// past but never emitted — the emitted call's sessionId is conversationId.
const statusLineCalls = decodeAntigravityStatusLine({
records: [
JSON.stringify({
at: '2026-07-17T10:00:00.000Z',
conversationId: 'sess-hostile-statusline',
sessionId: SECRETS.apiKey,
cwd: SECRETS.absPath,
model: 'gemini-3-pro',
usage: {
inputTokens: 100,
outputTokens: 50,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
},
}),
],
context: antigravityContext,
seenKeys: new Set(),
}).calls
const rpcCalls = decodeAntigravityGeneratorMetadata({
records: [
{
chatModel: {
model: 'gemini-3-pro',
usage: {
model: 'gemini-3-pro',
inputTokens: '100',
outputTokens: '50',
responseOutputTokens: '50',
apiProvider: SECRETS.commandLine,
responseId: 'rpc-secret',
},
chatStartMetadata: { createdAt: '2026-07-17T10:00:00.000Z' },
},
},
],
context: antigravityContext,
cascadeId: 'sess-hostile',
modelMap: {},
}).calls
const calls = [...genMetadataCalls, ...statusLineCalls, ...rpcCalls]
const { sessions } = toAntigravityObservations(
{ sessionId: 'sess-hostile', projectPath: SECRETS.absPath, calls },
{ privacyKey: 'test-privacy-key', provider: 'antigravity' },
)
return {
schemaVersion: OBSERVATION_SCHEMA_VERSION,
generator: { name: '@codeburn/core', version: '0.0.0-test' },
sessions,
}
}
it('produces a schema-valid envelope from the hostile cascade', () => {
const envelope = decodeAndMinimize()
expect(ObservationEnvelope.safeParse(envelope).success).toBe(true)
// Guards against a vacuous fixture: a malformed record would be dropped by
// its decoder and the secret assertions below would then prove nothing.
expect(envelope.sessions[0]?.calls).toHaveLength(3)
})
it('the serialized envelope contains none of the planted secrets', () => {
const serialized = JSON.stringify(decodeAndMinimize())
for (const secret of ALL_SECRETS) {
expect(serialized).not.toContain(secret)
}
})
it('emits no tool names (antigravity has no tool map)', () => {
const env = decodeAndMinimize()
const allToolNames = env.sessions.flatMap(s => s.calls.flatMap(c => c.toolNames))
expect(allToolNames).toEqual([])
})
it('a MODEL_PLACEHOLDER id with no displayName surfaces as unknown, never the raw placeholder', () => {
const usage = lenField(4, [...varintField(2, 10), ...varintField(3, 1)])
const chatModel = [...usage, ...textField(19, 'MODEL_PLACEHOLDER_HOSTILE')]
const row = { idx: 0, data: Buffer.from(lenField(1, chatModel)) }
const { calls } = decodeAntigravityGenMetadata({
records: [row],
context: antigravityContext,
cascadeId: 'placeholder-test',
})
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('unknown')
})
})

View file

@ -0,0 +1,400 @@
import { describe, expect, it } from 'vitest'
import {
decodeAntigravityGenMetadata,
decodeAntigravityGeneratorMetadata,
decodeAntigravityStatusLine,
parseAntigravityStatusLinePayload,
toObservations,
} from '../../src/providers/antigravity/index.js'
import { ObservationEnvelope } from '../../src/observations.js'
import { OBSERVATION_SCHEMA_VERSION } from '../../src/schema.js'
import type { DecodeContext } from '../../src/contracts.js'
const context: DecodeContext = { privacyKey: 'k', providerId: 'antigravity', sourceRef: 'ref' }
function varint(n: number): number[] {
const out: number[] = []
let v = n
while (v > 0x7f) {
out.push((v & 0x7f) | 0x80)
v = Math.floor(v / 128)
}
out.push(v)
return out
}
function tag(field: number, wire: number): number[] {
return varint(field * 8 + wire)
}
function varintField(field: number, n: number): number[] {
return [...tag(field, 0), ...varint(n)]
}
function lenField(field: number, bytes: number[]): number[] {
return [...tag(field, 2), ...varint(bytes.length), ...bytes]
}
function textField(field: number, text: string): number[] {
const bytes = [...new TextEncoder().encode(text)]
return lenField(field, bytes)
}
function attrField(key: string, value: string): number[] {
return lenField(20, [...textField(1, key), ...textField(2, value)])
}
function sqliteRow(idx: number, hex: string): { idx: number; data: Uint8Array } {
return { idx, data: Buffer.from(hex, 'hex') }
}
describe('antigravity rich decode (moved to @codeburn/core)', () => {
it('G2: per-cascade seenResponseIds deduplicates identical response ids', () => {
const usage = lenField(4, [...varintField(2, 100), ...varintField(3, 10), ...textField(11, 'dup-response')])
const chatModel = [...usage, ...textField(19, 'gemini-pro-default'), ...textField(21, 'Gemini 3.1 Pro (High)')]
const hex = Buffer.from(lenField(1, chatModel)).toString('hex')
const { calls } = decodeAntigravityGenMetadata({
records: [sqliteRow(0, hex), sqliteRow(1, hex)],
context,
cascadeId: 'g2-dedup',
})
expect(calls).toHaveLength(1)
expect(calls[0]).toEqual({
provider: 'antigravity',
model: 'gemini-3.1-pro-high',
inputTokens: 100,
outputTokens: 10,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
timestamp: '',
speed: 'standard',
deduplicationKey: 'antigravity:g2-dedup:dup-response',
sessionId: 'g2-dedup',
})
})
it('G3: responseTokens falls back to totalOutputTokens when split fields are absent', () => {
const usage = lenField(4, [...varintField(2, 50), ...varintField(3, 500)])
const chatModel = [...usage, ...textField(19, 'gemini-pro-default'), ...textField(21, 'Gemini 3.1 Pro (High)')]
const hex = Buffer.from(lenField(1, chatModel)).toString('hex')
const { calls } = decodeAntigravityGenMetadata({
records: [sqliteRow(0, hex)],
context,
cascadeId: 'g3-total-output',
})
expect(calls[0]).toMatchObject({ outputTokens: 500, reasoningTokens: 0 })
})
it('G4: output split adjust arm reconciles response + thinking to total', () => {
const usage = lenField(4, [
...varintField(2, 80),
...varintField(3, 100),
...varintField(9, 10),
...varintField(10, 30),
])
const chatModel = [...usage, ...textField(19, 'gemini-pro-default'), ...textField(21, 'Gemini 3.1 Pro (High)')]
const hex = Buffer.from(lenField(1, chatModel)).toString('hex')
const { calls } = decodeAntigravityGenMetadata({
records: [sqliteRow(0, hex)],
context,
cascadeId: 'g4-adjust',
})
expect(calls[0]).toMatchObject({ outputTokens: 70, reasoningTokens: 30 })
})
it('G5: inputTokens falls back to field #1 when field #2 is absent', () => {
const usage = lenField(4, [...varintField(1, 777), ...varintField(3, 5)])
const chatModel = [...usage, ...textField(19, 'gemini-pro-default'), ...textField(21, 'Gemini 3.1 Pro (High)')]
const hex = Buffer.from(lenField(1, chatModel)).toString('hex')
const { calls } = decodeAntigravityGenMetadata({
records: [sqliteRow(0, hex)],
context,
cascadeId: 'g5-input-fallback',
})
expect(calls[0]).toMatchObject({ inputTokens: 777 })
})
it('G6: responseId falls back to row idx when field #11 is absent', () => {
const usage = lenField(4, [...varintField(2, 33), ...varintField(3, 3)])
const chatModel = [...usage, ...textField(19, 'gemini-pro-default'), ...textField(21, 'Gemini 3.1 Pro (High)')]
const hex = Buffer.from(lenField(1, chatModel)).toString('hex')
const { calls } = decodeAntigravityGenMetadata({
records: [sqliteRow(3, hex)],
context,
cascadeId: 'g6-response-id-fallback',
})
expect(calls[0]).toMatchObject({ deduplicationKey: 'antigravity:g6-response-id-fallback:3' })
})
it('G7: model precedence and placeholder guard produce distinct canonical models', () => {
const baseUsage = lenField(4, [...varintField(2, 10), ...varintField(3, 1)])
const aChat = [...baseUsage, ...textField(19, 'gemini-3.1-pro-high')]
const bChat = [...baseUsage, ...attrField('model_enum', 'gemini-3.5-flash-agent')]
const cChat = [...baseUsage, ...textField(21, 'Gemini 3 Flash')]
const dChat = [...baseUsage]
const eChat = [...baseUsage, ...textField(19, 'MODEL_PLACEHOLDER_M99')]
const { calls } = decodeAntigravityGenMetadata({
records: [
sqliteRow(0, Buffer.from(lenField(1, aChat)).toString('hex')),
sqliteRow(1, Buffer.from(lenField(1, bChat)).toString('hex')),
sqliteRow(2, Buffer.from(lenField(1, cChat)).toString('hex')),
sqliteRow(3, Buffer.from(lenField(1, dChat)).toString('hex')),
sqliteRow(4, Buffer.from(lenField(1, eChat)).toString('hex')),
],
context,
cascadeId: 'g7-model-precedence',
})
expect(calls.map(c => c.model)).toEqual([
'gemini-3.1-pro-high',
'gemini-3.5-flash-agent',
'gemini-3-flash',
'unknown',
'unknown',
])
})
it('G8: chatStartMetadata.created_at beats any host mtime', () => {
const seconds = 1783326234
const nanos = 724675400
const timestamp = [...varintField(1, seconds), ...varintField(2, nanos)]
const chatStartMetadata = lenField(4, timestamp)
const usage = lenField(4, [...varintField(2, 100), ...varintField(3, 50)])
const chatModel = [...usage, ...lenField(9, chatStartMetadata)]
const hex = Buffer.from(lenField(1, chatModel)).toString('hex')
const { calls } = decodeAntigravityGenMetadata({
records: [sqliteRow(0, hex)],
context,
cascadeId: 'g8-created-at',
})
expect(calls[0]).toMatchObject({ timestamp: '2026-07-06T08:23:54.724Z' })
})
it('G9: zero input and output tokens skip the row', () => {
const usage = lenField(4, [...varintField(2, 0), ...varintField(3, 0)])
const chatModel = [...usage, ...textField(19, 'gemini-pro-default'), ...textField(21, 'Gemini 3.1 Pro (High)')]
const hex = Buffer.from(lenField(1, chatModel)).toString('hex')
const { calls } = decodeAntigravityGenMetadata({
records: [sqliteRow(0, hex)],
context,
cascadeId: 'g9-zero',
})
expect(calls).toEqual([])
})
it('G14: RPC generator metadata decode handles skips, fallbacks, model map, and timestamps', () => {
const modelMap = {
'raw-model-a': 'gemini-3-pro',
'placeholder-key': 'MODEL_PLACEHOLDER_7',
}
const metadata = [
{ chatModel: { model: 'no-usage' } },
{
chatModel: {
model: 'zero',
usage: {
model: 'zero',
inputTokens: '0',
outputTokens: '0',
apiProvider: 'google',
},
},
},
{
chatModel: {
model: 'unmapped-model-a',
usage: {
model: 'unmapped-model-a',
inputTokens: '100',
outputTokens: '20',
responseOutputTokens: '15',
thinkingOutputTokens: '5',
apiProvider: 'google',
},
},
},
{
chatModel: {
model: 'raw-model-a',
usage: {
model: 'raw-model-a',
inputTokens: '50',
outputTokens: '10',
responseOutputTokens: '8',
thinkingOutputTokens: '2',
apiProvider: 'google',
responseId: 'mapped-call',
},
},
},
{
chatModel: {
model: 'unmapped-model-b',
usage: {
model: 'unmapped-model-b',
inputTokens: '77',
outputTokens: '7',
responseOutputTokens: '6',
thinkingOutputTokens: '1',
apiProvider: 'google',
responseId: 'raw-passthrough-call',
},
},
},
{
chatModel: {
model: 'placeholder-key',
usage: {
model: 'placeholder-key',
inputTokens: '25',
outputTokens: '5',
responseOutputTokens: '4',
thinkingOutputTokens: '1',
apiProvider: 'google',
responseId: 'placeholder-call',
},
},
},
{
chatModel: {
model: 'with-timestamp',
usage: {
model: 'with-timestamp',
inputTokens: '99',
outputTokens: '9',
responseOutputTokens: '9',
apiProvider: 'google',
responseId: 'timestamp-call',
},
chatStartMetadata: { createdAt: '2026-03-15T09:30:00.000Z' },
},
},
{
chatModel: {
model: 'no-timestamp',
usage: {
model: 'no-timestamp',
inputTokens: '11',
outputTokens: '2',
responseOutputTokens: '2',
apiProvider: 'google',
responseId: 'no-timestamp-call',
},
},
},
]
const { calls } = decodeAntigravityGeneratorMetadata({
records: metadata as unknown[],
context,
cascadeId: 'g14-rpc',
modelMap,
})
expect(calls).toHaveLength(6)
expect(calls.map(c => c.deduplicationKey)).toEqual([
'antigravity:g14-rpc:2',
'antigravity:g14-rpc:mapped-call',
'antigravity:g14-rpc:raw-passthrough-call',
'antigravity:g14-rpc:placeholder-call',
'antigravity:g14-rpc:timestamp-call',
'antigravity:g14-rpc:no-timestamp-call',
])
expect(calls.map(c => ({ model: c.model, inputTokens: c.inputTokens, outputTokens: c.outputTokens, reasoningTokens: c.reasoningTokens, timestamp: c.timestamp }))).toEqual([
{ model: 'unmapped-model-a', inputTokens: 100, outputTokens: 15, reasoningTokens: 5, timestamp: '' },
{ model: 'gemini-3-pro', inputTokens: 50, outputTokens: 8, reasoningTokens: 2, timestamp: '' },
{ model: 'unmapped-model-b', inputTokens: 77, outputTokens: 6, reasoningTokens: 1, timestamp: '' },
{ model: 'unknown', inputTokens: 25, outputTokens: 4, reasoningTokens: 1, timestamp: '' },
{ model: 'with-timestamp', inputTokens: 99, outputTokens: 9, reasoningTokens: 0, timestamp: '2026-03-15T09:30:00.000Z' },
{ model: 'no-timestamp', inputTokens: 11, outputTokens: 2, reasoningTokens: 0, timestamp: '' },
])
})
it('statusline: run collapse, monotonic delta, and seenKeys respect', () => {
const lines = [
JSON.stringify({ at: '2026-01-01T00:00:01.000Z', conversationId: 's1', model: 'm', usage: { inputTokens: 100, outputTokens: 10, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 } }),
JSON.stringify({ at: '2026-01-01T00:00:02.000Z', conversationId: 's1', model: 'm', usage: { inputTokens: 200, outputTokens: 20, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 } }),
JSON.stringify({ at: '2026-01-01T00:00:03.000Z', conversationId: 's1', model: 'm', usage: { inputTokens: 200, outputTokens: 20, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 } }),
JSON.stringify({ at: '2026-01-01T00:00:04.000Z', conversationId: 's1', model: 'm', usage: { inputTokens: 300, outputTokens: 30, cacheCreationInputTokens: 0, cacheReadInputTokens: 50 } }),
]
const { calls } = decodeAntigravityStatusLine({ records: lines, context, seenKeys: new Set() })
expect(calls).toHaveLength(2)
expect(calls[0]).toMatchObject({ inputTokens: 200, outputTokens: 20, cacheReadInputTokens: 0 })
expect(calls[1]).toMatchObject({ inputTokens: 100, outputTokens: 10, cacheReadInputTokens: 50 })
})
it('parseAntigravityStatusLinePayload uses the injected at value and never captures cwd', () => {
const fixedAt = '2026-05-05T05:05:05.005Z'
const payload = {
conversation_id: 'parse-test',
session_id: 'session-secret',
cwd: '/Users/victim/secret-project',
model: { id: 'gemini-3-pro', display_name: 'Gemini 3 Pro' },
context_window: {
current_usage: {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
},
}
const event = parseAntigravityStatusLinePayload(payload, fixedAt)
expect(event).not.toBeNull()
expect(event!.at).toBe(fixedAt)
expect(event).not.toHaveProperty('cwd')
expect(JSON.stringify(event)).not.toContain('/Users/victim/secret-project')
})
it('toObservations produces a schema-valid, secret-free envelope', () => {
const chatStartMetadata = textField(4, '2026-07-17T10:00:00.000Z')
const usage = lenField(4, [...varintField(2, 100), ...varintField(3, 10)])
const chatModel = [...usage, ...lenField(9, chatStartMetadata), ...textField(19, 'gemini-3.1-pro-high')]
const hex = Buffer.from(lenField(1, chatModel)).toString('hex')
const { calls } = decodeAntigravityGenMetadata({
records: [sqliteRow(0, hex)],
context,
cascadeId: 'obs-test',
})
const { sessions } = toObservations(
{ sessionId: 'obs-test', projectPath: '/Users/t/secret-project', calls },
{ privacyKey: 'test-privacy-key', provider: 'antigravity' },
)
const envelope = {
schemaVersion: OBSERVATION_SCHEMA_VERSION,
generator: { name: '@codeburn/core', version: '0.0.0-test' },
sessions,
}
expect(ObservationEnvelope.safeParse(envelope).success).toBe(true)
expect(sessions[0]?.calls[0]?.toolNames).toEqual([])
expect(JSON.stringify(envelope)).not.toContain('/Users/t/secret-project')
})
})

View file

@ -41,6 +41,7 @@ export default defineConfig({
'src/providers/copilot/index.ts',
'src/providers/vscode-cline/index.ts',
'src/providers/opencode-session/index.ts',
'src/providers/antigravity/index.ts',
],
format: ['esm'],
target: 'node20',