Skip to content

Support Antigravity CLI sessions#73

Open
sebastianbarrozo wants to merge 1 commit into
yigitkonur:mainfrom
sebastianbarrozo:codex/support-antigravity-cli
Open

Support Antigravity CLI sessions#73
sebastianbarrozo wants to merge 1 commit into
yigitkonur:mainfrom
sebastianbarrozo:codex/support-antigravity-cli

Conversation

@sebastianbarrozo

@sebastianbarrozo sebastianbarrozo commented Jun 4, 2026

Copy link
Copy Markdown

Summary

  • add Antigravity CLI session discovery from ~/.gemini/antigravity-cli/conversations/*.db
  • extract CLI context from SQLite steps with read-only immutable access and best-effort protobuf/JSON string decoding
  • update the Antigravity adapter to prefer agy, support native --conversation resume, and map handoff flags
  • document the Antigravity CLI storage format and access recipe

Validation

  • pnpm test
  • pnpm run build
  • pnpm exec biome check src/parsers/antigravity.ts src/parsers/registry.ts src/tests/antigravity-parser.test.ts src/tests/forward-flags.test.ts

Note: pnpm run check still reports pre-existing unrelated Biome diagnostics outside this change set.


Summary by cubic

Adds first-class support for Antigravity CLI conversations from SQLite, extracting messages and tool activity without launching the IDE. Also switches the adapter to prefer agy with native --conversation resume and clearer flag mapping.

  • New Features

    • Discover CLI sessions from ~/.gemini/antigravity-cli/conversations/*.db and the CLI brain dir; IDE conversations/*.pb and state summaries remain supported.
    • Extract context from SQLite steps in read-only immutable mode via node:sqlite; decode protobuf/JSON strings to messages and tool activity; infer CWD from cache/last_conversations.json or file:// URIs; derive titles and step counts.
    • Adapter updates: prefer agy (fallback antigravity), support --conversation <id>, map auto-approve → --dangerously-skip-permissions, normalize --sandbox, and forward --model with warnings for unsupported approval/permission modes; registry now accepts ANTIGRAVITY_CLI_HOME. Docs updated for CLI storage/access; tests added for CLI parser and flag forwarding.
  • Migration

    • Ensure agy is on PATH; antigravity remains a fallback.
    • If using a custom CLI home, set ANTIGRAVITY_CLI_HOME (parser also respects GEMINI_CLI_HOME).
    • Resume a session with: agy --conversation <conversation-id> (or antigravity --conversation <conversation-id>).

Written for commit f4e050e. Summary will update on new commits.

Review in cubic

@sebastianbarrozo
sebastianbarrozo force-pushed the codex/support-antigravity-cli branch from b22da1a to f4e050e Compare June 4, 2026 15:29
@sebastianbarrozo sebastianbarrozo changed the title [codex] Support Antigravity CLI sessions Support Antigravity CLI sessions Jun 5, 2026
@sebastianbarrozo
sebastianbarrozo marked this pull request as ready for review June 5, 2026 01:48

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 8 potential issues.

⚠️ 1 issue in files not directly in the diff

⚠️ REVIEW.md rule violation: parser logic changed without updating fixture data in fixtures/index.ts (src/__tests__/fixtures/index.ts:968-998)

REVIEW.md mandates: "PRs that change parser logic must include or update fixture data in src/__tests__/fixtures/index.ts." This PR adds significant new parser logic (CLI SQLite conversation discovery/extraction, new readCliSteps, extractFromCliDbSteps, etc.) but does not update createAntigravityFixture() in src/__tests__/fixtures/index.ts to include CLI conversation data. As a result, the conversion tests in unit-conversions.test.ts do not exercise the new CLI conversation code path at all — they still only test the brain-artifact offline path.

Open in Devin Review

const uriMatch = text.match(/file:\/\/[^\s"')]+/u)?.[0];
if (uriMatch) {
const cwd = decodeFileUri(uriMatch);
if (cwd) return path.dirname(cwd);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 extractCliCwdFromStrings incorrectly applies path.dirname to decoded file URIs, stripping one directory level

The new extractCliCwdFromStrings function at src/parsers/antigravity.ts:912 calls path.dirname(cwd) on the result of decodeFileUri, which strips one directory level from the path. If a file:// URI references a workspace root (e.g. file:///home/user/project), the function returns /home/user instead of /home/user/project. This is inconsistent with every other URI-to-CWD function in the same file: inferCwdFromBrain (src/parsers/antigravity.ts:1365), extractFolderFromSummaryProto (src/parsers/antigravity.ts:656), and normalizeCwd (src/parsers/antigravity.ts:289) all return the decoded URI directly without calling path.dirname. The bug is mitigated in practice because extractCliCwdFromStrings is a fallback behind loadCliLastConversationCwds, but when the fallback is reached, the CWD will be one level too high.

Suggested change
if (cwd) return path.dirname(cwd);
if (cwd) return cwd;
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

[record.conversationPath, record.cliConversationPath, ...artifactPaths].filter(isNonEmptyString),
);
const cliSteps = record.cliConversationPath ? readCliSteps(record.cliConversationPath).slice(0, 40) : [];
const cliCwds = record.cliConversationPath ? await loadCliLastConversationCwds() : new Map<string, string>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: loadCliLastConversationCwds() is re-read from disk for every CLI conversation record during session discovery

In buildSessionFromRecord at src/parsers/antigravity.ts:1414, loadCliLastConversationCwds() reads and JSON-parses antigravity-cli/cache/last_conversations.json for each record that has a cliConversationPath. If there are N CLI conversations, the same file is read N times. This could be hoisted to parseAntigravitySessions() and passed into buildSessionFromRecord, or cached at module level with a TTL. Not a correctness bug, but worth noting for repos with many CLI conversations.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1413 to +1434
const cliSteps = record.cliConversationPath ? readCliSteps(record.cliConversationPath).slice(0, 40) : [];
const cliCwds = record.cliConversationPath ? await loadCliLastConversationCwds() : new Map<string, string>();
const cliStrings = cliSteps.flatMap(stringsFromStep);
const cliCwd = cliCwds.get(record.id) ?? extractCliCwdFromStrings(cliStrings);
const title = recordPreferredTitle(record) ?? (await titleFromBrain(record.brainDir)) ?? titleFromCliSteps(cliSteps);
const cwd = record.live?.cwd ?? record.state?.cwd ?? (await inferCwdFromBrain(record.brainDir)) ?? cliCwd ?? '';
const createdAt =
record.live?.createdAt ?? record.state?.createdAt ?? stats.createdAt ?? stats.updatedAt ?? new Date(0);
const updatedAt = record.live?.updatedAt ?? record.state?.updatedAt ?? stats.updatedAt ?? createdAt;
const summary = cleanSummary(title ?? `Antigravity conversation ${record.id.slice(0, 8)}`, 80);
const originalPath = record.conversationPath ?? record.brainDir ?? getAntigravityRoot();
const originalPath = record.cliConversationPath ?? record.conversationPath ?? record.brainDir ?? getAntigravityRoot();

return {
id: record.id,
source: SOURCE_NAME,
cwd,
repo: extractRepoFromCwd(cwd),
lines: record.live?.stepCount ?? record.state?.stepCount ?? artifactPaths.length,
lines:
record.live?.stepCount ??
record.state?.stepCount ??
(record.cliConversationPath ? readCliStepCount(record.cliConversationPath) : undefined) ??
artifactPaths.length,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: readCliSteps reads all rows with all blob columns during session discovery, then slices to 40

At src/parsers/antigravity.ts:1413, readCliSteps(record.cliConversationPath).slice(0, 40) opens the conversation DB synchronously, reads ALL step rows (including large blob columns: metadata, task_details, render_info, step_payload), returns them all, then the caller slices to 40. Additionally, readCliStepCount at line 1433 opens the same DB again to SELECT COUNT(*). The query in readCliSteps could use LIMIT 40 to avoid loading unbounded data, and the count could be derived from cliSteps.length if the full set had already been read (or vice versa). The synchronous SQLite usage is consistent with the pre-existing readStateValue/openDb pattern, so it's not a new anti-pattern, but the double-open and full-table-scan-then-slice is worth optimizing for conversations with many steps.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/parsers/registry.ts
Comment on lines +212 to +267
function mapAntigravityFlags(context: ForwardFlagMapContext): ForwardMapResult {
const args: string[] = [];
const warnings: string[] = [];

const autoOccurrences = collectAutoApproveOccurrences(context);
const fullAutoOccurrences = context.all('fullAuto');
const askOccurrences = context.all('askForApproval');
const approvalModeOccurrences = context.all('approvalMode');
const permissionModeOccurrences = context.all('permissionMode');
const approvalMode = context.latestString('approvalMode')?.toLowerCase();
const askForApproval = context.latestString('askForApproval')?.toLowerCase();

if (
autoOccurrences.length > 0 ||
fullAutoOccurrences.length > 0 ||
approvalMode === 'yolo' ||
askForApproval === 'never'
) {
context.consume(
...autoOccurrences,
...fullAutoOccurrences,
...askOccurrences,
...approvalModeOccurrences,
...permissionModeOccurrences,
);
args.push('--dangerously-skip-permissions');

if (approvalModeOccurrences.length > 0 && approvalMode && approvalMode !== 'yolo') {
warnings.push('Antigravity: mapped auto-approve behavior overrides unsupported approval-mode values.');
}
} else if (askOccurrences.length > 0 || approvalModeOccurrences.length > 0 || permissionModeOccurrences.length > 0) {
context.consume(...askOccurrences, ...approvalModeOccurrences, ...permissionModeOccurrences);
warnings.push('Antigravity: approval and permission mode forwarding flags are not supported and were ignored.');
}

const sandbox = context.latest('sandbox');
if (sandbox) {
context.consumeKeys('sandbox');
const normalized = String(sandbox.value).toLowerCase();
if (sandbox.value === true || ['true', '1', 'yes', 'on', 'enabled'].includes(normalized)) {
args.push('--sandbox=true');
} else if (['false', '0', 'no', 'off', 'disabled', 'none'].includes(normalized)) {
args.push('--sandbox=false');
} else {
args.push('--sandbox', String(sandbox.value));
}
}

const model = context.latestString('model');
if (model) {
context.consumeKeys('model');
args.push('--model', model);
}

return { mappedArgs: args, warnings };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Antigravity flag mapping correctly gates --dangerously-skip-permissions behind explicit auto-approve signals

The mapAntigravityFlags function at src/parsers/registry.ts:212-267 only emits --dangerously-skip-permissions when the user explicitly passed auto-approve flags (--yolo, --allow-all, --dangerously-*), --full-auto, --approval-mode yolo, or --ask-for-approval never. Non-yolo approval modes and permission modes are consumed with a warning. This is consistent with REVIEW.md's security requirement that dangerous flags are "only set when the source session explicitly requested auto-approve behavior." The sandbox forwarding correctly normalizes boolean-like values into --sandbox=true/--sandbox=false.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +460 to +479
function immutableSqliteUri(dbPath: string): string {
const url = pathToFileURL(dbPath);
url.searchParams.set('mode', 'ro');
url.searchParams.set('immutable', '1');
return url.toString();
}

function openCliConversationDb(dbPath: string): { db: SqliteDatabase; close: () => void } | null {
try {
const require = createRequire(import.meta.url);
const sqliteModule = require('node:sqlite') as {
DatabaseSync: new (database: string, options?: { open?: boolean; readOnly?: boolean }) => SqliteDatabase;
};
const db = new sqliteModule.DatabaseSync(immutableSqliteUri(dbPath), { open: true, readOnly: true });
return { db, close: () => db.close() };
} catch (err) {
logger.debug('antigravity: failed to open cli conversation database', dbPath, err);
return null;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: immutableSqliteUri relies on Node.js node:sqlite supporting URI filenames

The openCliConversationDb function at src/parsers/antigravity.ts:467-479 passes a file://...?mode=ro&immutable=1 URI to DatabaseSync. This works because Node.js 22's node:sqlite wraps sqlite3_open_v2 with the SQLITE_OPEN_URI flag enabled. The immutable=1 parameter tells SQLite the DB won't change during the connection, enabling read optimizations (no rollback journal checks). This is a nice safety net on top of the readOnly: true option. If Node.js ever changes its URI support, the catch block at line 475 would gracefully degrade to returning null.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const updatedAt = record.live?.updatedAt ?? record.state?.updatedAt ?? stats.updatedAt ?? createdAt;
const summary = cleanSummary(title ?? `Antigravity conversation ${record.id.slice(0, 8)}`, 80);
const originalPath = record.conversationPath ?? record.brainDir ?? getAntigravityRoot();
const originalPath = record.cliConversationPath ?? record.conversationPath ?? record.brainDir ?? getAntigravityRoot();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 CLI conversation path takes precedence over IDE path for originalPath, changing extraction behavior for dual-source sessions

At src/parsers/antigravity.ts:1423, originalPath prefers cliConversationPath over conversationPath. In extractAntigravityContext at line 2069, extractCliDbContext checks session.originalPath.endsWith('.db') to decide whether to use CLI DB extraction. This means if a session UUID exists in both ~/.gemini/antigravity-cli/conversations/<id>.db AND ~/.gemini/antigravity/conversations/<id>.pb, the CLI DB extraction is used and live RPC extraction from the IDE is skipped entirely. This is likely intentional (CLI data is directly readable without requiring the IDE), but it's a behavioral choice worth documenting if dual-source sessions are possible.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +849 to +861
function textLooksUseful(value: string): boolean {
if (value.length < 8) return false;
if (UUIDISH_RE.test(value.replace(/^\$/u, ''))) return false;
if (UUIDISH_ANYWHERE_RE.test(value)) return false;
if (value.includes('sessionID')) return false;
if (/\b(command|execute_url|read_url|mcp)\(\*\)/u.test(value)) return false;
if (/^(sessionID|trajectory_id|model_enum|toolAction|toolSummary|CommandLine|Cwd|DirectoryPath)$/u.test(value)) {
return false;
}
if (/^[A-Za-z_]+\(.\)$/u.test(value)) return false;
if (/^[A-Za-z0-9_-]{8,}$/u.test(value) && !value.includes(' ')) return false;
return /[A-Za-z][A-Za-z]/u.test(value);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: textLooksUseful aggressively filters strings containing UUIDs anywhere

At src/parsers/antigravity.ts:852, UUIDISH_ANYWHERE_RE.test(value) rejects any string containing a UUID-like pattern anywhere in it. This could filter out meaningful messages that happen to reference a UUID (e.g., "Fixed handler for session abc12345-..."). The filter is part of best-effort protobuf string extraction where false positives (protobuf field IDs that look like UUIDs) are more common than false negatives, so the aggressive filtering is a reasonable tradeoff for the CLI conversation decoding use case.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant