Skip to content

Commit d54f21d

Browse files
fix(antigravity): read current agy antigravity-cli on-disk layout (#541)
* fix(antigravity): read current agy antigravity-cli on-disk layout * fix(antigravity): propagate SQLITE_BUSY from the .db read so the run retries --------- Co-authored-by: AgentSeal <hello@agentseal.org>
1 parent 4dcb7e6 commit d54f21d

4 files changed

Lines changed: 434 additions & 25 deletions

File tree

src/providers/antigravity.ts

Lines changed: 276 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'url'
77
import https from 'https'
88

99
import { calculateCost } from '../models.js'
10-
import { isSqliteAvailable, openDatabase } from '../sqlite.js'
10+
import { isSqliteAvailable, isSqliteBusyError, openDatabase } from '../sqlite.js'
1111
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
1212

1313
type 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+
140157
const cachedServers = new Map<string, ServerInfo | null>()
141158
const cachedModelMaps = new Map<string, ModelMap>()
142159
let memCache: AntigravityCache | null = null
143160
let cacheDirty = false
144161
let httpsAgent: https.Agent | undefined
162+
const protoTextDecoder = new TextDecoder('utf-8', { fatal: false })
145163

146164
const SERVER_PORT_FLAGS = ['https_server_port', 'extension_server_port', 'https-server-port', 'extension-server-port']
147165
const 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

284302
export 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+
547774
function 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+
9571200
function 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] = {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{"step_index":0,"source":"USER_EXPLICIT","type":"USER_INPUT","status":"DONE","created_at":"2026-06-21T13:32:13Z","content":"<USER_REQUEST>sanitized fixture prompt</USER_REQUEST>"}
2+
{"step_index":1,"source":"MODEL","type":"PLANNER_RESPONSE","status":"DONE","created_at":"2026-06-21T13:32:14Z","content":"sanitized fixture response","thinking":"sanitized fixture reasoning"}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"conversationId": "fixture-current-cli",
3+
"layout": "~/.gemini/antigravity-cli/conversations/<conversation-id>.db with sibling brain/<conversation-id>/.system_generated/logs/transcript.jsonl",
4+
"rows": [
5+
{
6+
"idx": 0,
7+
"hex": "120202032213666978747572652d63757272656e742d636c690ace01222508f80710b9ec0118da05301848930550475a12666978747572652d726573706f6e73652d319a011267656d696e692d70726f2d64656661756c74a201230a0d7472616a6563746f72795f69641212666978747572652d7472616a6563746f7279a201140a0b757365645f636c61756465120566616c7365a201140a0f6c6173745f737465705f696e646578120132a201230a0a6d6f64656c5f656e756d12154d4f44454c5f504c414345484f4c4445525f4d3136aa011547656d696e6920332e312050726f20284869676829"
8+
}
9+
]
10+
}

0 commit comments

Comments
 (0)