969b06253c
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.
11 KiB
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_metadatahas 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 isModelUsageStats.- 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_idis the conversation id.trajectory_metadata_blobholds the workspace as afile://URI.- agy 1.0.0 wrote a
.pbfile instead, and Google's service refuses 1.0.0.
Hooks
Documented at antigravity.google/docs/hooks.
~/.gemini/config/hooks.jsonmaps hook names to their events:PreToolUse/PostToolUse({matcher, hooks}groups), andPreInvocation,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) andmodelName.PostToolUseaddstoolCall {name, args},stepIdxanderror.StopaddsterminationReasonandfullyIdle. - The hook runs with its cwd in
~/.gemini/config, andAGENTHUB_LANE_IDdoes not reach it.
Traps found in the spike
- A
PreToolUsehook that answers{}DENIES the tool ("tool call denied by pre-tool hook"). There is no neutral answer:allowwould override the user's permission rules;askwould 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.
invocationNumrestarts at 0 for every run, including--continue.- Running
agyself-updates~/.local/bin/agy, even with anotherHOME. The spike took the user's install from 1.0.0 to 1.2.13; the old binary is kept asagy.<n>.old.
Design
- Sessions and tokens (
server/lib/antigravity-usage.js, everyDASHBOARD_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 inantigravity_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
-walincluded), 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 aStopagy 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 setidle, but only if the hooks left itrunningand it has norun_id. AStopthat marked iterrorkeeps thaterror. - A file that is not agy's (
SQLITE_NOTADB,SQLITE_CORRUPT,no such table) is recorded withsession_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.
- Each conversation file is opened read-only when its mtime (or its
non-empty
- Hooks (
server/lib/antigravity-hooks.js, Settings → Antigravity → Antigravity hooks,GET/POST/DELETE /api/settings/antigravity-hooks,sameOriginGuardon the writes).- The named hook
agenthubhasPreInvocation,PostToolUseandStop, eachtimeout: 5. It never hasPreToolUse. - 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 runsscripts/hook-handler.js <Event> antigravity, prints{}, and is refreshed at every start when it is ours.
- The named hook
- Route (
antigravityHookInputinserver/routes/hooks.js).- A hook is accepted only with a UUID
conversationIdand atranscriptPathunder the CLI's directory. That keeps out the Antigravity IDE, which loads the same global file. session_idis theconversationIdandcwdisworkspacePaths[0].- A
PreInvocationbecomesUserPromptSubmitwheninvocationNumis 0, when the session has no event yet, when its last event is aStop, when the session is notactive, or when its last event is more than two minutes old; any other is dropped.PostToolUsecarriestool_name/tool_inputandstep.StopstaysStop— asstop_reason: "error"when it carries a non-emptyerroror aterminationReasonmatching/error|fail/i, and dropped whilefullyIdleis false. Every other event is dropped.
- A hook is accepted only with a UUID
- 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
antigravitymarker, 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_DIRset. The hook was installed through the API, then a realagy -pread 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_filein its timeline; - Settings → Antigravity showed the card as installed, with its three events.
- the lane went
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 toactivewith "Last active" set to the read time. An empty-walno longer counts. - The poll undid the hooks'
runningwithin 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 aStop, because a long tool call writes nothing until it ends. - A failed run showed as "ready for input". A
Stopcarrying anerror, or aterminationReasonnaming one, now arrives asstop_reason: "error", and the poll keeps thaterror. - Stops and turns.
- A
StopwithfullyIdle: falseis dropped, since background work is still running. - The first
PreInvocationafter aStopis a new prompt, whatever itsinvocationNum.
- A
- A script that is not ours at the forwarder's path is never overwritten
or removed (
EFOREIGN, 400), incodex-hooks.jstoo. - A rewritten
hooks.jsonkeeps its mode, incodex-hooks.jstoo. - Session id and symlinks.
- The session id is the file name's UUID;
cascade_idis no longer read. - A symlinked
~/.geminiis matched under both paths (agyCliDirs).
- The session id is the file name's UUID;
- 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
completedalso completes its agents and clears their waiting flags.
- Docs.
.env.examplenow listsDASHBOARD_ANTIGRAVITY_POLL_MSandAGENTHUB_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 tocompletedtwo 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
PreInvocationis now also a prompt when the session has no event yet, is notactive(a run that ended with no Stop, a session the poll settled), or its last event is over two minutes old — not only oninvocationNum: 0or after aStop. - Error Stops. Only a non-empty
errorcounts, and aterminationReasoncounts 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, incodex-hooks.jstoo. - A symlinked
hooks.json(a dotfiles repo's) left with no hook is emptied through the link instead of being unlinked, in both installers.