Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/providers/antigravity.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ CodeBurn discovers Antigravity sessions from local directories on disk, then que
1. **Session Discovery:** It scans the following folders for `.pb` or `.db` files:
- **Antigravity CLI:** `%USERPROFILE%\.gemini\antigravity-cli\conversations` (and `implicit`)
- **Antigravity App/older path:** `%USERPROFILE%\.gemini\antigravity\conversations`
- **Antigravity IDE (VSCode-based):** `%USERPROFILE%\.gemini\antigravity-ide\conversations` (and `implicit`)
- **Antigravity IDE:** `%USERPROFILE%\.gemini\antigravity-ide\conversations` (and `implicit`). The IDE also maintains VSCode-style global state at `%APPDATA%\Antigravity IDE\User\globalStorage\state.vscdb`, but that DB stores trajectory metadata (titles, timestamps, workspace paths) — not token usage. Token usage data still comes from the `.db` conversation files.
2. **Language Server RPC Query:** It locates the active language-server process via `ps` on POSIX or `Get-CimInstance Win32_Process` on Windows. It extracts the port and CSRF token from the process arguments, and queries the local HTTPS RPC endpoint `GetCascadeTrajectoryGeneratorMetadata` to parse the session.
3. **Cache Fallback:** If the language server is not running, it falls back to the local results cache.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1496,9 +1496,10 @@ program
try {
if (action === 'install') {
const result = await installAntigravityStatusLineHook(!!opts.force)
console.log(result === 'already-installed'
? '\n Antigravity CLI usage capture is already installed.\n'
: '\n Antigravity CLI usage capture installed.\n')
const headline = result === 'already-installed'
? 'Antigravity CLI usage capture is already installed.'
: 'Antigravity CLI usage capture installed.'
console.log(`\n ${headline}\n Note: this captures CLI (agy) sessions only. IDE sessions are read from .db files automatically.\n`)
return
}
if (action === 'uninstall') {
Expand Down
154 changes: 118 additions & 36 deletions src/providers/antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,39 @@ type AntigravityConversationRoot = {
extensions: readonly string[]
}

const CONVERSATION_ROOTS: readonly AntigravityConversationRoot[] = [
{
dir: join(homedir(), '.gemini', 'antigravity', 'conversations'),
project: 'antigravity',
extensions: ['.pb', '.db'],
},
{
dir: join(homedir(), '.gemini', 'antigravity-cli', 'conversations'),
project: 'antigravity-cli',
extensions: ['.pb', '.db'],
},
{
dir: join(homedir(), '.gemini', 'antigravity-cli', 'implicit'),
project: 'antigravity-cli',
extensions: ['.pb'],
},
{
dir: join(homedir(), '.gemini', 'antigravity-ide', 'conversations'),
project: 'antigravity-ide',
extensions: ['.pb', '.db'],
},
{
dir: join(homedir(), '.gemini', 'antigravity-ide', 'implicit'),
project: 'antigravity-ide',
extensions: ['.pb'],
},
] as const
const CACHE_VERSION = 2
// Computed on each call rather than frozen at module load so discovery honors
// the current home directory (env overrides in tests, and any runtime change).
function conversationRoots(): readonly AntigravityConversationRoot[] {
const home = homedir()
return [
{
dir: join(home, '.gemini', 'antigravity', 'conversations'),
project: 'antigravity',
extensions: ['.pb', '.db'],
},
{
dir: join(home, '.gemini', 'antigravity-cli', 'conversations'),
project: 'antigravity-cli',
extensions: ['.pb', '.db'],
},
{
dir: join(home, '.gemini', 'antigravity-cli', 'implicit'),
project: 'antigravity-cli',
extensions: ['.pb'],
},
{
dir: join(home, '.gemini', 'antigravity-ide', 'conversations'),
project: 'antigravity-ide',
extensions: ['.pb', '.db'],
},
{
dir: join(home, '.gemini', 'antigravity-ide', 'implicit'),
project: 'antigravity-ide',
extensions: ['.pb'],
},
]
}
const CACHE_VERSION = 5

const RPC_TIMEOUT_MS = 5000
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024
Expand Down Expand Up @@ -690,6 +695,40 @@ function antigravitySqliteModel(chatFields: readonly ProtoField[]): string {
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())
Expand Down Expand Up @@ -729,7 +768,7 @@ function buildCallFromSqliteGenMetadataRow(cascadeId: string, row: AntigravityGe
costUSD,
tools: [],
bashCommands: [],
timestamp: '',
timestamp: antigravitySqliteCreatedAt(chatFields),
speed: 'standard',
deduplicationKey: `antigravity:${cascadeId}:${responseId}`,
userMessage: '',
Expand Down Expand Up @@ -880,11 +919,14 @@ export function isAntigravityStatusLineEventsPath(path: string): boolean {
}

export async function discoverAntigravitySessionSources(
roots: readonly AntigravityConversationRoot[] = CONVERSATION_ROOTS,
roots?: readonly AntigravityConversationRoot[],
): Promise<SessionSource[]> {
const includeStatusLineEvents = roots === CONVERSATION_ROOTS
// The statusline JSONL is a synthetic source only appended for the real
// default roots, not when a caller passes an explicit (test) root set.
const includeStatusLineEvents = roots === undefined
const effectiveRoots = roots ?? conversationRoots()
const sources: SessionSource[] = []
for (const root of roots) {
for (const root of effectiveRoots) {
let files: string[]
try {
files = await readdir(root.dir)
Expand Down Expand Up @@ -1134,10 +1176,12 @@ export async function snapshotAntigravityStatusLinePayload(input: unknown): Prom
metadata = extractAntigravityGeneratorMetadata(
await rpc(server, 'GetCascadeTrajectoryGeneratorMetadata', { cascadeId }),
)
const snapshotCalls = buildCallsFromGeneratorMetadata(cascadeId, metadata, modelMap)
assignStableTimestamps(snapshotCalls, cached?.calls, new Date(s.mtimeMs).toISOString())
cache.cascades[cascadeId] = {
mtimeMs: s.mtimeMs,
sizeBytes: s.size,
calls: buildCallsFromGeneratorMetadata(cascadeId, metadata, modelMap),
calls: snapshotCalls,
}
cacheDirty = true
await flushCache()
Expand Down Expand Up @@ -1197,6 +1241,41 @@ 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.
//
// assignStableTimestamps carries forward the timestamp already recorded for a
// dedup key (its first-seen time, held in the durable Antigravity cache) and
// only falls back to the current file mtime for genuinely new calls. Real
// timestamps (created_at) are preserved untouched. This runs on the fresh-parse
// paths whose result is written back to the cache.
function assignStableTimestamps(
calls: ParsedProviderCall[],
priorCalls: readonly ParsedProviderCall[] | undefined,
firstSeenTimestamp: string,
): void {
const priorByKey = new Map<string, string>()
for (const prior of priorCalls ?? []) {
if (prior.timestamp) priorByKey.set(prior.deduplicationKey, prior.timestamp)
}
for (const call of calls) {
if (call.timestamp) continue
call.timestamp = priorByKey.get(call.deduplicationKey) ?? firstSeenTimestamp
}
}

// Emit-time safety net for cache-hit / cached-fallback paths, where the calls
// already carry stable timestamps from a prior parse. Applied to a copy so the
// cache is never mutated; only fills a still-empty timestamp defensively.
function withFallbackTimestamp(call: ParsedProviderCall, fallbackTimestamp: string): ParsedProviderCall {
return call.timestamp ? call : { ...call, timestamp: fallbackTimestamp }
}

function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
return {
async *parse(): AsyncGenerator<ParsedProviderCall> {
Expand All @@ -1215,20 +1294,22 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
if (!s) return

const projectPath = await extractWorkspacePath(source.path)
const fallbackTimestamp = new Date(s.mtimeMs).toISOString()

const cached = cache.cascades[cascadeId]
if (cached && cached.mtimeMs === s.mtimeMs && cached.sizeBytes === s.size && cached.calls.length > 0) {
for (const call of cached.calls) {
applyAntigravityProject(call, source, projectPath)
if (seenKeys.has(call.deduplicationKey)) continue
seenKeys.add(call.deduplicationKey)
yield call
yield withFallbackTimestamp(call, fallbackTimestamp)
}
return
}

const sqliteResults = await parseSqliteGenMetadataCalls(source.path, cascadeId)
if (sqliteResults.length > 0) {
assignStableTimestamps(sqliteResults, cached?.calls, fallbackTimestamp)
for (const call of sqliteResults) {
applyAntigravityProject(call, source, projectPath)
}
Expand All @@ -1255,7 +1336,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
applyAntigravityProject(call, source, projectPath)
if (seenKeys.has(call.deduplicationKey)) continue
seenKeys.add(call.deduplicationKey)
yield call
yield withFallbackTimestamp(call, fallbackTimestamp)
}
}
return
Expand All @@ -1274,13 +1355,14 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
applyAntigravityProject(call, source, projectPath)
if (seenKeys.has(call.deduplicationKey)) continue
seenKeys.add(call.deduplicationKey)
yield call
yield withFallbackTimestamp(call, fallbackTimestamp)
}
}
return
}

const results = buildCallsFromGeneratorMetadata(cascadeId, metadata, modelMap)
assignStableTimestamps(results, cached?.calls, fallbackTimestamp)
for (const call of results) {
applyAntigravityProject(call, source, projectPath)
}
Expand Down
2 changes: 1 addition & 1 deletion src/session-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const PROVIDER_PARSE_VERSIONS: Record<string, string> = {
'kilo-code': 'worktree-project-grouping-v1',
'roo-code': 'worktree-project-grouping-v1',
warp: 'worktree-project-grouping-v1',
antigravity: 'worktree-project-grouping-v3',
antigravity: 'worktree-project-grouping-v5',
}

// ── Cache Dir ──────────────────────────────────────────────────────────
Expand Down
Loading
Loading