@@ -7,7 +7,7 @@ import { fileURLToPath } from 'url'
77import https from 'https'
88
99import { calculateCost } from '../models.js'
10- import { isSqliteAvailable , openDatabase } from '../sqlite.js'
10+ import { isSqliteAvailable , isSqliteBusyError , openDatabase } from '../sqlite.js'
1111import type { Provider , SessionSource , SessionParser , ParsedProviderCall } from './types.js'
1212
1313type AntigravityConversationRoot = {
@@ -25,7 +25,7 @@ const CONVERSATION_ROOTS: readonly AntigravityConversationRoot[] = [
2525 {
2626 dir : join ( homedir ( ) , '.gemini' , 'antigravity-cli' , 'conversations' ) ,
2727 project : 'antigravity-cli' ,
28- extensions : [ '.pb' ] ,
28+ extensions : [ '.pb' , '.db' ] ,
2929 } ,
3030 {
3131 dir : join ( homedir ( ) , '.gemini' , 'antigravity-cli' , 'implicit' ) ,
@@ -137,11 +137,29 @@ type AntigravityCache = {
137137 cascades : Record < string , CachedCascade >
138138}
139139
140+ type ProtoField = {
141+ number : number
142+ wireType : number
143+ value ?: bigint
144+ bytes ?: Uint8Array
145+ }
146+
147+ type ProtoVarint = {
148+ value : bigint
149+ offset : number
150+ }
151+
152+ type AntigravityGenMetadataRow = {
153+ idx : number
154+ data : Uint8Array | string
155+ }
156+
140157const cachedServers = new Map < string , ServerInfo | null > ( )
141158const cachedModelMaps = new Map < string , ModelMap > ( )
142159let memCache : AntigravityCache | null = null
143160let cacheDirty = false
144161let httpsAgent : https . Agent | undefined
162+ const protoTextDecoder = new TextDecoder ( 'utf-8' , { fatal : false } )
145163
146164const SERVER_PORT_FLAGS = [ 'https_server_port' , 'extension_server_port' , 'https-server-port' , 'extension-server-port' ]
147165const CSRF_TOKEN_FLAGS = [ 'csrf_token' , 'extension_server_csrf_token' , 'csrf-token' , 'extension-server-csrf-token' ]
@@ -270,15 +288,15 @@ export function extractAntigravityModelMap(resp: unknown): ModelMap {
270288 if ( ! resp || typeof resp !== 'object' ) return { }
271289 const data = resp as ModelMapResponse
272290 const models = data . response ?. models ?? data . models
273- const map : ModelMap = { }
274- if ( ! models ) return map
291+ const map = new Map < string , string > ( )
292+ if ( ! models ) return { }
275293 for ( const [ key , info ] of Object . entries ( models ) ) {
276294 if ( info && typeof info === 'object' && typeof info . model === 'string' ) {
277295 const canonicalKey = getCanonicalModelId ( key , info . displayName )
278- map [ info . model ] = canonicalKey
296+ map . set ( info . model , canonicalKey )
279297 }
280298 }
281- return map
299+ return Object . fromEntries ( map )
282300}
283301
284302export function extractAntigravityGeneratorMetadata ( resp : unknown ) : GeneratorMetadata [ ] {
@@ -544,6 +562,215 @@ function normalizePricingModel(model: string): string {
544562 return PRICING_ALIASES [ stripped ] ?? stripped
545563}
546564
565+ function readProtoVarint ( data : Uint8Array , startOffset : number ) : ProtoVarint | null {
566+ let value = 0n
567+ let shift = 0n
568+ let offset = startOffset
569+
570+ while ( offset < data . length ) {
571+ const byte = BigInt ( data [ offset ] ! )
572+ offset += 1
573+ value |= ( byte & 0x7fn ) << shift
574+ if ( ( byte & 0x80n ) === 0n ) return { value, offset }
575+ shift += 7n
576+ if ( shift > 70n ) return null
577+ }
578+
579+ return null
580+ }
581+
582+ function parseProtoFields ( data : Uint8Array ) : ProtoField [ ] {
583+ const fields : ProtoField [ ] = [ ]
584+ let offset = 0
585+
586+ while ( offset < data . length ) {
587+ const key = readProtoVarint ( data , offset )
588+ if ( ! key ) break
589+ offset = key . offset
590+
591+ const fieldNumber = Number ( key . value >> 3n )
592+ const wireType = Number ( key . value & 0x7n )
593+ if ( ! Number . isSafeInteger ( fieldNumber ) || fieldNumber <= 0 ) break
594+
595+ if ( wireType === 0 ) {
596+ const value = readProtoVarint ( data , offset )
597+ if ( ! value ) break
598+ fields . push ( { number : fieldNumber , wireType, value : value . value } )
599+ offset = value . offset
600+ continue
601+ }
602+
603+ if ( wireType === 1 ) {
604+ if ( offset + 8 > data . length ) break
605+ fields . push ( { number : fieldNumber , wireType, bytes : data . subarray ( offset , offset + 8 ) } )
606+ offset += 8
607+ continue
608+ }
609+
610+ if ( wireType === 2 ) {
611+ const length = readProtoVarint ( data , offset )
612+ if ( ! length ) break
613+ offset = length . offset
614+ const byteLength = Number ( length . value )
615+ if ( ! Number . isSafeInteger ( byteLength ) || byteLength < 0 || offset + byteLength > data . length ) break
616+ fields . push ( { number : fieldNumber , wireType, bytes : data . subarray ( offset , offset + byteLength ) } )
617+ offset += byteLength
618+ continue
619+ }
620+
621+ if ( wireType === 5 ) {
622+ if ( offset + 4 > data . length ) break
623+ fields . push ( { number : fieldNumber , wireType, bytes : data . subarray ( offset , offset + 4 ) } )
624+ offset += 4
625+ continue
626+ }
627+
628+ break
629+ }
630+
631+ return fields
632+ }
633+
634+ function firstProtoField ( fields : readonly ProtoField [ ] , fieldNumber : number ) : ProtoField | undefined {
635+ return fields . find ( field => field . number === fieldNumber )
636+ }
637+
638+ function protoFieldText ( field : ProtoField | undefined ) : string | undefined {
639+ if ( ! field ?. bytes || field . bytes . length === 0 ) return undefined
640+ const text = protoTextDecoder . decode ( field . bytes )
641+ if ( ! text || / [ \u0000 - \u0008 \u000E - \u001F \u007F \uFFFD ] / . test ( text ) ) return undefined
642+ return text
643+ }
644+
645+ function protoFieldPositiveInteger ( field : ProtoField | undefined ) : number {
646+ if ( field ?. value === undefined ) return 0
647+ const value = Number ( field . value )
648+ return Number . isSafeInteger ( value ) && value > 0 ? value : 0
649+ }
650+
651+ function protoFieldBytes ( field : ProtoField | undefined ) : Uint8Array | undefined {
652+ return field ?. bytes
653+ }
654+
655+ function isAntigravityResponseId ( value : string ) : boolean {
656+ return / ^ [ ^ \s ] + $ / . test ( value )
657+ }
658+
659+ function antigravitySqliteResponseId ( usageFields : readonly ProtoField [ ] , fallback : string ) : string {
660+ const responseId = protoFieldText ( firstProtoField ( usageFields , 11 ) )
661+ return responseId && isAntigravityResponseId ( responseId ) ? responseId : fallback
662+ }
663+
664+ function genMetadataDataBytes ( value : Uint8Array | string ) : Uint8Array {
665+ return typeof value === 'string'
666+ ? new TextEncoder ( ) . encode ( value )
667+ : value
668+ }
669+
670+ function antigravitySqliteMetadataAttributes ( chatFields : readonly ProtoField [ ] ) : Map < string , string > {
671+ const attributes = new Map < string , string > ( )
672+ for ( const field of chatFields ) {
673+ if ( field . number !== 20 ) continue
674+ const pairFields = parseProtoFields ( protoFieldBytes ( field ) ?? new Uint8Array ( ) )
675+ const key = protoFieldText ( firstProtoField ( pairFields , 1 ) )
676+ const value = protoFieldText ( firstProtoField ( pairFields , 2 ) )
677+ if ( key && value ) attributes . set ( key , value )
678+ }
679+ return attributes
680+ }
681+
682+ function antigravitySqliteModel ( chatFields : readonly ProtoField [ ] ) : string {
683+ const attributes = antigravitySqliteMetadataAttributes ( chatFields )
684+ const displayName = protoFieldText ( firstProtoField ( chatFields , 21 ) )
685+ const rawModel = protoFieldText ( firstProtoField ( chatFields , 19 ) )
686+ ?? attributes . get ( 'model_enum' )
687+ ?? displayName
688+ ?? 'unknown'
689+
690+ return getCanonicalModelId ( rawModel , displayName )
691+ }
692+
693+ function buildCallFromSqliteGenMetadataRow ( cascadeId : string , row : AntigravityGenMetadataRow ) : ParsedProviderCall | null {
694+ const rootFields = parseProtoFields ( genMetadataDataBytes ( row . data ) )
695+ const chatFields = parseProtoFields ( protoFieldBytes ( firstProtoField ( rootFields , 1 ) ) ?? new Uint8Array ( ) )
696+ const usageFields = parseProtoFields ( protoFieldBytes ( firstProtoField ( chatFields , 4 ) ) ?? new Uint8Array ( ) )
697+ if ( usageFields . length === 0 ) return null
698+
699+ const inputTokens = protoFieldPositiveInteger ( firstProtoField ( usageFields , 2 ) )
700+ || protoFieldPositiveInteger ( firstProtoField ( usageFields , 1 ) )
701+ const totalOutputTokens = protoFieldPositiveInteger ( firstProtoField ( usageFields , 3 ) )
702+ let responseTokens = protoFieldPositiveInteger ( firstProtoField ( usageFields , 9 ) )
703+ let thinkingTokens = protoFieldPositiveInteger ( firstProtoField ( usageFields , 10 ) )
704+
705+ if ( responseTokens === 0 && thinkingTokens === 0 ) {
706+ responseTokens = totalOutputTokens
707+ } else if ( totalOutputTokens > 0 && responseTokens + thinkingTokens !== totalOutputTokens ) {
708+ const adjustedResponseTokens = totalOutputTokens - thinkingTokens
709+ if ( adjustedResponseTokens >= 0 ) responseTokens = adjustedResponseTokens
710+ }
711+
712+ if ( inputTokens === 0 && totalOutputTokens === 0 ) return null
713+
714+ const responseId = antigravitySqliteResponseId ( usageFields , String ( row . idx ) )
715+ const model = antigravitySqliteModel ( chatFields )
716+ const pricingModel = normalizePricingModel ( model )
717+ const costUSD = calculateCost ( pricingModel , inputTokens , responseTokens + thinkingTokens , 0 , 0 , 0 )
718+
719+ return {
720+ provider : 'antigravity' ,
721+ model,
722+ inputTokens,
723+ outputTokens : responseTokens ,
724+ cacheCreationInputTokens : 0 ,
725+ cacheReadInputTokens : 0 ,
726+ cachedInputTokens : 0 ,
727+ reasoningTokens : thinkingTokens ,
728+ webSearchRequests : 0 ,
729+ costUSD,
730+ tools : [ ] ,
731+ bashCommands : [ ] ,
732+ timestamp : '' ,
733+ speed : 'standard' ,
734+ deduplicationKey : `antigravity:${ cascadeId } :${ responseId } ` ,
735+ userMessage : '' ,
736+ sessionId : cascadeId ,
737+ }
738+ }
739+
740+ function buildCallsFromSqliteGenMetadata ( cascadeId : string , rows : AntigravityGenMetadataRow [ ] ) : ParsedProviderCall [ ] {
741+ const calls : ParsedProviderCall [ ] = [ ]
742+ const seenResponseIds = new Set < string > ( )
743+
744+ for ( const row of rows ) {
745+ const call = buildCallFromSqliteGenMetadataRow ( cascadeId , row )
746+ if ( ! call ) continue
747+ if ( seenResponseIds . has ( call . deduplicationKey ) ) continue
748+ seenResponseIds . add ( call . deduplicationKey )
749+ calls . push ( call )
750+ }
751+
752+ return calls
753+ }
754+
755+ async function parseSqliteGenMetadataCalls ( filePath : string , cascadeId : string ) : Promise < ParsedProviderCall [ ] > {
756+ if ( ! filePath . toLowerCase ( ) . endsWith ( '.db' ) ) return [ ]
757+ if ( ! isSqliteAvailable ( ) ) return [ ]
758+
759+ let db : ReturnType < typeof openDatabase > | null = null
760+ try {
761+ db = openDatabase ( filePath )
762+ const rows = db . query < AntigravityGenMetadataRow > ( 'SELECT idx, data FROM gen_metadata ORDER BY idx' )
763+ return buildCallsFromSqliteGenMetadata ( cascadeId , rows )
764+ } catch ( err ) {
765+ // Let a transient lock propagate so the run retries this file on the next
766+ // refresh instead of treating it as empty (see parser.ts busy handling).
767+ if ( isSqliteBusyError ( err ) ) throw err
768+ return [ ]
769+ } finally {
770+ db ?. close ( )
771+ }
772+ }
773+
547774function parseFiniteToken ( value : unknown ) : number {
548775 return typeof value === 'number' && Number . isFinite ( value ) && value > 0
549776 ? Math . floor ( value )
@@ -954,6 +1181,22 @@ function sanitizeProject(path: string): string {
9541181 return basename ( path . replace ( / \\ / g, '/' ) )
9551182}
9561183
1184+ function applyAntigravityProject ( call : ParsedProviderCall , source : SessionSource , projectPath : string | undefined ) : void {
1185+ if ( source . project === 'antigravity-cli' ) {
1186+ call . project = source . project
1187+ delete call . projectPath
1188+ return
1189+ }
1190+
1191+ if ( projectPath ) {
1192+ call . projectPath = projectPath
1193+ call . project = sanitizeProject ( projectPath )
1194+ return
1195+ }
1196+
1197+ call . project = source . project
1198+ }
1199+
9571200function createParser ( source : SessionSource , seenKeys : Set < string > ) : SessionParser {
9581201 return {
9591202 async * parse ( ) : AsyncGenerator < ParsedProviderCall > {
@@ -974,12 +1217,30 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
9741217 const projectPath = await extractWorkspacePath ( source . path )
9751218
9761219 const cached = cache . cascades [ cascadeId ]
977- if ( cached && cached . mtimeMs === s . mtimeMs && cached . sizeBytes === s . size ) {
1220+ if ( cached && cached . mtimeMs === s . mtimeMs && cached . sizeBytes === s . size && cached . calls . length > 0 ) {
9781221 for ( const call of cached . calls ) {
979- if ( projectPath ) {
980- call . projectPath = projectPath
981- call . project = sanitizeProject ( projectPath )
982- }
1222+ applyAntigravityProject ( call , source , projectPath )
1223+ if ( seenKeys . has ( call . deduplicationKey ) ) continue
1224+ seenKeys . add ( call . deduplicationKey )
1225+ yield call
1226+ }
1227+ return
1228+ }
1229+
1230+ const sqliteResults = await parseSqliteGenMetadataCalls ( source . path , cascadeId )
1231+ if ( sqliteResults . length > 0 ) {
1232+ for ( const call of sqliteResults ) {
1233+ applyAntigravityProject ( call , source , projectPath )
1234+ }
1235+
1236+ cache . cascades [ cascadeId ] = {
1237+ mtimeMs : s . mtimeMs ,
1238+ sizeBytes : s . size ,
1239+ calls : sqliteResults ,
1240+ }
1241+ cacheDirty = true
1242+
1243+ for ( const call of sqliteResults ) {
9831244 if ( seenKeys . has ( call . deduplicationKey ) ) continue
9841245 seenKeys . add ( call . deduplicationKey )
9851246 yield call
@@ -991,10 +1252,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
9911252 if ( ! server ) {
9921253 if ( cached ) {
9931254 for ( const call of cached . calls ) {
994- if ( projectPath ) {
995- call . projectPath = projectPath
996- call . project = sanitizeProject ( projectPath )
997- }
1255+ applyAntigravityProject ( call , source , projectPath )
9981256 if ( seenKeys . has ( call . deduplicationKey ) ) continue
9991257 seenKeys . add ( call . deduplicationKey )
10001258 yield call
@@ -1013,10 +1271,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
10131271 } catch {
10141272 if ( cached ) {
10151273 for ( const call of cached . calls ) {
1016- if ( projectPath ) {
1017- call . projectPath = projectPath
1018- call . project = sanitizeProject ( projectPath )
1019- }
1274+ applyAntigravityProject ( call , source , projectPath )
10201275 if ( seenKeys . has ( call . deduplicationKey ) ) continue
10211276 seenKeys . add ( call . deduplicationKey )
10221277 yield call
@@ -1026,12 +1281,8 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
10261281 }
10271282
10281283 const results = buildCallsFromGeneratorMetadata ( cascadeId , metadata , modelMap )
1029- if ( projectPath ) {
1030- const projectName = sanitizeProject ( projectPath )
1031- for ( const call of results ) {
1032- call . projectPath = projectPath
1033- call . project = projectName
1034- }
1284+ for ( const call of results ) {
1285+ applyAntigravityProject ( call , source , projectPath )
10351286 }
10361287
10371288 cache . cascades [ cascadeId ] = {
0 commit comments