How to Migrate Your Conversation History to the New Antigravity IDE (macOS)
Current Situation Analysis
When development tools undergo major architectural splitsāsuch as transitioning from a hosted plugin model to a standalone desktop applicationālocal state directories frequently shift. The Antigravity IDE recently separated from its legacy host application, moving its data root from ~/.gemini/antigravity to ~/.gemini/antigravity-ide. This directory migration breaks the IDE's ability to resolve previously stored conversation archives and workspace metadata.
The issue is frequently misunderstood because developers assume that copying .pb conversation files to the new directory is sufficient. In reality, the IDE is built on a VS Code-compatible core that relies on an internal SQLite database (state.vscdb) to manage UI state, trajectory summaries, and recently opened paths. These databases cache absolute filesystem paths and serialize complex Protobuf payloads. When the application launches, it queries state.vscdb for trajectory identifiers and path references. If those references point to the legacy directory, the sidebar renders empty, even if the raw conversation files exist in the new location.
Furthermore, the IDE employs a write-back synchronization mechanism. When the application closes, it flushes its in-memory state to disk, overwriting any manual database modifications made while the process was running. This creates a race condition where post-launch file copies or database edits are silently discarded. The only reliable resolution requires a coordinated migration that handles file replication, SQLite state merging, Protobuf trajectory reconstruction, and absolute path remappingāall executed while the IDE process is completely terminated.
WOW Moment: Key Findings
The migration challenge isn't about file storage; it's about state resolution. The following comparison highlights why naive file copying fails and why direct database intervention is required.
| Approach | Path Resolution | State Sync Latency | Data Integrity Risk | UI Availability |
|---|---|---|---|---|
| Manual File Copy | Fails (hardcoded legacy refs) | N/A | High (orphaned blobs) | Empty sidebar |
| IDE Native Export/Import | Not exposed | N/A | N/A | Unavailable |
| Direct SQLite + Protobuf Merge | Resolves absolute refs | Immediate | Low (transactional) | Fully restored |
This finding matters because it shifts the migration strategy from filesystem operations to state database engineering. By directly manipulating the ItemTable within state.vscdb, merging Base64-encoded Protobuf trajectory summaries, and applying intelligent path remapping, developers can restore full conversation history, workspace metadata, and artifact links without data loss. The IDE's architecture treats these databases as the source of truth; aligning them with the new directory structure is the only path to a seamless transition.
Core Solution
The migration requires a coordinated sequence: backup creation, path remapping, Protobuf trajectory merging, and SQLite transaction execution. Below is a production-grade implementation that handles these steps safely.
Architecture Decisions and Rationale
- Direct SQLite Access: The IDE does not expose a public migration API. Accessing
state.vscdbdirectly is necessary because trajectory summaries and workspace paths are stored as serialized key-value pairs in theItemTable. - Protobuf Wire Format Parsing: Trajectory summaries are stored as repeated Protobuf messages encoded in Base64. The script manually decodes varint lengths and extracts field tags (
0x0a) to reconstruct the binary payload without requiring external Protobuf compiler dependencies. - Regex Path Remapping with Negative Lookahead: Simple string replacement risks creating double suffixes (e.g.,
antigravity-ide-ide). The regex uses negative lookahead(?!-ide)to ensure only legacy paths are transformed. - Transaction Boundaries: All SQLite modifications use
INSERT OR REPLACEwithin explicit commit boundaries. This prevents partial writes and ensures atomic state updates. - Workspace Storage Mirroring: VS Code-based tools isolate workspace state in
workspaceStorage. The script iterates through legacy workspace folders, creates corresponding directories in the new path, and migrates their individualstate.vscdbfiles.
Implementation
import os
import sqlite3
import shutil
import base64
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
class AntigravityStateMigrator:
LEGACY_ROOT = Path.home() / ".gemini" / "antigravity"
NEW_ROOT = Path.home() / ".gemini" / "antigravity-ide"
LEGACY_APP_SUPPORT = Path.home() / "Library" / "Application Support" / "Antigravity"
NEW_APP_SUPPORT = Path.home() / "Library" / "Application Support" / "Antigravity IDE"
TRAJECTORY_KEY = "antigravityUnifiedStateSync.trajectorySummaries"
RECENT_PATHS_KEY = "history.recentlyOpenedPathsList"
def __init__(self):
self.log_buffer: List[str] = []
def log(self, message: str) -> None:
print(f"[MIGRATOR] {message}")
self.log_buffer.append(message)
def _remap_filesystem_path(self, raw_value: str) -> str:
"""Replace legacy directory references with new IDE paths."""
if not isinstance(raw_value, str):
return raw_value
pattern_home = r'/Users/([^/]+)/\.gemini/antigravity(?!-ide)'
replacement_home = r'/Users/\1/.gemini/antigravity-ide'
pattern_support = r'Application Support/Antigravity(?! IDE)'
replacement_support = r'Application Support/Antigravity IDE'
result = re.sub(pattern_home, replacement_home, raw_value)
result = re.sub(pattern_support, replacement_support, result)
return result
def _decode_protobuf_varint(self, data: bytes, offset: int) -> Tuple[int, int]:
"""Extract varint length from Protobuf wire format."""
value = 0
shift = 0
while offset < len(data):
byte = data[offset]
offset += 1
value |= (byte & 0x7F) << shift
if not (byte & 0x80):
break
shift += 7
return value, offset
def _extract_trajectory_entries(self, payload: bytes) -> Dict[str, bytes]:
"""Parse repeated Protobuf messages and index by trajectory ID."""
entries: Dict[str, bytes] = {}
idx = 0
while idx < len(payload):
if payload[idx] != 0x0A:
idx += 1
continue
idx += 1
length, idx = self._decode_protobuf_varint(payload, idx)
chunk = payload[idx : idx + length]
idx += length
if len(chunk) >= 38 and chunk[0] == 0x0A and chunk[1] == 0x24:
tid = chunk[2:38].decode('utf-8', errors='ignore')
entries[tid] = chunk
return entries
def _rebuild_protobuf_payload(self, entries: Dict[str, bytes]) -> bytes:
"""Serialize trajectory entries back into a valid Protobuf stream."""
output = bytearray()
for entry_bytes in entries.values():
output.append(0x0A)
length_bytes = []
length = len(entry_bytes)
while length >= 0x80:
length_bytes.append((length & 0x7F) | 0x80)
length >>= 7
length_bytes.append(length)
output.extend(length_bytes)
output.extend(entry_bytes)
return bytes(output)
def _merge_trajectory_summaries(self, legacy_b64: Optional[str], current_b64: Optional[str]) -> str:
"""Combine legacy and current trajectory data, deduplicating by ID."""
legacy_data = base64.b64decode(legacy_b64) if legacy_b64 else b""
current_data = base64.b64decode(current_b64) if current_b64 else b""
merged = self._extract_trajectory_entries(legacy_data)
merged.update(self._extract_trajectory_entries(current_data))
rebuilt = self._rebuild_protobuf_payload(merged)
return base64.b64encode(rebuilt).decode('ascii')
def _migrate_global_storage(self) -> None:
"""Handle global state database migration."""
legacy_db = self.LEGACY_APP_SUPPORT / "User" / "globalStorage" / "state.vscdb"
new_db = self.NEW_APP_SUPPORT / "User" / "globalStorage" / "state.vscdb"
if not legacy_db.exists() or not new_db.exists():
self.log("Global storage databases not found. Skipping.")
return
backup_path = str(new_db) + ".pre_migration_backup"
shutil.copy2(new_db, backup_path)
self.log(f"Backed up global DB to {backup_path}")
legacy_conn = sqlite3.connect(str(legacy_db))
new_conn = sqlite3.connect(str(new_db))
legacy_items = {
row[0]: row[1]
for row in legacy_conn.execute(
"SELECT key, value FROM ItemTable WHERE key LIKE 'antigravity%' OR key LIKE 'google.antigravity%' OR key = ?",
(self.RECENT_PATHS_KEY,)
)
}
new_items = {row[0]: row[1] for row in new_conn.execute("SELECT key, value FROM ItemTable")}
for key, legacy_val in legacy_items.items():
if "notification" in key:
continue
if key == self.TRAJECTORY_KEY:
merged_val = self._merge_trajectory_summaries(legacy_val, new_items.get(key))
new_conn.execute("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)", (key, merged_val))
elif key == self.RECENT_PATHS_KEY:
new_conn.execute("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)", (key, self._remap_filesystem_path(legacy_val)))
else:
if key not in new_items or "Preferences" in key:
new_conn.execute("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)", (key, self._remap_filesystem_path(legacy_val)))
new_conn.commit()
legacy_conn.close()
new_conn.close()
self.log("Global storage migration complete.")
def _migrate_workspace_storage(self) -> None:
"""Replicate workspace-specific state directories."""
legacy_ws = self.LEGACY_APP_SUPPORT / "User" / "workspaceStorage"
new_ws = self.NEW_APP_SUPPORT / "User" / "workspaceStorage"
if not legacy_ws.exists():
self.log("No legacy workspace storage found.")
return
new_ws.mkdir(parents=True, exist_ok=True)
for ws_folder in legacy_ws.iterdir():
legacy_db = ws_folder / "state.vscdb"
if not legacy_db.exists():
continue
target_dir = new_ws / ws_folder.name
target_dir.mkdir(parents=True, exist_ok=True)
target_db = target_dir / "state.vscdb"
target_conn = sqlite3.connect(str(target_db))
target_conn.execute("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value TEXT)")
legacy_conn = sqlite3.connect(str(legacy_db))
for row in legacy_conn.execute("SELECT key, value FROM ItemTable"):
target_conn.execute("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)",
(row[0], self._remap_filesystem_path(row[1])))
legacy_conn.close()
target_conn.commit()
target_conn.close()
self.log("Workspace storage migration complete.")
def _copy_conversation_artifacts(self) -> None:
"""Transfer raw Protobuf conversation files to the new directory."""
legacy_conv = self.LEGACY_ROOT / "conversations"
new_conv = self.NEW_ROOT / "conversations"
new_conv.mkdir(parents=True, exist_ok=True)
if not legacy_conv.exists():
self.log("No legacy conversation directory found.")
return
count = 0
for pb_file in legacy_conv.glob("*.pb"):
shutil.copy2(pb_file, new_conv / pb_file.name)
count += 1
self.log(f"Copied {count} conversation artifacts to new directory.")
def execute(self) -> None:
self.log("Starting Antigravity IDE state migration...")
self._migrate_global_storage()
self._migrate_workspace_storage()
self._copy_conversation_artifacts()
self.log("Migration pipeline finished successfully.")
if __name__ == "__main__":
migrator = AntigravityStateMigrator()
migrator.execute()
Pitfall Guide
Executing Migration While IDE is Running
- Explanation: The IDE maintains an in-memory cache of
state.vscdband flushes it to disk on exit. Running the script while the process is active guarantees that your database modifications will be overwritten. - Fix: Always terminate the application completely (
Cmd + Qorkillall Antigravity IDE) and verify no background processes remain viaps aux | grep -i antigravity.
- Explanation: The IDE maintains an in-memory cache of
Ignoring Workspace-Specific Storage
- Explanation: VS Code-based architectures isolate workspace metadata in
workspaceStorage. Migrating only the global database leaves project-specific settings, open editors, and local trajectory references broken. - Fix: Ensure the migration script iterates through
workspaceStoragesubdirectories and applies path remapping to eachstate.vscdbinstance.
- Explanation: VS Code-based architectures isolate workspace metadata in
Corrupting Protobuf Varint Boundaries
- Explanation: Protobuf uses variable-length encoding for message lengths. Incorrectly calculating or truncating varint bytes during merge operations results in unreadable trajectory summaries.
- Fix: Implement strict varint decoding with continuation bit checks (
byte & 0x80). Validate payload lengths before reconstruction and avoid manual byte slicing without boundary verification.
Double-Suffix Path Replacement
- Explanation: Naive string replacement (
antigravityāantigravity-ide) applied multiple times creates invalid paths likeantigravity-ide-ide. - Fix: Use negative lookahead regex patterns
(?!-ide)and(?! IDE)to ensure replacement only targets legacy references.
- Explanation: Naive string replacement (
Overriding Notification and Preference Keys
- Explanation: The
ItemTablecontains UI preferences, notification states, and telemetry flags. Blindly merging all keys can reset user configurations or trigger unwanted notification floods. - Fix: Filter keys containing
notificationorPreferencesduring migration. Only merge trajectory, path, and workspace state keys.
- Explanation: The
SQLite WAL Mode Conflicts
- Explanation: Modern SQLite databases use Write-Ahead Logging (WAL). If the IDE leaves
.walor.shmfiles behind, direct connections may read stale data or fail to commit. - Fix: Delete or archive
state.vscdb-walandstate.vscdb-shmfiles before opening connections, or force checkpointing viaPRAGMA wal_checkpoint(TRUNCATE)before migration.
- Explanation: Modern SQLite databases use Write-Ahead Logging (WAL). If the IDE leaves
Assuming
.pbFiles Are the Sole Data Source- Explanation: Conversation files are necessary but insufficient. The IDE's sidebar relies on trajectory summaries stored in the database to render titles, timestamps, and artifact links.
- Fix: Always pair file copying with database trajectory merging. File existence without DB registration results in orphaned data.
Production Bundle
Action Checklist
- Terminate Antigravity IDE completely and verify no background processes
- Create manual backups of
state.vscdbin both global and workspace directories - Execute the migration script and monitor console output for errors
- Verify
.pbconversation files exist in~/.gemini/antigravity-ide/conversations - Launch the IDE and confirm conversation history populates the sidebar
- Open previously active workspaces and validate artifact links resolve correctly
- Check
history.recentlyOpenedPathsListin the database for correct path formatting - Archive migration logs and database backups for rollback capability
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Fresh installation, no legacy data | Skip migration | No state to preserve | Zero |
| Single user, minimal workspace history | Manual file copy + IDE restart | Low complexity, acceptable risk | Low |
| Active development, multiple workspaces, critical conversation archives | Direct SQLite + Protobuf migration | Guarantees state resolution and trajectory continuity | Medium (requires script execution) |
| Team environment with shared configuration management | Automate via CI/CD or config management | Ensures consistent state across developer machines | High (initial setup) |
Configuration Template
Save the following as migrate_antigravity_state.py and execute via Python 3.8+:
#!/usr/bin/env python3
import os
import sqlite3
import shutil
import base64
import re
from pathlib import Path
class StateMigrator:
def __init__(self):
self.home = Path.home()
self.legacy_root = self.home / ".gemini" / "antigravity"
self.new_root = self.home / ".gemini" / "antigravity-ide"
self.legacy_support = self.home / "Library" / "Application Support" / "Antigravity"
self.new_support = self.home / "Library" / "Application Support" / "Antigravity IDE"
def run(self):
print("Starting migration...")
self._backup_databases()
self._remap_global_state()
self._sync_workspace_storage()
self._transfer_conversations()
print("Migration complete. Restart the IDE.")
def _backup_databases(self):
targets = [
self.legacy_support / "User" / "globalStorage" / "state.vscdb",
self.new_support / "User" / "globalStorage" / "state.vscdb"
]
for db in targets:
if db.exists():
shutil.copy2(db, f"{db}.backup")
def _remap_global_state(self):
new_db = self.new_support / "User" / "globalStorage" / "state.vscdb"
if not new_db.exists(): return
conn = sqlite3.connect(str(new_db))
conn.execute("PRAGMA journal_mode=WAL")
keys_to_fix = [
"antigravityUnifiedStateSync.trajectorySummaries",
"history.recentlyOpenedPathsList"
]
for key in keys_to_fix:
row = conn.execute("SELECT value FROM ItemTable WHERE key = ?", (key,)).fetchone()
if row and isinstance(row[0], str):
fixed = re.sub(r'/Users/([^/]+)/\.gemini/antigravity(?!-ide)', r'/Users/\1/.gemini/antigravity-ide', row[0])
fixed = re.sub(r'Application Support/Antigravity(?! IDE)', r'Application Support/Antigravity IDE', fixed)
conn.execute("UPDATE ItemTable SET value = ? WHERE key = ?", (fixed, key))
conn.commit()
conn.close()
def _sync_workspace_storage(self):
old_ws = self.legacy_support / "User" / "workspaceStorage"
new_ws = self.new_support / "User" / "workspaceStorage"
if not old_ws.exists(): return
new_ws.mkdir(parents=True, exist_ok=True)
for folder in old_ws.iterdir():
db_path = folder / "state.vscdb"
if not db_path.exists(): continue
dest = new_ws / folder.name
dest.mkdir(parents=True, exist_ok=True)
shutil.copy2(db_path, dest / "state.vscdb")
def _transfer_conversations(self):
src = self.legacy_root / "conversations"
dst = self.new_root / "conversations"
dst.mkdir(parents=True, exist_ok=True)
if src.exists():
for f in src.glob("*.pb"):
shutil.copy2(f, dst / f.name)
if __name__ == "__main__":
StateMigrator().run()
Quick Start Guide
- Terminate the IDE: Press
Cmd + Qand confirm the process has exited using Activity Monitor orps aux | grep Antigravity. - Prepare the Script: Save the configuration template above as
migrate_antigravity_state.pyin a temporary directory. - Execute Migration: Run
python3 migrate_antigravity_state.pyin Terminal. Wait for the completion message. - Verify & Restart: Launch Antigravity IDE. Navigate to the sidebar to confirm conversation history and workspace references are restored. Check
~/.gemini/antigravity-ide/conversationsto ensure.pbfiles are present.
Mid-Year Sale ā Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register ā Start Free Trial7-day free trial Ā· Cancel anytime Ā· 30-day money-back
