Files
AgentHub/docs/superpowers/specs/2026-09-29-antigravity-parity-design.md
T
nntrivi2001 969b06253c feat: per-tool settings, persistent settings file, Codex and Antigravity sessions, tokens and hooks (#40 B, C1, C2, D)
Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from
Settings → Dashboard to the Claude Code tab's AgentHub sub-tab
(ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub.
A home that is or contains the OS home is refused (EBADHOME).

Settings persistence: dashboard-saved settings go to
~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin
cache an update wipes. Load order: real env > the checkout's .env > that file.
The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put.

C1: codex-usage.js reads Codex rollout journals into sessions (new column
tool) and token_usage, tracked per file in codex_rollouts; forks deduped by
total vector, sub-agent threads folded into the root; pricing-sync also
prices OpenAI models; PricingSettings with a vendor filter.

C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on
a click, through a stable forwarder; Codex trust is never written. The hook
route creates tool='codex' sessions and folds sub-agent hooks into the root.

D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens
from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js
installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never
PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab;
Gemini pricing.

Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing
on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=,
clear-data covering the new tables, Codex child-thread events moving no
status, hooks owning status only while current, EFOREIGN for a forwarder
that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE
honoured from .env.

Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
2026-09-29 16:09:19 +07:00

11 KiB

Antigravity parity, part D: sessions, tokens and hooks

Issue: #40 part D. Date: 2026-09-29. Status: self-reviewed (the user asked for autonomous work with a self-review per part). Mirrors parts C1 and C2 (2026-09-29-codex-usage-design.md, 2026-09-29-codex-hooks-design.md).

Spike: what agy offers

Verified on agy 1.2.13 in an isolated HOME. agy keeps its login in the OS keyring, so that run used the user's own account.

Tokens

Not in the transcript, but in agy's own SQLite file per conversation: ~/.gemini/antigravity-cli/conversations/<conversation id>.db.

  • gen_metadata has one protobuf row per model call. Field 1 is a message whose field 19 is the model name (gemini-3.8-flash) and whose field 4 is ModelUsageStats.
  • The field numbers were read from the proto descriptor embedded in the binary: 2 input, 3 output, 4 cache write, 5 cache read, 9 thinking.
  • Input does not include cache reads. Output includes thinking (171 = 73 + 98).
  • trajectory_meta.cascade_id is the conversation id.
  • trajectory_metadata_blob holds the workspace as a file:// URI.
  • agy 1.0.0 wrote a .pb file instead, and Google's service refuses 1.0.0.

Hooks

Documented at antigravity.google/docs/hooks.

  • ~/.gemini/config/hooks.json maps hook names to their events: PreToolUse/PostToolUse ({matcher, hooks} groups), and PreInvocation, PostInvocation, Stop (flat handler lists). A handler is {type: "command", command, timeout (s)}.
  • agy asks for no trust.
  • The payload carries conversationId, workspacePaths, transcriptPath (brain/<id>/.system_generated/logs/transcript_full.jsonl) and modelName. PostToolUse adds toolCall {name, args}, stepIdx and error. Stop adds terminationReason and fullyIdle.
  • The hook runs with its cwd in ~/.gemini/config, and AGENTHUB_LANE_ID does not reach it.

Traps found in the spike

  • A PreToolUse hook that answers {} DENIES the tool ("tool call denied by pre-tool hook"). There is no neutral answer:
    • allow would override the user's permission rules;
    • ask would force a prompt.
  • No permission event, and no Stop when a run ends on an API error or on a permission that headless mode refused.
  • invocationNum restarts at 0 for every run, including --continue.
  • Running agy self-updates ~/.local/bin/agy, even with another HOME. The spike took the user's install from 1.0.0 to 1.2.13; the old binary is kept as agy.<n>.old.

Design

  1. Sessions and tokens (server/lib/antigravity-usage.js, every DASHBOARD_ANTIGRAVITY_POLL_MS, default 15 s).
    • Each conversation file is opened read-only when its mtime (or its non-empty -wal's) changes; the mtime lives in antigravity_conversations.
    • It becomes a tool = 'antigravity' session: its name and start come from the transcript, its cwd from the blob, its model from the last generation.
    • Tokens are written whole, per model.
    • The status reflects the session's last activity: the newest of its file (a non-empty -wal included), its transcript and its last hook event. It is quiet after ten minutes when it has any hook event — mid-turn a long tool call writes nothing, and after a Stop agy may be waiting for the user, with no exit hook to say otherwise — and after two minutes when it has none.
    • When a session settles to completed, its agents complete too, its waiting flags are cleared, and its antigravity lane is set idle, but only if the hooks left it running and it has no run_id. A Stop that marked it error keeps that error.
    • A file that is not agy's (SQLITE_NOTADB, SQLITE_CORRUPT, no such table) is recorded with session_id '' and retried only once it changes or at the next server start; any other failure is retried on the next pass.
    • Only the CLI's directory is read.
    • Gemini families are added to pricing-sync.js, taken from Vertex AI's or the Gemini API's LiteLLM entries.
  2. Hooks (server/lib/antigravity-hooks.js, Settings → Antigravity → Antigravity hooks, GET/POST/DELETE /api/settings/antigravity-hooks, sameOriginGuard on the writes).
    • The named hook agenthub has PreInvocation, PostToolUse and Stop, each timeout: 5. It never has PreToolUse.
    • Every other named hook is kept, and a non-object file is refused (EBADJSON).
    • The command is the stable path ~/.gemini/config/agenthub-hook.sh. The script runs scripts/hook-handler.js <Event> antigravity, prints {}, and is refreshed at every start when it is ours.
  3. Route (antigravityHookInput in server/routes/hooks.js).
    • A hook is accepted only with a UUID conversationId and a transcriptPath under the CLI's directory. That keeps out the Antigravity IDE, which loads the same global file.
    • session_id is the conversationId and cwd is workspacePaths[0].
    • A PreInvocation becomes UserPromptSubmit when invocationNum is 0, when the session has no event yet, when its last event is a Stop, when the session is not active, or when its last event is more than two minutes old; any other is dropped. PostToolUse carries tool_name/tool_input and step. Stop stays Stop — as stop_reason: "error" when it carries a non-empty error or a terminationReason matching /error|fail/i, and dropped while fullyIdle is false. Every other event is dropped.
  4. Settings → Antigravity (AntigravityConfig.tsx) holds Model Pricing (vendor="google") and the hooks card. There is no sub-tab bar, because agy has no config surface AgentHub edits.

Limits (documented, not fixable from here)

  • There is no "needs your permission" state for an Antigravity lane: agy has no hook for it.
  • A lane that a dashboard pane owns (run_id) keeps its run-owned status, as it does for Codex.
  • The Antigravity IDE's sessions are not read.
  • Sub-agents: none were seen in a CLI run, so nothing folds them.
  • The formats are undocumented: a future agy may move them. A generation that no longer decodes is skipped, not guessed.

Tests

  • Server:
    • usage: the protobuf reader; the session and per-model tokens; no re-read; quiet → completed; the stuck lane set idle, while a paned one is kept; deleted stays deleted; another tool's row untouched; a hook-made cwd kept; a non-SQLite file skipped;
    • hooks: install with three events and no PreToolUse; the forwarder prints {}; other named hooks kept; remove; EBADJSON; a hook switched off is reported; the refresh is foreign-safe;
    • route: a turn on the antigravity lane only; later PreInvocations dropped; tool calls in the timeline; IDE and bad-id hooks ignored; the settings routes, including the cross-origin refusal;
    • handler: the antigravity marker, and an unknown marker dropped;
    • pricing: Gemini from Vertex AI, and another vendor refused.
  • Client: the hooks card (install, a hook switched off, Refresh, Remove, a refusal).
  • End to end: an isolated server on :4851 with AGENTHUB_GEMINI_DIR set. The hook was installed through the API, then a real agy -p read a file. Seen:
    • the lane went running;
    • the run ended with no Stop (a shell command refused in headless mode), and the poll set the lane back to idle;
    • the session had its name, cwd, model and tokens, and view_file in its timeline;
    • Settings → Antigravity showed the card as installed, with its three events.

Found in the code review, fixed

  • Our own read-only open looked like activity. On a file agy closed cleanly, better-sqlite3's read-only open leaves an empty -wal, whose fresh mtime flipped the session back to active with "Last active" set to the read time. An empty -wal no longer counts.
  • The poll undid the hooks' running within 15 s. Activity is now the newest of the file, the transcript and the session's last hook event. The quiet window is ten minutes, not two, when that event is not a Stop, because a long tool call writes nothing until it ends.
  • A failed run showed as "ready for input". A Stop carrying an error, or a terminationReason naming one, now arrives as stop_reason: "error", and the poll keeps that error.
  • Stops and turns.
    • A Stop with fullyIdle: false is dropped, since background work is still running.
    • The first PreInvocation after a Stop is a new prompt, whatever its invocationNum.
  • A script that is not ours at the forwarder's path is never overwritten or removed (EFOREIGN, 400), in codex-hooks.js too.
  • A rewritten hooks.json keeps its mode, in codex-hooks.js too.
  • Session id and symlinks.
    • The session id is the file name's UUID; cascade_id is no longer read.
    • A symlinked ~/.gemini is matched under both paths (agyCliDirs).
  • Settling and bad files.
    • A file that is not agy's is recorded, so it is not re-read and warned about every 15 s.
    • Settling a session to completed also completes its agents and clears their waiting flags.
  • Docs. .env.example now lists DASHBOARD_ANTIGRAVITY_POLL_MS and AGENTHUB_GEMINI_DIR.

Found in the whole-change review, fixed

  • An open agy looked finished after a Stop. The ten-minute window applied only while the last event was not a Stop, so a session waiting at the prompt settled to completed two minutes after each turn. agy fires no exit hook, so an open agy and a closed one look the same: any session with hook events now gets ten minutes, a hookless one two.
  • A turn after a missed Stop was lost. A PreInvocation is now also a prompt when the session has no event yet, is not active (a run that ended with no Stop, a session the poll settled), or its last event is over two minutes old — not only on invocationNum: 0 or after a Stop.
  • Error Stops. Only a non-empty error counts, and a terminationReason counts when it matches /error|fail/i.
  • A bad file was never retried, a busy one was given up on. A file that is not agy's is recorded with session_id '', and those rows are deleted at every server start (startAntigravityUsagePoll), so a newer AgentHub tries it again; any other read failure (a busy file, our own error) is no longer recorded, so the next pass retries it.
  • Settling left the session waiting. It now clears the session's own waiting flag as well as its agents'.
  • A forwarder path that is not a file of ours (other content, an unreadable file, a directory — scriptIsOurs) is never overwritten (EFOREIGN) or removed, in codex-hooks.js too.
  • A symlinked hooks.json (a dotfiles repo's) left with no hook is emptied through the link instead of being unlinked, in both installers.