Skip to content

About

tools for antigravity-cli (agy) transcripts

Resources

Stars

16 stars

Watchers

1 watching

Forks

Repository files navigation

agy-reader

A Unix-style Go CLI that extracts transcripts from Google Antigravity CLI sessions by talking to the local language-server daemon Antigravity runs while agy is active. Conversation SQLite .db (storing protobuf-encoded steps) and legacy encrypted .pb session files live under ~/.gemini/antigravity-cli/conversations/; agy-reader fetches the canonical JSON trajectory from the daemon, renders Markdown for humans, and writes a <uuid>.trajectory.json sidecar next to each session file for downstream tools.

Antigravity 2.0 (the IDE) stores its conversations at ~/.gemini/antigravity in the same format with the same daemon RPCs, and is covered too: a bare invocation operates on every default store that exists on disk, so IDE sessions are listed, synced, and watched automatically alongside CLI ones — see Antigravity 2.0 (IDE) sessions.

By default, the daemon binds a different ephemeral port each agy session. agy-reader discovers the live CLI endpoint and its CSRF token from local processes on Linux, with session logs as a fallback for older versions.

The explicit export-offline command also reads supported SQLite sessions directly, without a running agy daemon or proxy. See Offline export.

Offline export

Read a supported CLI or IDE SQLite session directly, even when Antigravity and the agy-reader service are stopped. Qualified mappings include agy 1.4.4 and the IDE 2.25.0 file reads and artifact edits described in the completeness profile:

agy-reader export-offline /path/to/session.db
agy-reader export-offline ~/.gemini/antigravity/conversations/SESSION_ID.db
agy-reader export-offline --format=json /path/to/session.db
agy-reader export-offline --format=md --out=/path/to/new-transcript.md /path/to/session.db

Flags precede the path. Both formats default to stdout; Markdown is the default format. --out creates a new file (mode 0600 on filesystems with Unix permissions) and refuses existing paths, including symlinks. Prefer it to shell redirection when protecting an existing file matters: the shell can truncate a redirected file before the reader starts. The transcript is validated before the output file is created. Reported write errors remove the new file; a process kill or power loss during writing may leave a partial new output file.

The command uses one read-only SQLite transaction and includes committed WAL data. Keep the .db, -wal, and -shm files together when reading an active session, or use SQLite's backup API to make a consistent standalone copy. SQLite may create coordination files when opening a WAL database. The command does not discover or contact daemons, read adjacent sidecars, expand child sessions, or write sync sidecars.

JSON contains the reconstructed trajectory plus agyReader.offline provenance: format version, decoder profile, native source digest, and any declared source-only protobuf payloads (base64) with their locations and hashes. Empty sessions have steps: []. Markdown carries an offline notice and renders the selected session only. Neither format supplies a new daemon completeness attestation. This is a transcript export, not a lossless database backup; qualified generic wrappers are converted to their display form.

Unknown schemas, protobuf fields, or unsupported tool projections fail without publishing an output file. This command accepts SQLite .db sources only; legacy encrypted .pb sources still use the existing daemon export path.

Why

Antigravity stores conversations as protobuf steps in SQLite .db files or legacy encrypted .pb files. agy-reader makes those sessions available as readable Markdown and structured JSON for archives, analysis, transcript viewers, and custom tools. Use the daemon to fetch and sync sessions, or export supported SQLite sessions directly with export-offline.

For ongoing integrations, the file contract is <uuid>.trajectory.json next to the source .db or .pb. Consumers can read these sidecars without importing agy-reader or connecting to its process. AgentsView, a local session viewer, is one consumer of this format.

Features

  • CLI connection discovery: on Linux, matches the active agy process's HTTP listener to its explicit launch token or credentials exported to tools. Older daemons fall back to cli.log.
  • Rich transcript formatting: renders CodeAction steps as git-style diffs and converts file URI paths into clickable local links so you can jump into your IDE. An ASK_QUESTION step shows each question, its options with the selected ones marked, and the answer given (the chosen option's text, the written-in response, or that it was skipped). The permission request before a command is deliberately not shown: the transcript's step view carries neither the step's completedInteractions (the request's reason, resource and approval) nor metadata.preToolHookResults (the hook decision), so a command that ran after an approval renders like any other. Both stay in the saved <uuid>.trajectory.json payload; surfacing them needs the step view extended and is tracked separately.
  • JSON sidecars for downstream tools: daemon-backed fetches and syncs write <uuid>.trajectory.json next to the source session. Offline exports write only to stdout or the requested output file.
  • Antigravity 2.0 (IDE) coverage: reads the IDE's conversations (~/.gemini/antigravity) through the same daemon RPCs, with automatic port and CSRF-token discovery from the IDE's logs.
  • Multi-root by default: with no --root, every default store that exists is covered in one invocation — one --list, one --watch process, one doctor for both surfaces. A surface whose daemon is down (e.g. the IDE is closed) just waits; it never fails the others.

Install

Building requires Go 1.25 or newer. The reader binary has no Python, SQLite CLI, or agent-harness dependency. Fetching transcripts requires a running Antigravity daemon with access to the selected sessions; see the CSRF setup below if authentication fails.

go install github.com/mjacobs/agy-reader@latest

This drops agy-reader into $(go env GOBIN) (or $(go env GOPATH)/bin); make sure that directory is on your PATH.

To build from a local checkout instead:

go build .

For the supplied Linux user service, install into its expected location:

make install PREFIX="$HOME/.local"

This creates ~/.local/bin if needed. If you use go install instead, set the service's ExecStart to the actual installed binary path. See event-driven service installation.

Quick start

List syncable conversation sessions (does not contact the daemon):

agy-reader --list

Antigravity also writes background implicit/ trajectory files. They are not ordinary chat transcripts and are not syncable through the current daemon API, so they are hidden by default. To inspect them while debugging:

agy-reader --list --include-implicit

Pick a cascade id from that list and render it to stdout as Markdown:

agy-reader <cascade-id>

Render and save:

agy-reader --format md  --out transcript.md  <cascade-id>
agy-reader --format json --out trajectory.json <cascade-id>

Sync the sidecar for downstream tools without printing anything:

agy-reader --sync <cascade-id>

Even in the default (Markdown) mode, agy-reader writes the sidecar <uuid>.trajectory.json next to the source .db or .pb whenever it can — that's the point of the contract.

Sidecar contract

For every syncable ~/.gemini/antigravity-cli/conversations/<uuid>.db or .pb, agy-reader writes <uuid>.trajectory.json in the same directory. When both <uuid>.db and <uuid>.pb exist, as agy 1.2.17 leaves them for a forked conversation (the .pb from the moment of the fork, the .db once the fork continues), they are one conversation and the .db is its source, because it holds the later, complete history; the .pb is not read. The contents preserve the raw GetCascadeTrajectory trajectory object from the Antigravity daemon. Unknown fields, nested values, and large JSON numbers survive the fetch/write boundary. Object formatting and key order are not contractual. Consumers should tolerate unknown step types and use metadata.createdAt for step timestamps.

Machine-enforceable JSON schema & contract verification

The sidecar format and the agyReader metadata block are formally defined by a Draft 2020-12 JSON Schema.

Consumers and local CI pipelines can verify that recorded sidecars adhere to the schema before release:

# Validate one or more sidecars or directory trees:
agy-reader verify-contract ~/.gemini/antigravity-cli/conversations/
agy-reader verify-contract path/to/<uuid>.trajectory.json

# Export the embedded JSON Schema (Draft 2020-12) to stdout:
agy-reader verify-contract --schema > schema.json

Reader metadata contract v1

agy-reader owns one optional top-level namespace that is deliberately separate from the daemon schema:

"agyReader": {
  "parentCascadeId": "<bare cascade UUID>"
}
  • The block is omitted for roots and sessions whose immediate parent cannot be resolved unambiguously; an empty parent value is never written.
  • parentCascadeId names only the immediate parent. Deeper trees are recovered by following parent pointers, so there is no depth/level field.
  • Old sidecars without agyReader remain valid. Unknown sibling fields inside agyReader are preserved when the parent pointer is stamped or updated.
  • The stored ID is deliberately bare. A consumer using qualified session IDs must add its own source prefix (for agentsview, antigravity-cli:<parentCascadeId>) and set the relationship to subagent explicitly.
  • The sidecar-shape fingerprint excludes the entire agyReader subtree. Format audits therefore continue to measure daemon-owned schema only.

Normal one-shot sync and watch use two passes: first fetch/write every eligible sidecar in the batch, then resolve relationships across the sibling corpus and stamp unambiguous children atomically. Existing sidecars can be migrated offline, without a running daemon:

agy-reader backfill-parent-links
agy-reader backfill-parent-links --root ~/.gemini/antigravity-cli
agy-reader backfill-parent-links --root ~/.gemini/antigravity-cli --root ~/.gemini/antigravity

Backfill is idempotent. Missing parent sidecars do not fail the scan; malformed, stale, or conflicting evidence is reported diagnostically and never guessed or silently overwritten.

The resolver prefers the daemon's metadata.parentConversationId and invokeSubagent.results[].conversationId fields. Older agent-definition paths and prompt-matched invocation responses are fallbacks. Messages between agents do not establish ancestry. Cyclic links are diagnosed before new stamps are written and excluded from the reader's rendered tree.

To repair existing incorrect stamps, first stop writers to the selected store and back up its sidecars, then run:

agy-reader backfill-parent-links --repair --root ~/.gemini/antigravity-cli

Repair recomputes every parent from the available directional evidence, replaces stale pointers, and clears unsupported, conflicting, or cyclic ones. Use the complete conversations directory: a partial copy may omit the evidence needed to retain a valid link. Daemon-owned payloads and other agyReader fields are preserved. Re-ingest affected sessions in downstream consumers afterward. Ordinary sync/watch does not replace historical stamps automatically.

Use the repair and rollback runbook for the complete stop → backup → repair → verify → restart workflow. Its Python helper checks payload preservation, freshness timestamps, and parent cycles. The repair command itself needs only the reader binary.

Antigravity 2.0 (IDE) sessions

Antigravity 2.0 stores its conversations at ~/.gemini/antigravity in the same shape as the CLI (conversations/<uuid>.db, identical SQLite schema — see COMPATIBILITY.md) and runs the same language-server daemon with the same RPCs. (Sessions from the deprecated pre-2.0 IDE share the same store.)

No configuration is needed: when the store exists, a bare invocation covers it automatically. --list gains a surface column (cli/ide) whenever more than one root is in play, --watch runs one loop per root in a single process, and doctor reports each root as its own block.

To pin the roots explicitly, pass --root — it is repeatable, and explicit roots suppress store discovery entirely (as does ANTIGRAVITY_CLI_ROOT, which pins a single root):

agy-reader --root ~/.gemini/antigravity --list           # IDE store only
agy-reader --root ~/.gemini/antigravity <cascade-id>
agy-reader --root ~/.gemini/antigravity-cli --root ~/.gemini/antigravity --watch
agy-reader --root ~/.gemini/antigravity doctor

When a cascade id is fetched with multiple roots active, the roots are searched in order and the daemon, CSRF configuration, and sidecar location all follow the root that holds the session. An id that is on no root's disk (e.g. one the daemon holds only in memory) is probed against each root's daemon in order.

Two things differ from the CLI, and both are handled automatically:

  • Port discovery. The IDE daemon logs its ephemeral HTTP port to ~/.config/Antigravity/logs/language_server.log (the platform config dir on macOS/Windows) instead of a cli.log inside the session root. agy-reader detects an IDE root — no cli.log, plus the antigravity_state.pbtxt marker or the default IDE path — and reads the right log. ANTIGRAVITY_DAEMON_URL still overrides everything.
  • CSRF. The IDE launches its daemon with --csrf_token <token>, and that daemon rejects RPCs missing the matching x-codeium-csrf-token header. The CLI in agy 1.2.4 also requires a token; older CLI daemons may not. For the IDE, agy-reader reads the token from the newest daemon spawn command recorded in the IDE's main.log (a fresh token is minted on every IDE restart) and attaches the header only when a token was found. Set ANTIGRAVITY_CSRF_TOKEN to override discovery.

As with the CLI, the daemon only runs while the IDE is open; sidecars written earlier remain readable after it closes.

Watch mode

agy-reader --watch                       # 30s interval (default)
agy-reader --watch --watch-interval=10s  # custom interval

Polls every session root, fetches a trajectory for any conversation .db or legacy .pb whose sidecar is missing or stale, and writes the sidecar atomically. After writing the batch it resolves and stamps unambiguous immediate-parent links across the sibling sidecars. With multiple roots (the default when both stores exist) one process runs one loop per root, with log lines labeled by surface; each loop discovers and re-discovers its own daemon independently, so an IDE restart on one surface never disturbs the other. Daemon errors are non-fatal — connection-refused logs once per failure streak and the loop retries on the next tick; a root whose daemon is down (e.g. the IDE is closed) simply waits. SIGINT or SIGTERM drains in-flight work and exits cleanly. With --watch-idle-timeout set, the process exits only once every root's daemon has been idle that long.

On Linux, watch mode remembers successful SQLite and legacy protobuf verification checks in memory and in $XDG_CACHE_HOME/agy-reader/verification-<root-hash>.json (normally ~/.cache/agy-reader/). A later tick or watcher process can reuse a check while the database, WAL, rollback journal and sidecar retain the same file identity, size, mtime and ctime. While the WAL has content, as it does for a session agy still has open, the WAL-index header in the -shm file must also be unchanged, and the WAL must hold no frames after the ones that header publishes. SQLite writes a commit's WAL frames before it publishes them in that header, so a check during a commit can see the previous content under the new WAL metadata, and publishing changes only the header. If the writer stops before publishing, the next connection to open the database makes the commit visible with no change on disk. A session whose WAL has content is checked in full on every tick while its header cannot be read consistently or its WAL holds unpublished frames. A session whose rollback journal has content is always checked in full. Logs distinguish verification: N reused, M checked; reused checks mean previously verified content with unchanged file metadata, not a new content comparison. Both SQLite and legacy .pb checks are counted; held unsupported sources are excluded. Legacy reuse tracks the source and sidecar, and stops if a sibling SQLite file appears. A held unsupported source gets the same reuse in memory: after one full read confirms its content is unchanged, it is not read again until its file metadata changes or that confirmation expires under the rule below. A held source whose WAL or rollback journal has content is read on every tick.

How long a check is reused depends on how long the session had been quiet when it was checked, counted from the latest mtime or ctime of the database, a WAL or rollback journal with content, and the sidecar. A session that changed in the last 20 minutes or so keeps a limit of four to five minutes. A quieter one is reused for a quarter of its quiet time: 15 minutes after an hour of quiet. The limit stops growing at five to six hours, reached after about a day of quiet, and never reaches six hours. Both limits are staggered by source. Any change the comparison above can see, such as a write, a chmod or a newly published WAL commit, still forces a full check on the next tick, however long the limit; the limit only bounds how long a change that leaves all of that metadata as it was could go unnoticed. ctime is set by the kernel and cannot be set back, so backdating a file's mtime cannot lengthen reuse. Entries are discarded across reboots or verification-profile/cache-format changes. Only full successful checks create entries; errors and incomplete results remain retryable. The cache contains paths and file metadata, not transcript content or credentials. It is written atomically with private permissions; missing or damaged cache files cause full checks, and write failures leave the watcher working with its memory cache. Deleting the cache files is safe. Other platforms retain full checks.

doctor and one-shot operations always verify independently. This optimization assumes ordinary local filesystem metadata behavior; it is not protection against someone deliberately forging cache files or filesystem metadata. A changed active session still needs content verification and, after a successful sync, parent-link resolution across the sidecar corpus.

On Linux, CLI discovery checks the daemon's open session log and owned listening socket before using credentials. By default, discovery uses the ANTIGRAVITY_LS_ADDRESS and ANTIGRAVITY_CSRF_TOKEN values agy exports to a live tool process. A process environment is readable only by its own user, so the token stays private to you. A running watcher keeps that token in memory after the tool exits. A fresh watcher that you start yourself, such as the systemd service, cannot authenticate until it catches another tool process; commands shorter than its polling interval may be missed entirely.

Single-user machines only: for reliable discovery even while agy is idle or the watcher restarts, launch agy with a fresh token:

agy --csrf_token="$(uuidgen)"

Keep --csrf_token first, before any other agy arguments. This launch flag was verified with agy 1.2.5 but is omitted from its help output. agy-reader reads it from the live process's arguments and pairs it only with the HTTP listener recorded in that process's open log. It does not change the token of an already-running agy session.

A command's arguments are world-readable on Linux through /proc/<pid>/cmdline, unlike its environment. On a host you share with other Unix accounts, anyone with a local login can therefore read a launch token, find the loopback listener, and make authenticated RPCs to the daemon — including listing and reading conversations. Do not use the launch flag there; stay on the exported-environment path above, which keeps the token out of other users' reach, or use the test-pair helper's no-flag or shim mode (below), which pair a watcher with agy without putting the token in any process's arguments. On a shared host, mounting /proc with hidepid=invisible (or hidepid=2) also hides other accounts' processes, including their arguments. agy-reader itself never logs the token or writes it to disk.

An agy started by the test-pair helper in shim mode shows only a placeholder in its arguments. Its token stays in agy's own /proc/<pid>/environ, which only your user and root can read, and discovery reads it there. So a watcher you start yourself, such as the systemd service, can authenticate to that agy even while it is idle.

Every discovery path requires readable /proc entries in the same Linux network namespace and a daemon log under the selected root (cli.log or log/cli-*.log). This avoids stale cli.log ports left by short-lived CLI invocations. Other platforms, restricted /proc access, and custom log locations can use ANTIGRAVITY_DAEMON_URL and ANTIGRAVITY_CSRF_TOKEN overrides. Authentication rejection stops the batch with one authentication failed; sync blocked message per poll instead of repeating the same failure for every session.

Running a test pair

scripts/agy-pair.py starts agy and this checkout's reader together. It reads agy's HTTP port from that launch's own log, checks that agy itself listens on that port, confirms the token with an authenticated RPC, then starts the reader with ANTIGRAVITY_DAEMON_URL and ANTIGRAVITY_CSRF_TOKEN in its environment, so the watcher stays with that agy process even if other sessions are running.

agy writes that log to log/cli-pair-<time>-<random>.log under --root, the place where agy-reader's discovery matches an agy to its store. A watcher you start separately, such as the systemd service, can therefore find this agy too; the last column below says when it can authenticate. An installed watcher for the same store then syncs alongside the pair's reader, as it does for any other agy session. Discovery also recognizes agy by its name, so give --agy a path whose last part is agy; a symlink named agy to a versioned file works, and agy keeps that path as its first argument.

The helper uses the store's log directory only if it and every directory above it belong to you or root and no other account can write to them, unless the directory has the sticky bit, as /tmp does. Otherwise another account could swap agy's log for a file of its own and read what agy writes, or point the name at one of your files. In that case the helper keeps agy's log in its private run directory, says so on stderr, and a watcher started separately does not find that agy. A group-writable directory counts even when you are the only member of the group, as with a umask of 002; remove its group write permission to use it. The same fallback applies when the helper cannot create the log directory or a file in it, for example in a store you made read-only: the reader writes only under conversations, so the pair still starts. The helper also reads agy's port through the log file it created, never by the file's name, and refuses a --log-dir that another account could change.

--token-mode chooses how agy gets its token:

Mode How agy gets the token Sync starts Token in process arguments A watcher started separately authenticates
argv (default) a fresh token as agy's first --csrf_token argument at once, even while agy is idle yes, readable by every local account at once, from agy's arguments
no-flag agy picks its own; the helper reads it from the environment of agy's first tool process after agy runs its first tool no once it sees a tool process, as with any agy started without the flag
shim (opt-in) a preload library swaps a placeholder argument for the token before agy reads its arguments at once no; arguments show --pair_token_slot=1 at once, from agy's own environment
  • argv is for single-user machines only, because of the process-argument exposure described above.

  • no-flag works on Linux only. The helper polls this agy's own process tree every 20 ms for a process whose exported address matches the logged port, and confirms the token before starting the reader. A token it has found stays a candidate until agy accepts it or refuses it with 401 or 403, so a slow or failing first check does not lose it when the tool that exported it has already exited. --startup-timeout bounds only the wait for agy's listener; the wait for the first tool has no limit, and the helper says on stderr that sync is pending. A session with no tool calls is not synced during that run. Each watch tick fetches every conversation whose sidecar is missing, so a later authenticated watcher, such as the next pair in any mode, picks such a session up. The helper catches the token itself so that its reader starts as soon as possible. A watcher started separately finds this agy through its log but learns the token only from a tool process it sees, so it can miss commands shorter than its polling interval.

  • shim needs make shim (a C compiler) and works only on Linux with glibc. It depends on agy's hidden --csrf_token flag in a closed-source binary that updates often; it was verified with agy 1.2.17.

    --agy must name the agy executable itself. Every process started with the library loads it, so a wrapper script or a launcher such as a mise shim would receive the real flag and pass it to agy in its arguments. The helper therefore refuses, before starting anything, an --agy that is not a native (ELF) executable containing the flag's name, and the library swaps in the token only in a process running that exact file. In any other process it clears the token, so agy reached through a wrapper rejects the placeholder. Point --agy at the real binary instead. One case remains: if agy re-ran its own executable with its arguments, the new process's arguments would hold the token. That did not happen with --sandbox. Once agy accepts the token, the helper checks the arguments of agy and every process under it, and stops the pair if the token is there.

    Before agy starts any tool, the library removes LD_PRELOAD, ANTIGRAVITY_PAIR_TOKEN and ANTIGRAVITY_PAIR_EXE from agy's environment, so the tools agy runs inherit none of them. agy's own /proc/<pid>/environ shows the environment agy started with, and removing a variable does not change it. That file therefore still lists LD_PRELOAD with the library's path, ANTIGRAVITY_PAIR_EXE with agy's path, and ANTIGRAVITY_PAIR_TOKEN with the token. The token stays there on purpose: a watcher you start separately reads it there to authenticate (see Watch mode). Only your own user and root can read that file. Discovery trusts the variable only while --pair_token_slot is still agy's first argument, which agy accepts only after the swap. In any other process, or when no swap happened, the library zeroes the token's bytes and leaves only the empty ANTIGRAVITY_PAIR_TOKEN= name. The helper sets agy's LD_PRELOAD to the library alone: an LD_PRELOAD you already had does not apply to agy or its tools in this mode, and the helper says so on stderr. The reader keeps it.

    It fails closed: the helper refuses to start without the library, and if the library does not load, agy rejects the placeholder flag and exits with status 2. The helper also refuses a library whose path contains a space or colon, such as a checkout under My Projects: LD_PRELOAD splits at those characters, so the library could never load. Copy it to a path without them and pass that path with --shim.

In every mode the reader receives the token through its environment, and agy hands its token to every tool it runs; in shim mode agy's own environment holds it too. Processes running as your own user can read all of these. The no-flag and shim modes protect against other Unix accounts and anything that records process arguments, such as monitoring agents, audit logs, or pasted ps output.

The helper requires Python 3.10+; detached mode also requires tmux. Build the reader first:

make build

# Foreground: use agy normally; exiting agy stops its reader.
scripts/agy-pair.py run
scripts/agy-pair.py run -- --continue
scripts/agy-pair.py run --token-mode=no-flag
make shim && scripts/agy-pair.py run --token-mode=shim

# Detached: each named pair has its own tmux session.
scripts/agy-pair.py start --session audit
scripts/agy-pair.py attach --session audit
# Detach with Ctrl-b d; both processes keep running.
scripts/agy-pair.py status --session audit
scripts/agy-pair.py restart --session audit
scripts/agy-pair.py stop --session audit

run and start accept the same launch options; put agy's arguments after --. For example, run --watch-interval=2s -- --conversation ID resumes a conversation with a two-second watcher interval. --reader /path/to/agy-reader selects another build. Restart reuses the original arguments, token mode, and working directory, with a new token and log directory; --continue or --conversation is needed if you want agy to resume a conversation on every restart. tmux runs the detached pair directly, not through your login shell, so agy receives its arguments exactly as given even when that shell is fish or another shell that quotes differently from sh.

The helper prints the private per-run directory containing reader.log and agy.log, a link to agy's log under --root; use tail -f /path/to/reader.log to inspect sync activity. Logs remain after shutdown. --log-dir /path/to/logs selects the run directory's parent; agy's log stays under --root. The helper does not print or save its token separately. status reports whether the detached supervisor is running or has exited; the authenticated startup message and reader log show readiness and sync results. An exited tmux pane remains available for inspection until you stop or restart it.

The watcher polls every second by default and writes sidecars in the normal CLI store, ~/.gemini/antigravity-cli. --root changes only the store the reader watches and where agy's log goes; it does not relocate agy's data or isolate conversations. Inherited ANTIGRAVITY_* endpoint, token, and reader-root overrides are cleared for the pair. Existing agy sessions and installed watcher services are left running.

If either process exits, the helper stops its partner. Startup times out after 30 seconds (--startup-timeout; in no-flag mode it covers only agy's listener); shutdown allows five seconds before killing an unresponsive child (--stop-timeout). stop and restart wait up to 15 seconds for cleanup; increase their --timeout if you selected a longer stop timeout. A foreground run preserves agy's exit status; startup or watcher failure returns nonzero. Very short print-mode runs may exit before the pair is ready or before the final watcher tick, so a successful launch alone does not guarantee a final sidecar.

A process the helper is not allowed to signal, such as one an --agy wrapper ran with sudo, is reported and left running; the rest of the pair still stops. The helper also stops what such a process starts, but only what it finds while it is still stopping the pair. It looks under that process on each check during the stop timeout, and once more when the timeout ends and it stops and kills what is left. It never waits for that process to exit. So these are left running:

  • anything it starts after the helper has finished. If everything else exits on SIGTERM, the helper finishes at once, so the command that sudo -u "$USER" runs once its password is typed is usually left running;
  • anything it starts after the helper's one last look under it at the end of the timeout, even while the helper is still killing the rest. For a process that first refuses then, that look comes just after its refusal;
  • anything started under a process that first refuses at the end of the timeout after the helper's first four looks there. It searches under such processes only in those four looks, so that it always finishes; this matters only for a chain of them, each started as the one before refuses;
  • anything started after the helper's last look at the end of the timeout by a process it had stopped (SIGSTOP), when a process it may not signal keeps resuming that one (SIGCONT). This can happen however the stop ends: after a look that finds nothing new, when the resumed process starts one more just before the kill, or at the sixteenth look, where the helper stops looking so that it always finishes. Without such a resume, a stopped process cannot start another and the helper finishes within a few looks. The helper says so when new processes are still appearing at the sixteenth look, or when it sees a process it stopped running again; a resume just before the kill can go unnoticed. Either way, it kills everything it has found.

Doctor

agy-reader doctor checks the daemon connection, version compatibility, sidecar verification status, and watcher. Start here when sessions are missing from sync output or a downstream tool is showing stale transcripts. It checks the reader's state; it does not verify that a downstream tool has ingested the sidecars.

agy-reader doctor
  surface:     cli
  daemon:      reachable (http://127.0.0.1:51847)
  agy version: 1.2.14  (compatible)
  sidecars:    SQLite 23 verified, 0 incomplete, 0 unknown (supported daemon projection)
  watch:       running

  exit 0 — healthy

It checks the following:

  • surface — whether the root belongs to the CLI (agy) or the Antigravity IDE, which determines where the daemon's port (and CSRF token) are discovered from.
  • daemon — whether the Antigravity daemon is reachable, resolved the same way the CLI does: a pinned ANTIGRAVITY_DAEMON_URL if set, otherwise CLI process discovery (with cli.log fallback) or the IDE's language_server.log. An unpinned daemon that is simply not running is informational (it runs only while agy/the IDE is open). A pinned override that is unreachable is actionable, since a stale pin never self-heals and the CLI would keep using it.
  • csrf — whether a CSRF token was discovered. CLI roots show this line when a token is available; IDE roots always report it. A reachable IDE daemon with no token is actionable: every RPC would be rejected until a token is found or pinned via ANTIGRAVITY_CSRF_TOKEN.
  • auth — a reachable CLI daemon that rejects a read-only RPC is reported as rejected — sync blocked, and fails doctor even if the IDE root is healthy.
  • agy version — the running agy --version compared against the baseline recorded in COMPATIBILITY.md. A skew means the format audit should re-run.
  • sidecars — SQLite and legacy encrypted .pb sessions are verified, incomplete, or unknown using native source checks, as in watch mode. Legacy files must authenticate and decode before their daemon responses can qualify.
  • watch — best-effort detection of a separate agy-reader --watch process covering the reported root, resolved the way the watcher itself would: its explicit --root flags, else its own ANTIGRAVITY_CLI_ROOT pin, else the default stores under its own HOME (both read from the watcher's process environment). Linux only; reported as unknown elsewhere, or when a watcher's environment cannot be read (a different user's process).

With multiple roots (the default when both stores exist), doctor prints one block per root, each headed by its root path, and a single exit line.

Exit codes: 0 when nothing is actionable, non-zero when there is — stale or missing sidecars (run agy-reader --watch to refresh them all, or agy-reader --sync <cascade-id> per session), an agy-version skew versus the recorded baseline, or a pinned ANTIGRAVITY_DAEMON_URL that is unreachable (a stale pin never self-heals, since agy rebinds a new port each start). A daemon that is simply not running with no pin is not by itself an error, so doctor is safe to wire into a health check that runs while agy is closed.

With multiple roots, explicitly requested roots (--root or ANTIGRAVITY_CLI_ROOT) are hard requirements: any unhealthy one fails the run. A discovered root with an unavailable daemon can report waiting while another root is healthy, provided it has no incomplete or unknown native sidecars. Missing sidecars and older sidecars without completeness evidence are unknown, so they fail doctor even when their daemon is closed and another root is healthy. Legacy .pb conversations follow the same rule. All-roots-unhealthy and CLI authentication rejection also exit non-zero.

Native completeness checks

A successful daemon response can still contain an old in-memory trajectory. For supported SQLite sessions, the reader compares a full raw RPC response against a read-only database snapshot, including committed WAL data. It also checks the ordinary RPC response against the supported display projection and rechecks the source before writing. Equal step counts and matching timestamps alone cannot pass these checks.

The sidecar keeps the ordinary daemon response, including unknown JSON fields and large numbers. agyReader.completeness records the checked source and payload digests; existing reader-owned parent links survive refreshes. It certifies the supported daemon display projection, not an archive of generic wrapper fields that the ordinary RPC omits. A WAL checkpoint with unchanged content does not force a rewrite. Old sidecars without evidence are unknown until a successful verification replaces them.

On a content mismatch, the reader tries at most eight other existing daemon endpoints admitted by the same root ownership and credential checks. It never starts or stops a daemon and never overrides ANTIGRAVITY_DAEMON_URL. If none matches, watch leaves the sidecar unchanged and retries later. One-shot reads may return an existing cache with a warning; that fallback cannot refresh its contents or freshness evidence.

Unknown source fields, schemas, enum values, or projections fail closed and need a compatibility update. When the source itself causes the refusal (it cannot be read, lies outside the supported profile, or has a projection the profile does not support), watch logs it once and does not fetch that conversation again until its source content or modification time, its sidecar, or the daemon endpoint changes, or the watcher restarts; it still counts as skipped on each tick. A refusal caused by the daemon's response, such as malformed JSON or an enum name outside the profile, is retried on every tick, as is an incomplete (stale) daemon response. See the supported profile for its exact limits and schema provenance. These checks do not install a new watcher or repair historical sidecars until the updated reader is run.

Troubleshooting

Auto-discovery failed and ANTIGRAVITY_DAEMON_URL is not set

On Linux, agy-reader first looks for the active CLI process and its launch token or exported tool credentials. As a fallback, it scans cli.log inside the session root directory (~/.gemini/antigravity-cli/ or $ANTIGRAVITY_CLI_ROOT) to locate the active HTTP port. For an IDE root (~/.gemini/antigravity) it scans ~/.config/Antigravity/logs/language_server.log instead — that log only exists once the IDE has run on this machine.

If the log file is missing, empty, or the server is unresponsive, auto-discovery will fail. You can troubleshoot by:

  1. Ensuring agy is running, as the daemon is only active during an active session.
  2. Manually overriding the port if necessary. Find the port using:
    ss -tlnp 2>/dev/null | grep agy            # Linux
    lsof -iTCP -sTCP:LISTEN -anP | grep agy    # macOS
    Use the port explicitly logged as for HTTP, not the HTTPS/gRPC port. For CLI versions that require CSRF, also supply the matching token exported to a CLI tool process. Export the HTTP endpoint manually:
    export ANTIGRAVITY_DAEMON_URL=http://127.0.0.1:<port>

connection refused

The daemon only listens while agy (Antigravity CLI) is running, and the port changes each time agy restarts. Ensure agy is running. If you are manually specifying ANTIGRAVITY_DAEMON_URL, remember to update the port to match the new session.

daemon error 5xx or unknown cascade id

The daemon only knows about sessions it has loaded. Calling LoadTrajectory first (which agy-reader does automatically) usually solves this, but the daemon will refuse if the session truly doesn't exist or its key is unavailable. The current daemon LoadTrajectory request accepts only a cascade ID and resolves it under conversations/; implicit/ files are hidden by default and are not synced by watch mode. Use --list --include-implicit only when debugging those unsupported background traces.

no sessions found

Set ANTIGRAVITY_CLI_ROOT (or pass --root) if your sessions are not at the default locations ~/.gemini/antigravity-cli / ~/.gemini/antigravity.

Configuration

Env var Purpose Default
ANTIGRAVITY_DAEMON_URL Daemon base URL override (optional, auto-detected by default) unset (optional fallback)
ANTIGRAVITY_CLI_ROOT Pin a single session root (suppresses store discovery) unset (each default store that exists)
ANTIGRAVITY_CSRF_TOKEN CSRF token override for daemons that enforce one unset (auto-discovered for Linux CLI and the IDE)
AGY_READER_LIVE Enable live daemon smoke test unset (test skips)
AGY_READER_TEST_UUID Cascade id to use in the live test unset

Running on a schedule

agy-reader does not ship a daemon installer. --watch is a long-running loop that can run under your process manager. Linux users can follow the event-driven systemd installation guide, using the tracked service and path units. They start the reader when Antigravity writes session data and stop it after both daemons go idle. The examples below are alternatives for an always-on watcher.

systemd (user service, Linux)

~/.config/systemd/user/agy-reader.service:

[Unit]
Description=agy-reader sync loop
After=default.target

[Service]
Type=simple
ExecStart=%h/.local/bin/agy-reader --watch
Restart=on-failure
RestartSec=30

[Install]
WantedBy=default.target

Enable with systemctl --user enable --now agy-reader. This example assumes installation with make install PREFIX="$HOME/.local". Do not combine it with the event-driven service configuration.

launchd (macOS)

~/Library/LaunchAgents/dev.mjacobs.agy-reader.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
        "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>dev.mjacobs.agy-reader</string>
        <key>ProgramArguments</key>
        <array>
            <string>/usr/local/bin/agy-reader</string>
            <string>--watch</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
        <key>KeepAlive</key>
        <true/>
        <key>StandardOutPath</key>
        <string>/tmp/agy-reader.out</string>
        <key>StandardErrorPath</key>
        <string>/tmp/agy-reader.err</string>
    </dict>
</plist>

Load with launchctl load -w ~/Library/LaunchAgents/dev.mjacobs.agy-reader.plist. Replace /usr/local/bin/agy-reader with your installed binary's absolute path.

Testing

The Linux audit-script tests also need Bash and Python 3. make check runs formatting, Go vet/tests, the Python maintenance tests, and a build.

go test ./...
make maintenance-test # Python 3: repair backup/verification tests

The daemon smoke test is gated. To exercise it against a live agy:

AGY_READER_LIVE=1 AGY_READER_TEST_UUID=<some-cascade-id> go test ./internal/daemon

Compatibility

The Antigravity CLI (agy) can change its local session format between releases. The last agy version agy-reader was verified against — along with a deterministic schema fingerprint and the commit it was verified at — is recorded in COMPATIBILITY.md.

To re-verify after an agy upgrade, run the audit helper directly:

skills/agy-format-audit/scripts/audit_format.sh          # read-only: print a record
skills/agy-format-audit/scripts/audit_format.sh --record # overwrite COMPATIBILITY.md

These commands require a repository checkout, Go 1.25+, Bash, Python 3, Git, and the sqlite3 CLI; jq is optional. Python uses only its standard library. File selection and hashing do not require GNU find or sha256sum. The optional agentsview tests require a separate checkout selected with AGENTSVIEW_DIR. See the full audit workflow.

For a broad audit, collect fresh responses with the strict sweep first:

go build -o bin/agy-reader .
bin/agy-reader audit-sweep --out /path/to/new-private-audit
AGY_SIDECAR_CORPUS=/path/to/new-private-audit/corpus \
  skills/agy-format-audit/scripts/audit_format.sh --corpus-swept

The sweep selects one store (CLI by default), fetches and renders every conversation, and writes corpus/ plus manifest.json in a new private directory. Any fetch or render failure exits nonzero and leaves an incomplete manifest. It never uses cached sidecars or overwrites live sidecars. It also records whether ordinary sync could verify each SQLite conversation; rendering is not syncing. The helper refuses a record unless the newest conversation, and any listed in AGY_QUALIFY_SESSIONS, passes that reader-sync check. After verifying the serving daemon's version and inspecting the results, add --record.

COMPATIBILITY.md is embedded in the binary at build time. Recording updates the checkout only. Rebuild/reinstall the reader and restart its watcher to update an installed binary's doctor baseline; record with a clean source tree when you need the commit to identify exactly what was tested.

If the newest database has just been created, the helper waits up to 35 seconds for agy-reader's watcher to write its first sidecar. If the sidecar shape still cannot be computed, the audit exits with a finding and will not print or overwrite a compatibility record.

It is read-only by default and prints an updated record; pass --record to overwrite COMPATIBILITY.md once you've confirmed the run qualifies. A re-run reports UNCHANGED when the schema fingerprint matches the recorded baseline and DRIFT when it does not.

The agy 1.2.10 audit records a live three-worker scenario and 240 freshly serialized conversations. Storage and raw-payload checks passed; bidirectional subagent messages exposed the reader parent-link cycle addressed by directional evidence and explicit repair above.

The agy 1.2.5 baseline report records coverage from 229 freshly serialized conversations and retains the canonical field/type list behind its fingerprint. Use its comparison procedure for a broad baseline after an upgrade. The newest paired sidecar is a spot check; an empty trace does not exercise the step payloads that a broad corpus covers.

The helper is also packaged as the agy-format-audit agent skill. Its tracked source lives under skills/; make install-skills symlinks it into the gitignored agent skill directories (.claude/skills/, .agents/skills/) so Claude Code and other agents can invoke it.

Status

Active development. Currently supports:

  • CLI port and CSRF discovery from Linux processes, with cli.log fallback; IDE discovery from language_server.log and main.log.
  • Antigravity 2.0 (IDE) conversations, discovered automatically alongside the CLI store, including CSRF token discovery for the IDE daemon.
  • Repeatable --root for explicit multi-root operation.
  • Single-shot fetch and render.
  • Continuous polling sync via --watch — one loop per root in one process.
  • Interactive formatting (clickable file links, code diffs, clean text layouts).

Native protobuf decoding supports completeness verification and offline rechecking of existing attestations for modern SQLite .db and legacy encrypted .pb sessions. export-offline reconstructs transcripts directly from supported SQLite sources. The normal fetch/sync commands still use the daemon, with verified sidecars for cached reads. Legacy decryption uses the fixed format key distributed in agy 1.4.4. Its separate verification profile authenticates the whole file and applies the same strict schema and projection checks as SQLite. See completeness.

License

MIT

About

tools for antigravity-cli (agy) transcripts

Resources

Stars

16 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages