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.
203 lines
11 KiB
Markdown
203 lines
11 KiB
Markdown
# 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.
|