fix(antigravity): fetch quota when the IDE is closed - #236
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fed1e4c31e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Thanks for this — the problem is real and the shape of the fix is right. Two layers of discovery with the CLI strictly as a last resort, only after both existing routes have failed, is the correct ordering, and fetch_agy_cli itself is careful: fixed argument array with no shell interpolation, stdin/stderr nulled, kill_on_drop, an outer timeout wrapping the CLI's own, and status/command.name both validated before the payload is trusted. Error strings are fixed rather than echoing CLI output. I have no security concerns with that invocation.
Two things I would like changed before merging, plus a request and a process note.
0. Withdrawn
This section originally argued that a closed-but-installed IDE should already return quota on
main, and asked whether this PR describes the wrong failure. That was wrong and I have removed it. We quit Antigravity and checked against a cleanmainbuild: the quota does disappear. Your title and problem statement are accurate. I had reasoned from the fall-through infetch_withwithout running it, and a code path existing turned out not to mean the route succeeds. Details in the follow-up comment; nothing below depends on it.
1. /bin/sh -l will not see the PATH this PR is looking for
let output = Command::new("/bin/sh")
.args(["-lc", "command -v -- agy"])On macOS /bin/sh is bash in POSIX mode, so -l sources /etc/profile and ~/.profile and never touches ~/.zprofile or ~/.zshrc. The default login shell is zsh, and Homebrew's PATH export lands in ~/.zprofile. For the exact population this PR targets — GUI-launched app, IDE not running, agy reachable only from the login shell — this probe returns nothing and the card still shows the OAuth error.
The repo already solved this for the Claude token harvest, and it is our code rather than something you would have to invent:
detect_login_shell()(crates/tb_core_ffi/src/agent_usage.rs:3421) resolves the real shell from$SHELL, falling back to Directory Services because launchd leaves$SHELLunset for a GUI app.harvest_shell_env_token_uncached()(:3385) invokes it as["-l", "-i", "-c", script](:3392) — the comment there records that-lalone only gets~/.zprofile, hence the-i.
detect_login_shell is private to agent_usage today, and it is gated #[cfg(target_os = "macos")] while your probe is #[cfg(unix)]. Neither is yours to solve: call it anyway, and I will land the one-line visibility change alongside this PR. macOS-only is the right scope for the Homebrew-under-zsh case regardless.
The same comparison covers two smaller problems in the probe:
fn agy_cli_artifact_candidates() -> Vec<PathBuf> {
let shell_path = discover_agy_from_login_shell();
agy_cli_artifact_candidates_from(std::env::var_os("PATH").as_deref(), shell_path)
}- It is evaluated eagerly, as an argument, so the shell runs even when
PATHalready resolvesagy. - It is uncached.
resolve_oauth_client()memoises its half behind aOnceLock, but thefetch_agy_clihalf has no equivalent, so it re-runs on every fetch that reaches the fallback. That is a login-shell spawn per minute while the popover is open (DashboardModel.pollAgentUsageends onRegistryChange.sleep(upTo: 60, ...),DashboardModel.swift:1725) and every 5 minutes from the tray (TrayAnimator.swift:435) — and for the users this PR targets, reaching the fallback is the steady state, not the exception.
Consulting PATH first and memoising the resolved path for the process collapses both.
One thing I am explicitly not asking you to change: the probe uses the blocking std::process::Command rather than tokio::process, but so does everything else in this file — fetch_local_ide runs ps and lsof (and PowerShell on Windows) the same way. That is our pattern, not something you introduced. It is worth a bound here specifically because a login shell is not ps: it executes the user's rc files, which can take seconds or block outright. So the timeout and kill_on_drop matter even though the blocking spawn itself is house style.
2. The fallback re-identifies every card, and card_id embeds positional indices
The bug is the trigger condition:
Err(primary_failure) => match fetch_agy_cli(now).await {ProviderFetchFailure distinguishes Transient from Terminal, and this fires on both. So a network blip or a 429 on the primary route is enough to switch sources — and because the CLI path mints a different card_id, every Antigravity card changes identity, then changes back on the next successful poll.
In this codebase card_id is identity. Swift matches the persisted selection as <clientId>|<cardId> exactly (WindowCardLoader.selectionKey, Sources/TokenBar/WindowCardLoader.swift:34, which shares its shape with QuotaResolver's canonical selection), and the quota-history series is keyed on it. When identity moves, the user's pinned gauge silently stops resolving and that window's recorded history restarts from nothing — with no visible change to the label to explain it. agent_usage.rs:4897 documents this exact trap for Claude's scoped weekly windows, which deliberately key on the display name rather than scope.model.id for the same reason.
Restricting the fallback to Terminal is the fix for the trigger, and I would do it regardless. But the key shape decides how much damage remains:
let card_id = format!("agy.{group_index}.{bucket_index}.{id}.v1");id is already in the string, so the indices add nothing except a second way for identity to move — if agy ever reorders its groups or buckets, every card_id shifts.
More importantly, the file already has a convention for this. Every other route resolves to model.{model_id}.v1 when a model id exists, and falls back to a positional row.*.{index}.v1 only when it does not — the local IDE path (row.cli.config), the OAuth models path (row.models), and the OAuth quota-buckets path (row.quota.bucket). So a positional key is already the codebase's degraded branch, not a namespace of its own.
Which means the right shape depends on a fact I do not have: do the agy bucket ids correspond to model ids? If they do, emitting model.{model_id}.v1 makes the fallback identity-transparent — a source switch changes nothing the user can see, which is the outcome we actually want. If they do not, agy.{id}.v1 without the indices is the honest shape, sitting alongside row.cli.config as another "nothing better available" branch.
A request: a redacted payload
Could you paste the output of agy --print /usage --output-format json with anything identifying stripped? Two reasons: it answers the question above, and it would let us add a fixture taken from the real CLI. Right now parse_agy_usage's contract rests on one live run that nothing in the repo can re-check, and the bucket ids in the tests (gemini-weekly, gemini-5h) are hand-authored — so a wire-format change would pass CI and fail in the field.
Minor
fetch_agy_clidoes not set.creation_flags(CREATE_NO_WINDOW). The Windows PowerShell discovery in this same file does, and without it Windows flashes a console on every fallback.executable_from_pathjoins the bare nameagywith noPATHEXThandling, so thePATHhalf never resolvesagy.exeon Windows. Either complete it or#[cfg]the fallback to Unix so the limitation is explicit rather than silent.agy_cli_artifact_candidates().into_iter().next()uses only the first candidate, andexecutable_from_pathchecksis_file()without checking the executable bit — so a non-executableagyearlier onPATHblocks the shell-discovered one instead of falling through to it.
CI
Fork PRs need workflow approval here, so the run was sitting at action_required and the PR showed no checks. It has been approved and CI is green on fed1e4c. Nothing for you to do — noting it so the gate is visible on the PR rather than only in your local run.
Note
Your validation section is unusually complete — exercising the fallback with GEMINI_CLI_HOME pointed at an unavailable credentials directory is exactly the case that would otherwise ship untested. Thanks for that.
|
Correction: withdraw §0 of my review. I claimed a closed-but-installed IDE should already return quota on The claim came from reading the fall-through in So: please disregard §0 entirely, including the question about whether your install location is the real cause and the suggestion that merging this would mask a discovery bug. None of that is established. What remains open is narrower and genuinely unknown: why the OAuth route does not carry the closed-IDE case, since it does not depend on the Antigravity process. Two candidates I have already ruled out — That does not change the review's substance. §1 and §2 stand on their own reading of this diff and are unaffected: the |
|
Update after digging into this on a machine that has both Antigravity.app and We found a bug on our side, separate from this PRTesting with the IDE installed but not running, the card showed So the OAuth route was silently dead for anyone with the IDE installed — the discovery could not produce a client at all. That is independent of this PR and I will send it as its own fix. It matters here only because it is a second, unrelated reason the Antigravity card can show an OAuth error, and it muddies the "IDE closed" signal. The part I need your help on: what does
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 099be6acb0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Review follow-upAddressed in
CLI response shapeOn the current Verification
|
|
Follow-up commit |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58315c4c10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Nanako0129
left a comment
There was a problem hiding this comment.
This is a thorough pass, @iF2007 — the review is resolved in the code, not just answered:
- Shell discovery reuses
detect_login_shellwith-l -i, checks the inheritedPATHfirst, caches per process, and now spawns viatokio::processwith a bound — which I hadn't asked for but is the right call for a login shell. - The fallback is restricted to
Terminal, so a transient or rate-limited primary no longer switches sources. card_idisagy.<bucket_id>.v1with the positional indices gone.- macOS-only, executable-file candidates, skip-and-retry — all as described.
I was wrong about the token cost — withdrawing that
My earlier comment claimed agy --print /usage routes to the model and spends ~24k tokens per poll. That does not hold, and you were right. The command is a local built-in that costs nothing — I re-ran the exact invocation your code uses, in the two directories that actually matter:
# an interactive shell in a project dir, and cwd=/ (where a launchd GUI app runs):
$ agy --print /usage --output-format json
{"status":"SUCCESS","command":{"name":"usage","data":{"groups":[...]}},
"num_turns":0,"usage":{"total_tokens":0},"duration_seconds":0}
num_turns:0, total_tokens:0 — no model turn. The one run that returned the chat shape with a 24k-token turn was the first agy invocation of my session (20s, num_turns:1, a real conversation_id). Re-running the byte-for-byte identical command — same --print-timeout 30s, same /tmp cwd — returns the command shape for free every time since. So the trigger is first-ever invocation, not cwd and not the timeout flag; that first call appears to do real init (download / session warm) during which /usage is sent to the model before the command is registered. The per-poll cost I described isn't real. Sorry for the noise on that.
Worth keeping only as a data point, not a request: on a machine where agy has never run, TokenBar's first poll could hit that one-time init and spend a turn. Your parser already does the safe thing — the chat shape has no command.name=usage, so it's rejected and the existing terminal error is preserved, no bad data — and it's a single occurrence, not per-poll. I wouldn't add anything for it.
Notes
detect_login_shellvisibility: you made itpub(crate)and call it throughcrate::agent_usage::detect_login_shell()— the exact one-line change I had staged to land alongside this. Yours supersedes it; I've dropped mine.- The scan bug is fixed on
main(#242, merged). That's the installed-but-closed case returningOAuth client was not found; it's independent of this PR, but it means the OAuth route now resolves a client for installed users, so this fallback is cleanly the not-installed / not-logged-in path.
From my side there's nothing blocking left — the two earlier requests (/bin/sh PATH and the Terminal restriction) are both resolved. The new commits just need a maintainer to re-approve the workflow so CI runs on them; that's on us.
…-quota # Conflicts: # crates/tb_core_ffi/src/agent_antigravity.rs
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c9c9a7723e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| executable: &Path, | ||
| now: DateTime<Utc>, | ||
| ) -> Result<Fetched, ProviderFetchFailure> { | ||
| let future = tokio::process::Command::new(executable) |
There was a problem hiding this comment.
Preserve the login-shell environment when launching agy
When agy is found only through the login-shell probe and depends on variables defined there—such as a custom GEMINI_CLI_HOME—this direct spawn inherits TokenBar's GUI environment instead of the shell environment. The fresh follow-up code extracts only the executable path in discover_agy_from_login_shell, so the fallback can find agy but then fail to find the same credentials it uses interactively; invoke it with a bounded captured shell environment or propagate the required variables.
Useful? React with 👍 / 👎.
| let card_id = format!("agy.{id}.v1"); | ||
| if let Some(window) = | ||
| quota_window(label, fraction, reset, now, card_id.clone(), Some(card_id)) |
There was a problem hiding this comment.
Document the new agy quota identity in the canonical KB
This introduces a new provider route with source = "agy" and agy.<bucket_id>.v1 card/window identities, but no canonical knowledge file is updated; docs/knowledge/plans/provider-quota-pace.md still describes Antigravity identities only in terms of model IDs. That leaves the authoritative identity mapping and fallback precedence stale for future selection/history work, so document this bucket mapping in docs/knowledge/.
AGENTS.md reference: AGENTS.md:L22-L24
Useful? React with 👍 / 👎.
|
Sync: merged #242's OAuth client scan fix into this branch. This branch now includes the merged
Since the head moved past the previously approved |
|
@codex review |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Replace the stale v1.14.1 override notes with v1.14.2's, covering the Antigravity "quota when the IDE is closed" work. `scripts/release_notes.sh` ships `release-notes.override.txt` verbatim to the Sparkle dialog / appcast / latest.json and `release-notes.override.md` verbatim as the GitHub Release body, so these files are the source of truth for this release's notes; DeepSeek generation is skipped when they are present. The deterministic "Thanks: @…" line (txt) and GitHub's "New Contributors" / "Full Changelog" tail (md) are still appended automatically from PR authorship, which credits @iF2007 for #236; the md also carries an explicit per-change credit. Change set since v1.14.1: - #236 (@iF2007): fetch Antigravity quota when the IDE is closed or not installed — OAuth remote plus an `agy` CLI fallback. - #242: fix the OAuth client scan absorbing a neighbouring value in the installed language_server binary, which returned zero client ids and disabled the OAuth route for an installed-but-closed IDE. No literal <, &, or > in either file, so the appcast CDATA/HTML escaping path is not exercised.
Problem
When the Antigravity IDE is closed, TokenBar cannot use the local IDE API. The previous OAuth client discovery could also fail to obtain a usable client, leaving the Antigravity card without quota usage.
Fix
This change uses two layers of discovery and retrieval:
agyfrom the inherited processPATH, then from the real macOS login shell using-l -i -c. No Homebrew path or fixed installation directory is assumed. The resolved CLI candidate is cached per process, and non-executable candidates are skipped.Fetch order remains local IDE API first, then the OAuth remote route. Only a terminal failure from those routes activates the optional CLI fallback. The fallback invokes
agy --print /usage --output-format json --print-timeout 30s, validates the structured usage response, and maps its groups/buckets into the existing quota windows. CLI bucket identities are stableagy.<bucket_id>.v1values, and the fallback is explicitly macOS-only.agyis optional: IDE OAuth behavior remains available, and the existing error is preserved when neither route can provide usable data.Result
agyfallback.agyCLI can provide the quota.Validation
cargo test -p tb_core_ffi antigravity --lib— 34 passed.cargo test --workspace— passed with no failures (1,783 passed; 4 documented ignored tests).cargo clippy --workspace --all-targets— passed; only existing repository warnings remain.make build— passed.make selftest— passed.rustfmt --check --edition 2021 crates/tb_core_ffi/src/agent_antigravity.rsandgit diff --check— passed.swift run TokenBar --smokereportedantigravity=4 windows.GEMINI_CLI_HOME, the IDE-closed smoke still reported Antigravity windows, exercising the CLI fallback.agy1.1.19, the slash formagy --print /usage --output-format jsonreturnedstatus=SUCCESS,command.name=usage, two groups, and four buckets; generic response/usage fields are ignored by the quota parser, with a regression fixture covering the response-only shape.