Repository navigation
consult: pre-flight auth-state cache for agy to prevent N-spawn browser-tab burst on unauthenticated state #1077
Description
Problem
When the agy (Antigravity CLI) binary is not currently authenticated, every Codev consult invocation that hits the gemini lane spawns a fresh agy --print process, and that process opens an OAuth browser tab via macOS open before it prints the OAuth URL to its stderr. Codev's consult code (packages/codev/src/commands/consult/index.ts:843-848) watches the early stream for AGY_OAUTH_MARKERS and kills agy with a non-blocking COMMENT skip the moment it sees one. That detection is correct semantically, but it fires after the browser tab has already opened. The kill closes the barn door once the horse is out.
Across N concurrent CMAP-3 consults (the architect-builder workflow runs them in parallel by design), N tabs open in rapid succession. On 2026-06-19 a user reported ~12-15 stranded OAuth tabs in a single Chrome window. The screenshot at tmp/screenshots/agy-oauth-tab-burst-2026-06-19.png in the architect worktree shows the tab strip filled with stacked "Sign in with Google: Choose an account to continue to Google Antigravity" tabs.
Preview
Why the user-side workaround does not stop the bleed
Running agy interactively once and completing the OAuth in one tab does write a persistent token, but the other in-flight consults that have already spawned agy children before that token landed cannot benefit. Each one is racing toward its own AGY_OAUTH_MARKERS detection. The user observes new tabs continuing to appear until the entire CMAP-3 burst has played out.
Where the spawn pressure compounds
Three amplifiers, each independently verified in the code:
- CMAP-3 = 3 separate
codev consult -m <model>process spawns per round (packages/codev/src/commands/porch/next.ts:529-530). Only the gemini lane spawns agy, so one agy spawn per CMAP-3 round per consult phase. - Multiple consult phases per builder. PIR runs plan-approval consult + review consult = 2 agy spawns per builder. SPIR adds spec consult = 3 spawns. AIR / BUGFIX = 1 spawn at review.
- Porch re-emits the same consult tasks when review files look "partial" (
packages/codev/src/commands/porch/next.ts:544, "Partial reviews, still waiting. Emit same consultation tasks (idempotent)."). If gemini's COMMENT-skip output is malformed (e.g. because the kill was racy and stdout was cut mid-banner), porch re-emits all 3 consult commands. The builder runs them again. The gemini one re-spawns agy. Another tab opens.
With 5 to 6 active builders mid-cycle, each running 1 to 2 consult phases, with the occasional re-emit, the math reaches the observed 12-15 in the visible burst.
What this issue tracks
A session-scoped auth-state cache + pre-flight that short-circuits runAgyConsultation when the cache says agy is unauthenticated, so the second and subsequent consult calls in a burst do not spawn fresh agy children.
The skip-with-COMMENT contract that ships today is already the right semantic behavior; this issue is about doing the skip before the agy spawn, not after.
Proposed fix
Add a cross-process, filesystem-backed auth-state cache to packages/codev/src/commands/consult/. The shape:
- Cache file:
~/.cache/codev/agy-auth.json. Stores{ state: 'auth' | 'unauth', checkedAt: <epoch-ms>, agyBinPath: '<resolved-path>', agyAuthMtime: <epoch-ms> }. Permissions 0600. - TTL: 5 minutes for
state: 'unauth'. Short enough that a user who completes OAuth in a separate terminal sees the cache invalidate promptly. Longer TTL (e.g. 30 min) forstate: 'auth', since authenticated probes are non-intrusive. - Pre-flight check in
runAgyConsultation(beforespawn(bin, args, ...)). Read the cache:- If
state: 'unauth'and the TTL has not expired: emit the COMMENT skip immediately, with reason"agy unauthenticated (cached); run agy once to sign in". No spawn, no browser tab. - If
state: 'auth'and the TTL has not expired: proceed with the spawn as today. - If the cache is missing or expired: probe agy with a fast
--print-timeout 5scall. The probe itself may open at most 1 tab if unauth (irreducible; this is the upstream agy behavior). Persist the result withagyAuthMtimeset to the mtime of agy's own credentials file if one can be located. Then either skip (if unauth) or proceed (if auth).
- If
- Cache invalidation on successful auth. When the user completes OAuth in any tab, agy writes its credentials to disk. If we can identify the credentials path (candidates:
~/.antigravity/credentials.json,~/Library/Application Support/Antigravity/credentials.json, the LevelDB at~/Library/Application Support/Google/Chrome/Default/IndexedDB/https_antigravity.google_0.indexeddb.leveldb), watch its mtime: if it changed sinceagyAuthMtime, invalidate the cache and re-probe on the next consult call. As a fallback if the credentials path is opaque, accept the TTL as the recovery window. - Cross-process safety. Multiple
codev consultprocesses started in parallel must not all race to probe simultaneously (that would defeat the point). Wrap the probe in an exclusive file lock (flock-style on~/.cache/codev/agy-auth.lock) with a short wait. The lock holder probes; others read the cache the moment the holder releases.
Acceptance criteria
- A new module (e.g.
packages/codev/src/commands/consult/agy-auth-cache.ts) exposescheckCachedAgyAuth()andrecordAgyAuthState(state, agyBinPath). -
runAgyConsultationcallscheckCachedAgyAuth()beforespawn. Onunauth(fresh), it callssettleSkipimmediately with reason mentioning the cache. - On a fresh process with no cache file, the first invocation probes agy with
--print-timeout 5sand writes the result. - Concurrent
codev consultprocesses do not all probe agy simultaneously: a file lock serializes the probe; non-holders read the cache when the lock releases. - Cache TTLs: 5 min for
unauth, 30 min forauth. Both overridable via env (CODEV_AGY_AUTH_CACHE_TTL_UNAUTH_MS,CODEV_AGY_AUTH_CACHE_TTL_AUTH_MS) for tests. - When the user completes OAuth in another window during the unauth-TTL window, the next consult call after the TTL expires re-probes and finds
auth(no manual cache clear required). - Smoke test: spawn 5 parallel
codev consult -m geminiinvocations against an unauthenticated agy. Verify at most 1 spawn of agy happens, not 5; the other 4 short-circuit on the cache. - Smoke test: with valid
authcache, acodev consult -m geminiinvocation completes normally (no behavior regression). -
codev doctor'sverifyAgy(packages/codev/src/commands/doctor.ts:380) reads from the same cache when fresh, instead of probing on every doctor run. (Optional but obvious extension.) - Updates: brief mention in the consult lane docs in CLAUDE.md / AGENTS.md and the
consultcommands reference undercodev/resources/commands/consult.md.
Alternatives considered
- Don't cache; serialize agy spawns with a global lock. Replace step 5 of the proposal with a process-wide lock: only one
runAgyConsultationmay have agy spawned at a time, all others wait. Eliminates the burst, but adds latency to the legitimate case and does not stop a single unauth spawn from opening 1 tab per N consult phases. Worse user experience than the cache for the same problem. - Detect unauth without spawning at all by stat'ing agy's credentials file. Cheapest possible pre-flight, no spawn ever needed. Blocked on: agy's credentials path is not officially documented, the LevelDB form is non-trivial to read from Node, and any path change in a future agy release breaks the heuristic silently. Keep this as a hint to invalidate the cache, not as the only check.
- Add a
codev.consult.gemini.enabledsetting and let the user toggle the lane off. Manual workaround, not a fix. Useful as a separate escape hatch but does not address the burst for users who want the gemini lane working. - Wait for upstream agy to add a non-interactive auth probe. Tracked at the agy CLI repo, no ETA. Codev's pre-flight cache is the right defense in depth regardless.
Out of scope
- The upstream agy behavior of opening a browser tab before printing the OAuth URL. That is the root cause of "one spawn opens one tab," but Codev cannot fix it directly. The cache reduces N-spawn-bursts to at most 1 spawn per TTL window, which is the achievable mitigation.
- Migrating away from agy to a different gemini backend. Out of scope; the gemini lane swap to agy was the deliberate decision recorded in Gemini CLI > Antigravity CLI - June 18, 2026 DEADLINE #778 / PR [Spec 778] Swap the gemini consult lane to the Antigravity CLI (agy) #988.
- Changes to the COMMENT-skip non-blocking semantics in porch's CMAP-3 verdict aggregation. Today's behavior is correct: skip + non-blocking advance. This issue only changes how soon the skip fires.
- Other agy hardening (sandbox isolation, off-topic reviews, malformed verdicts). Tracked under Consult: Gemini (agy/Antigravity) reviewer runs in empty sandbox + can execute real commands in the worktree (ran porch done, wrote files) #1051, agy/Gemini consult lane goes off-task on structured reviews — emits no VERDICT, breaking the porch consult loop #1032, consult: harden agy integration — Gemini lane silently drops out (upstream agy non-TTY stdout bug #76) #1020.
Why BUGFIX (or AIR)
The cache + pre-flight is well-bounded: one new module, two integration points (runAgyConsultation and the optional doctor.verifyAgy), small set of acceptance criteria. The behavior change is invisible to the user when agy is authenticated and dramatic when it is not. Could ship as BUGFIX (it fixes the N-spawn bug) or AIR (small additive feature). Lean: BUGFIX. PIR's plan-approval gate is overhead for this scope.
Related
- consult: harden agy integration — Gemini lane silently drops out (upstream agy non-TTY stdout bug #76) #1020, harden agy integration (sibling: silent drop-out when agy emits empty stdout). This issue addresses a different agy failure mode (unauth burst) but shares the broader "agy integration robustness" theme. Could fold as a sub-task; the recommendation is to keep them separate so they ship independently.
- Consult: Gemini (agy/Antigravity) reviewer runs in empty sandbox + can execute real commands in the worktree (ran porch done, wrote files) #1051, agy runs in empty sandbox + can execute commands.
- agy/Gemini consult lane goes off-task on structured reviews — emits no VERDICT, breaking the porch consult loop #1032, agy goes off-task on structured reviews (emits no VERDICT).
- Gemini CLI > Antigravity CLI - June 18, 2026 DEADLINE #778 / PR [Spec 778] Swap the gemini consult lane to the Antigravity CLI (agy) #988, the original gemini-to-agy lane swap. Background for why agy is the backend.
packages/codev/src/commands/consult/index.ts:614-862, the agy runner + skip detection.packages/codev/src/commands/doctor.ts:380-425, the doctorverifyAgyprobe (precedent for the spawn-with-timeout pattern).packages/codev/src/commands/porch/next.ts:529-544, the CMAP-3 dispatch and the partial-review re-emit logic that amplifies the burst.
Reference: tab-burst screenshot
The architect captured a screenshot of the stranded OAuth tab strip in Chrome on 2026-06-19; it is preserved at tmp/screenshots/agy-oauth-tab-burst-2026-06-19.png in the architect worktree (gitignored, not pushed). About 12-15 "Sign in with Google" tabs are visible at once, all pointing at accounts.google.com/v3/signin/accountchooser?access_type=offline&client_id=10710060... for Google Antigravity. The architect will attach the image to this issue via the web UI as a visual reference.

Activity
amrmelsayed commentedon Jul 16, 2026
Data point 2026-07-16: ~30 stranded tabs after a full-cycle release session
Observed a larger burst than the 12-15 documented in the issue body: ~30 stranded
google.com/v3/signin/accountchooser?...client_id=...&continue=...antigravity...tabs in a single Chrome window, all sitting on the "Choose an account to continue to Google Antigravity" step (no account picked yet, no OAuth completed).Context: this accumulated across a full v3.2.3 Karnak release-cycle session that shipped 14 changelog items via ~10 builder spawns (7 BUGFIX + 3 PIR). Each builder ran CMAP-3 at its PR gate, so each unauthenticated gemini lane spawned an agy child that opened its own tab. Consistent with the amplifier math in the issue body — same mechanism, larger denominator.
Also observed the "porch re-emit → additional tab" case at least twice this session, where a gemini COMMENT-skip stub caused porch to re-emit the CMAP tasks, producing a second (or third) agy spawn per builder. #1166 (skip-stubs counted as approving COMMENT) partially masks this because the re-emit doesn't always fire, but when porch judges the review "partial" it does re-emit and each re-spawn opens another tab.
Reinforces the case for the auth-state cache pre-flight proposed in this issue's body — the fix's primary win is stopping the second-through-Nth tab in the burst; the first probe is irreducible. In the observed session that would have collapsed ~30 tabs to 1.
mohidmakhdoomi commentedon Jul 18, 2026
@amrmelsayed one of my builders came across this issue of a burst of google OAuth browser tabs while running
pnpm --filter @cluesmith/codev test:e2e:cli.waleedkadous commentedon Jul 25, 2026
On it! Working on a fix now.
[Bugfix #1077] Fix: pre-flight agy auth cache to stop the OAuth tab b…
[Bugfix #1077] Test: regression coverage for the agy spawn burst
[Bugfix #1077] Docs: document the agy auth-state cache
[Bugfix #1077] Thread: builder narrative log
[Bugfix #1077] Fix: let positive OAuth evidence overwrite the auth guess
[Bugfix #1077] Thread: record the architect's grace-timer finding
Merge pull request #1250 from cluesmith/builder/bugfix-1077