When working with terminal-based agentic AI assistants like Google Antigravity (agy), you quickly realize that their raw reasoning power and tool execution are extraordinary. Whether diagnosing a degraded Btrfs array across remote NAS nodes, managing IoT smart home gateways, or optimizing complex build pipelines, an autonomous agent can orchestrate dozens of shell commands, inspect system logs, and implement multi-file refactors in minutes.
Yet, out of the box, agentic AI CLI tools suffer from a glaring handicap: the Amnesia Problem.
Each new terminal session starts tabula rasa. The agent has no memory of the troubleshooting breakthrough you achieved yesterday, the custom dynamic DNS topology you established last week, or the architectural rules you painstakingly aligned on last month. Even worse, while many tools quietly store their interaction history in local application caches or SQLite databases, that data remains trapped in opaque silos—unlinked, unsearchable from your daily workflow, and completely disconnected from your personal knowledge base.
In this post, I will share how I built a 100% local, zero-cloud memory bridge that automatically archives every Antigravity session into Obsidian in real time and equips the AI agent with a 5-layer persistent memory model.
Using a lightweight combination of Python, SQLite read-only queries, Linux systemd --user path watchers, and Obsidian markdown notes, my AI assistant now possesses persistent operational memory, tracks an ongoing audit trail of system modifications, and maintains a searchable chronological archive of all 90+ past sessions spanning 17,400+ interaction steps.
1. The Core Architecture
The design goal was simple: zero manual intervention, instantaneous synchronization, zero cloud leaks, and complete markdown transparency.
Rather than running heavy vector databases or external microservices, the system leverages native Linux subsystems and Obsidian’s core strength: plain text files with structured frontmatter.

The system operates across three tightly integrated pipelines:
- The Event-Driven Exporter: An inotify watcher triggers a non-blocking Python script to serialize raw session metadata into structured Obsidian tables.
- The 5-Layer Semantic Memory Model: A structured set of Markdown files in the Obsidian
Meta/directory that separate ephemeral task state, operational learnings, agent audit trails, and user identity. - The Agent Feedback Loop: System directives embedded in
~/.agents/AGENTS.mdinstruct the agent to inspect its persistent memory on session startup and record significant breakthroughs upon completion.
2. Peering Inside the Antigravity SQLite Database
Antigravity stores high-level conversation metadata locally under ~/.gemini/antigravity-cli/conversation_summaries.db.
Inspecting the database schema reveals how rich this data is:
CREATE TABLE `conversation_summaries` (
`conversation_id` TEXT PRIMARY KEY,
`title` TEXT NOT NULL DEFAULT "",
`preview` TEXT NOT NULL DEFAULT "",
`step_count` INTEGER NOT NULL DEFAULT 0,
`last_modified_time` DATETIME NOT NULL,
`workspace_uris` TEXT NOT NULL,
`status` TEXT NOT NULL DEFAULT "",
`source` TEXT NOT NULL DEFAULT "",
`project_id` TEXT NOT NULL DEFAULT "",
`agent_name` TEXT NOT NULL DEFAULT "",
`parent_conversation_id` TEXT NOT NULL DEFAULT "",
`nesting_depth` INTEGER NOT NULL DEFAULT 0,
`battle_id` TEXT NOT NULL DEFAULT "",
`winning_conversation_id` TEXT NOT NULL DEFAULT "",
`not_fully_idle` NUMERIC NOT NULL DEFAULT 0,
`killed` NUMERIC NOT NULL DEFAULT 0,
`last_user_input_time` DATETIME NOT NULL,
`last_user_input_step_index` INTEGER NOT NULL DEFAULT -1,
`app_data_dir` TEXT NOT NULL DEFAULT "",
`raw_summary` BLOB,
`group_id` TEXT NOT NULL DEFAULT ''
);
CREATE INDEX `idx_conversation_summaries_last_user_input_time`
ON `conversation_summaries`(`last_user_input_time`);
CREATE INDEX `idx_conversation_summaries_last_modified_time`
ON `conversation_summaries`(`last_modified_time`);
Each row records the unique conversation_id, the inferred title, a preview of the initial user request, the cumulative step_count, and exact ISO-8601 timestamps.
Why Direct Database Extraction Beats Log Parsing
Parsing raw terminal scrollback or JSON transcript logs is notoriously brittle: logs can span megabytes, get truncated, or interleave concurrent subagent streams. In contrast, conversation_summaries.db is an atomic, indexed single-row summary per conversation. Reading this database directly gives us clean, normalized session metadata with zero CPU overhead.
3. The Extraction Pipeline (export_antigravity_history.py)
To transform raw database rows into an elegant Obsidian note, I developed /home/gvoina/scripts/export_antigravity_history.py.
Key Engineering Challenges Solved:
1. Concurrency and Zero Database Locks
Because Antigravity writes to conversation_summaries.db in real time during a session, our export script must never take an exclusive database lock or block active agent writes. We solve this by opening SQLite using a read-only URI:
# Use read-only SQLite URI to avoid taking locks or interfering with active writes
db_uri = f"file:{DB_PATH.resolve()}?mode=ro"
conn = sqlite3.connect(db_uri, uri=True, timeout=10)
2. Clean Markdown Sanitization
User prompts frequently contain pipe characters (|) from shell commands or markdown tables, as well as raw newlines. If injected raw, they break Obsidian’s markdown table parsing. The script cleanses these inputs:
def sanitize(text: str) -> str:
if not text:
return ""
return text.strip().replace("|", "\\|").replace("\n", " ").replace("\r", "")
3. Semantic Dirty-Checking to Prevent Sync Churn
If an exporter rewrites a markdown file every time it runs, its timestamp changes. If you use file-synchronization tools like Git, Syncthing, or Obsidian Sync, this causes endless false-positive commits and synchronization churn.
The script implements a semantic diff check that strips out the dynamic updated: "YYYY-MM-DD" frontmatter line and checks whether the actual table rows have changed before writing to disk:
def strip_updated_field(content: str) -> str:
"""Strip the updated: line for comparing semantic body differences."""
return re.sub(r'updated:\s*".*?"', "", content)
# Check if content has actually changed
if OUTPUT_PATH.exists() and not args.force:
existing_content = OUTPUT_PATH.read_text()
if strip_updated_field(existing_content).strip() == strip_updated_field(new_content).strip():
print(f"Session archive is up to date ({len(rows)} sessions). No write needed.")
return
OUTPUT_PATH.write_text(new_content)
Complete Exporter Code
Here is the complete implementation of /home/gvoina/scripts/export_antigravity_history.py:
#!/usr/bin/env python3
"""
export_antigravity_history.py - Export historical Antigravity conversations into Obsidian.
Reads from ~/.gemini/antigravity-cli/conversation_summaries.db and writes
to /home/gvoina/Vault/George/Meta/antigravity-session-history.md
"""
import argparse
import datetime
import re
import sqlite3
import sys
from collections import defaultdict
from pathlib import Path
DB_PATH = Path("/home/gvoina/.gemini/antigravity-cli/conversation_summaries.db")
OUTPUT_PATH = Path("/home/gvoina/Vault/George/Meta/antigravity-session-history.md")
MONTH_NAMES = {
"2026-12": "December 2026",
"2026-11": "November 2026",
"2026-10": "October 2026",
"2026-09": "September 2026",
"2026-08": "August 2026",
"2026-07": "July 2026",
"2026-06": "June 2026",
"2026-05": "May 2026",
"2026-04": "April 2026",
"2026-03": "March 2026",
"2026-02": "February 2026",
"2026-01": "January 2026",
}
def sanitize(text: str) -> str:
if not text:
return ""
return text.strip().replace("|", "\\|").replace("\n", " ").replace("\r", "")
def strip_updated_field(content: str) -> str:
"""Strip the updated: line for comparing semantic body differences."""
return re.sub(r'updated:\s*".*?"', "", content)
def main():
parser = argparse.ArgumentParser(description="Sync Antigravity sessions to Obsidian")
parser.add_argument("--force", action="store_true", help="Force rewrite even if content unchanged")
args = parser.parse_args()
if not DB_PATH.exists():
print(f"Error: {DB_PATH} not found.", file=sys.stderr)
return
# Use read-only SQLite URI to avoid taking locks or interfering with active writes
db_uri = f"file:{DB_PATH.resolve()}?mode=ro"
try:
conn = sqlite3.connect(db_uri, uri=True, timeout=10)
c = conn.cursor()
rows = c.execute(
"""
SELECT conversation_id, title, preview, step_count, last_modified_time
FROM conversation_summaries
ORDER BY last_modified_time DESC
"""
).fetchall()
conn.close()
except Exception as e:
print(f"Error reading conversation database: {e}", file=sys.stderr)
return
by_month = defaultdict(list)
total_steps = 0
today_str = datetime.date.today().isoformat()
for cid, title, preview, steps, mtime in rows:
total_steps += steps or 0
date_str = mtime[:10] if mtime else "Unknown"
month_key = mtime[:7] if mtime else "Unknown"
clean_title = sanitize(title)
clean_preview = sanitize(preview)
if len(clean_preview) > 130:
clean_preview = clean_preview[:127] + "..."
if not clean_title:
clean_title = (
clean_preview[:40] + ("..." if len(clean_preview) > 40 else "")
if clean_preview
else "Untitled Session"
)
if clean_title == clean_preview:
clean_preview = "-"
by_month[month_key].append(
{
"cid": cid,
"title": clean_title,
"preview": clean_preview,
"steps": steps or 0,
"date": date_str,
"mtime": mtime,
}
)
lines = [
"---",
"type: archive",
f'updated: "{today_str}"',
f"total-sessions: {len(rows)}",
f"total-steps: {total_steps}",
"tags: [antigravity, history, sessions, system]",
"---",
"",
"# Antigravity Session History Archive",
"",
f"This archive chronologically indexes all **{len(rows)} local conversations and tasks** recorded by Antigravity (`agy`) across your workspace between **February 2026** and **October 2026** (totaling {total_steps:,} interaction steps).",
"",
"---",
"",
]
for m_key in sorted(by_month.keys(), reverse=True):
items = by_month[m_key]
name = MONTH_NAMES.get(m_key, m_key)
lines.append(f"## {name} ({len(items)} sessions)")
lines.append("")
lines.append(
"| Date | Session / Topic | Summary / Initial Prompt | Steps | Conversation ID |"
)
lines.append("| :--- | :--- | :--- | :---: | :--- |")
for item in items:
cid_short = f"`{item['cid'][:8]}...`"
lines.append(
f"| {item['date']} | **{item['title']}** | {item['preview']} | {item['steps']} | {cid_short} |"
)
lines.append("")
new_content = "\n".join(lines)
# Check if content has actually changed
if OUTPUT_PATH.exists() and not args.force:
existing_content = OUTPUT_PATH.read_text()
if strip_updated_field(existing_content).strip() == strip_updated_field(new_content).strip():
print(f"Session archive is up to date ({len(rows)} sessions). No write needed.")
return
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text(new_content)
print(f"Successfully exported {len(rows)} sessions ({total_steps:,} steps) to {OUTPUT_PATH}")
if __name__ == "__main__":
main()
4. Real-Time Event-Driven Sync with systemd --user

A common temptation when building sync scripts is to schedule a cron job that runs every minute. But polling a database via cron introduces unnecessary disk wakeups, CPU churn, and latency: if a session ends, you don’t want to wait 60 seconds to see it in Obsidian.
Instead, Linux provides a superior, battle-tested mechanism: kernel-level inotify path monitoring via systemd --user.
We configure three lightweight user units in ~/.config/systemd/user/:
1. The Path Unit (antigravity-obsidian-sync.path)
The path unit watches the SQLite database directly in the Linux kernel via inotify. Whenever Antigravity finishes an interaction or appends a step, the file modification immediately wakes the service.
[Unit]
Description=Watch Antigravity Session DB for changes and sync to Obsidian
After=default.target
[Path]
PathModified=%h/.gemini/antigravity-cli/conversation_summaries.db
Unit=antigravity-obsidian-sync.service
[Install]
WantedBy=default.target
2. The Service Unit (antigravity-obsidian-sync.service)
A oneshot unit that executes the Python exporter and routes stdout/stderr cleanly into the systemd journal.
[Unit]
Description=Sync Antigravity Session History to Obsidian Vault
After=default.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /home/gvoina/scripts/export_antigravity_history.py
StandardOutput=journal
StandardError=journal
3. The Fallback Safety Timer (antigravity-obsidian-sync.timer)
As defense-in-depth against missed file events (e.g. system sleep/wake cycles or container mounts), a 30-minute fallback timer ensures the archive is refreshed periodically.
[Unit]
Description=Periodic Safety Sync for Antigravity Session History to Obsidian
[Timer]
OnBootSec=5min
OnUnitActiveSec=30min
Unit=antigravity-obsidian-sync.service
[Install]
WantedBy=timers.target
Activating the Daemons
Enabling the system requires just two commands:
systemctl --user daemon-reload
systemctl --user enable --now antigravity-obsidian-sync.path antigravity-obsidian-sync.timer
Checking status with systemctl --user status antigravity-obsidian-sync.path:
● antigravity-obsidian-sync.path - Watch Antigravity Session DB for changes and sync to Obsidian
Loaded: loaded (/home/gvoina/.config/systemd/user/antigravity-obsidian-sync.path; enabled)
Active: active (waiting) since Sun 2026-10-11 11:15:21 EEST
Triggers: ● antigravity-obsidian-sync.service
Zero CPU utilization when idle, instantaneous synchronization upon write.
5. The 5-Layer Semantic Memory Model in Obsidian
Having an archive of past sessions is only half the equation. Session logs tell you what happened in the past; what an agent truly needs to be an effective partner is structured semantic context in the present.
Inside the Obsidian vault under Meta/, we designed a 5-Layer Memory Architecture:
Vault/George/Meta/
├── states/
│ └── antigravity.md # Layer 1: Working scratchpad & live context
├── antigravity-memory.md # Layer 2: Long-term operational learnings
├── antigravity-session-history.md # Layer 3: Chronological session archive
├── agent-log.md # Layer 4: Audit changelog of agent actions
└── user-profile.md # Layer 5: User identity & preferences
Let’s break down each layer:
Layer 1: Working State (Meta/states/antigravity.md)
Think of this as the agent’s sticky note. It captures the active focus, immediate milestones from the last session, and open reminders. It also records a dynamic last-run ISO timestamp.
---
agent: antigravity
last-run: "2026-10-11T12:43:45+03:00"
---
## Post-it
### Active Working Context
- **Workspace**: `/home/gvoina`
- **Active Focus**: Established cross-session memory bridge with Obsidian vault.
- **Recent Milestones**:
- Implemented Obsidian long-term memory integration into `Meta/` and `~/.agents/AGENTS.md`.
- Configured persistent tracking for infrastructure, smart home, and ongoing projects.
### Ongoing Tasks & Reminders
- Keep `Meta/antigravity-memory.md` updated with system insights and preferences.
- Log major operations and forward-fixes to `Meta/agent-log.md`.
Layer 2: Long-Term Memory (Meta/antigravity-memory.md)
This note persists foundational truths that must survive across months of development:
- Infrastructure Topology: Which server hostnames resolve dynamically, which nodes require SSH jump proxies, which IP ranges belong to out-of-band management interfaces.
- Engineering Policies: Strict guidelines such as the Forward-Fix Only Policy (never rolling back a resilient RAID or clustering setup to a degraded state just because it previously worked).
- Domain Integrations: Smart home hardware addresses, IoT serial daemons, and wallet integration details.
Layer 3: Session History Archive (Meta/antigravity-session-history.md)
Generated entirely by our Python exporter, this note serves as a high-density, chronological index of every conversation ever held with the agent, complete with exact dates, summarized topics, step counts, and conversation UUIDs:
## October 2026 (3 sessions)
| Date | Session / Topic | Summary / Initial Prompt | Steps | Conversation ID |
| :--- | :--- | :--- | :---: | :--- |
| 2026-10-11 | **Local Memory Storage In Obsidian** | - | 143 | `e1ba5102...` |
| 2026-10-08 | **Paraschiv Metal Band Search** | - | 41 | `e6022bdf...` |
| 2026-10-08 | **Organize Utility Account Links** | - | 2842 | `e7c5ebab...` |
If I need to investigate how we configured a specific SSL renewal or router VLAN six months ago, I can search Obsidian’s global search, find the row in seconds, and copy the Conversation ID to inspect the full transcript on disk.
Layer 4: Automated Agent Log (Meta/agent-log.md)
Whenever an agent executes significant infrastructure modifications, deploys services, or alters files, it appends an audit entry to agent-log.md. This gives you full visibility into what automated changes occurred over time:
## 2026-10-11
- **Antigravity**: Configured automated systemd path/timer units (antigravity-obsidian-sync) to auto-sync sessions to Obsidian.
- **Antigravity**: Backfilled 90 historical sessions into Meta/antigravity-session-history.md.
- **Antigravity**: Established and verified Obsidian cross-session memory integration.
Layer 5: User Profile (Meta/user-profile.md)
Defines user preferences, identity attributes, coding conventions, and working style so the agent doesn’t need to re-ask preferred programming languages or workspace patterns.
6. The Memory CLI Interface (obsidian_memory.py)
To make interacting with this memory model ergonomic for both human and AI, we created a CLI helper script at /home/gvoina/scripts/obsidian_memory.py.
It provides simple subcommands:
obsidian_memory.py status: Prints the current state and top log entries.obsidian_memory.py read <memory|state|log|profile>: Dumps the corresponding note content.obsidian_memory.py log "<message>": Safely appends an agent action to today’s date section inagent-log.md.obsidian_memory.py add-note "<note>": Records a persistent learning intoantigravity-memory.md.
#!/usr/bin/env python3
"""
obsidian_memory.py - Manage and query cross-session Antigravity memory in Obsidian.
Vault path: /home/gvoina/Vault/George
"""
import argparse
import datetime
import os
import re
import sys
from pathlib import Path
VAULT_DIR = Path("/home/gvoina/Vault/George")
META_DIR = VAULT_DIR / "Meta"
STATE_FILE = META_DIR / "states" / "antigravity.md"
MEMORY_FILE = META_DIR / "antigravity-memory.md"
LOG_FILE = META_DIR / "agent-log.md"
PROFILE_FILE = META_DIR / "user-profile.md"
def cmd_status(args):
print("=== Antigravity Obsidian Memory Status ===")
print(f"Vault Path: {VAULT_DIR}")
if STATE_FILE.exists():
print(f"\n--- State ({STATE_FILE.name}) ---")
print(STATE_FILE.read_text().strip())
if LOG_FILE.exists():
print(f"\n--- Recent Agent Log Entries ({LOG_FILE.name}) ---")
lines = LOG_FILE.read_text().splitlines()
print("\n".join(lines[:25]))
def cmd_log(args):
message = args.message.strip()
today = datetime.date.today().isoformat()
header = f"## {today}"
entry = f"- **Antigravity**: {message}"
if LOG_FILE.exists():
content = LOG_FILE.read_text()
else:
content = "# Agent Log\n\nThis file logs all automated changes made by agents.\n\n"
if header in content:
idx = content.find(header)
line_end = content.find("\n", idx)
content = content[: line_end + 1] + f"{entry}\n" + content[line_end + 1 :]
else:
match = re.search(r"(## \d{4}-\d{2}-\d{2})", content)
if match:
idx = match.start()
content = content[:idx] + f"{header}\n{entry}\n\n" + content[idx:]
else:
content += f"\n{header}\n{entry}\n"
LOG_FILE.write_text(content)
print(f"Logged to {LOG_FILE}: {entry}")
7. Closing the Loop: Rules Integration via AGENTS.md
How does the agent know to use all of this?
Antigravity natively reads workspace instruction rules from ~/.agents/AGENTS.md. We added an explicit operational directive:
### 9. Obsidian Cross-Session Memory & Long-Term Context
Antigravity (`agy`) maintains persistent semantic memory, active state, and audit history
directly inside the Obsidian Vault at `/home/gvoina/Vault/George/`.
* **Vault Memory Structure**:
* `Meta/states/antigravity.md`: Current working context, active focus, and last-run metadata.
* `Meta/antigravity-memory.md`: Long-term persistent knowledge, fleet notes, and operational learnings.
* `Meta/antigravity-session-history.md`: Chronological index of all historical CLI sessions.
* `Meta/agent-log.md`: Chronological log of automated changes and milestones.
* `Meta/user-profile.md`: User profile and identity attributes.
* **CLI Memory Management**:
* `python3 /home/gvoina/scripts/obsidian_memory.py status`
* `python3 /home/gvoina/scripts/obsidian_memory.py read <memory|state|log|profile>`
* `python3 /home/gvoina/scripts/obsidian_memory.py log "<message>"`
* `python3 /home/gvoina/scripts/obsidian_memory.py add-note "<note>"`
* **Operating Directives**:
* When starting or resuming complex tasks involving preferences or infrastructure history,
consult the relevant notes in `Meta/`.
* When completing major system modifications or upon user request to "remember" or "log",
update `Meta/states/antigravity.md` and append an entry via `obsidian_memory.py log`.
Because this rule is injected into the agent’s system prompt at every invocation, the agent immediately knows its own memory structure, where to look for fleet details, and how to record what it learned.
8. Security and Privacy by Design
Bridging AI workflows with personal notes demands rigorous attention to security and confidentiality:
- Zero Secret Leakage:
- The exporter only reads high-level session titles and user previews; it never dumps conversation payloads, API tokens, or credentials into markdown tables.
- Authentication secrets (such as private PEM signing keys or SSH certificates) remain strictly isolated in secure paths (
~/.ssh/,/etc/, or dedicated script directories) withchmod 600permissions. They are explicitly never written to Obsidian notes or git repositories.
- Local-Only Architecture:
- The entire memory subsystem runs locally on the host machine.
- SQLite access uses read-only local IPC.
- Files are standard Markdown stored on a local ext4/Btrfs filesystem. No third-party memory APIs, cloud vector databases, or telemetry beacons are involved.
- Graceful Failure Handling:
- If the database is locked during a rare checkpoint, the SQLite query times out safely without crashing the systemd runner.
- If the Obsidian vault path is unmounted, the script fails cleanly and logs to systemd journal.
9. Results and Everyday Experience
Since deploying this system, the difference in everyday agentic development has been profound:
- 90 Past Sessions Backfilled: On day one, the exporter backfilled 90 historic sessions representing 17,497 autonomous steps recorded between February and October 2026.
- Instant Historical Discovery: Whenever an error message or strange network behavior pops up, searching the term in Obsidian instantly reveals the exact date we previously encountered it and what steps we took to resolve it.
- No More “Groundhog Day”: When starting a fresh terminal session, the agent doesn’t need to ask for server jump ports, favorite CLI conventions, or existing script locations. It reads its own memory note and gets straight to work.
- Zero Ongoing Maintenance: The
systemdinotify watcher triggers completely in the background without requiring a second thought.
Conclusion: From Disposable Chats to Long-Term Pair Programming
For years, AI interactions have been treated as disposable: a user asks a question, gets an answer, and closes the tab or terminal window. But as agents gain the ability to execute complex, multi-system coding and operations tasks, treating their memory as disposable is a massive waste of intelligence.
By anchoring your AI agent’s memory inside a local-first, markdown-native knowledge base like Obsidian, you transform an ephemeral CLI tool into a true long-term engineering partner—one that learns from every incident, respects your architecture rules, and grows alongside your knowledge base.
If you are using Antigravity, Claude Code, or any terminal agent with a local SQLite backend, setting up a systemd watcher and a structured markdown memory vault is one of the highest-leverage improvements you can make to your workflow.