[Bugfix #1077] Pre-flight agy auth cache to stop the OAuth tab burst#1250
Conversation
…urst An unauthenticated `agy` opens an OAuth browser tab before it prints the URL that AGY_OAUTH_MARKERS detection keys off, so the gemini lane's kill closes the barn door after the horse has left. A CMAP round is N independent consult processes, so N tabs stranded (12-15 observed in the wild). Add a filesystem-backed, cross-process auth-state cache consulted BEFORE the spawn. A file lock elects one prober per cold window; it publishes the verdict as soon as it is knowable and the rest short-circuit without spawning, so an unauthenticated agy is spawned at most once per TTL window instead of once per consult. The real consult run doubles as the probe rather than adding a dedicated one, so the authenticated path costs no extra agy spawn and marker detection stays in one place. Only positive marker evidence records 'unauth', and an undecided wait proceeds anyway — failures land on "one unnecessary tab", never on "silently dropped a working reviewer". doctor's verifyAgy shares the cache in both directions: it reports a fresh verdict without probing, and records the verdicts its own probe produces.
The headline test drives the real lane (_runAgyConsultation) against a fake agy that logs every invocation, so 'did we spawn?' is asserted against observed process starts rather than mocks: a 5-wide unauthenticated burst must produce exactly one spawn. A companion case asserts 3 spawns with the cache disabled, which is what proves the burst test measures the cache and not something else. Also covers TTL expiry per state, bin-path and credential-mtime invalidation, corrupt/unknown entries, lock election, stale-lock reclaim, sign-in recovery after the unauth TTL, and that an authenticated burst still runs every lane. consult/doctor suites pin CODEV_AGY_AUTH_CACHE_DIR: the cache is a user-global side effect, and an unpinned run both wrote the developer's ~/.cache/codev and let one case's verdict suppress the spawn a sibling asserts on.
consult.md gets the mechanism, the recovery story (sign in elsewhere and the lane comes back on its own), and the TTL / opt-out env vars. CLAUDE.md and AGENTS.md get the one-paragraph version in the gemini lane bullet. Mirrored into codev-skeleton/ so adopters get it too; CLAUDE.md and AGENTS.md kept byte-identical.
Picks up the doctor formatBytes hotfix (PR #1248) that was blocking the build.
CMAP-3 review
Unanimous, no requested changes, so nothing to rebut or fix. What each reviewer independently called out:
|
Architect Integration ReviewExcellent work — this is a genuinely well-engineered fix. The single-prober lock election with cache-polling waiters, the fail-open bias, and the spawn-count-observed (not mocked) burst regression test are all exactly right. My two integration lanes agree with your CMAP-3: codex APPROVE (called the module boundary "the right system boundary for a cross-process concern") and claude APPROVE, high confidence (independently verified the two-tree mirroring and CLAUDE/AGENTS byte-identity). On your two flagged judgment calls:
One change I'd like before merge, because it protects the headline guarantee: Let positive OAuth-marker evidence write the cache unconditionally. Today the marker path reports Perhaps add one test alongside: grace timer fires Everything else is merge-ready. |
The lane guesses `auth` after a marker-free grace period so waiters are not stuck behind a review that runs for minutes. That guess went through the once-only publish, so if agy merely took longer than the grace period to print its banner, the guess stood and the real `unauth` evidence a moment later was a no-op — caching `auth` for the full 30-minute TTL and re-opening a tab on every consult in that window. The fix silently reverted to the status quo on exactly the slow machines that need it most. Record `unauth` directly from the marker handler so positive evidence wins regardless of arrival order. This also fixes a second case: a caller that failed open (another process held the lock) still spawns and can be the one to see the banner, but published nothing because publish is inert for non-probers. Its evidence now corrects a misfired guess on the very next spawn instead of waiting out the TTL. Both new tests were confirmed to fail against the previous code. Raised by the architect in integration review of PR #1250.
Requested change pushed — 42cf18eGood catch, and you were right that this protected the headline guarantee. Marker detection now calls Worth noting it was two bugs, not one — the second I had missed as well:
This was a real inversion of the fail-safe bias I claimed in the PR body: I wrote that "only positive marker evidence records TestsTwo added, and I verified both fail against the previous code before restoring the fix — not just pass with it:
Full suite 3722 passed, 0 failed (up 2). Typecheck clean. Cross-process burst smoke re-run after the change: 5 parallel unauthenticated consults → still 1 spawn. No changes to the docs or the public surface. Gate is still yours. |
Summary
The gemini consult lane spawned
agybefore it could knowagywas signed out, and every spawn opens an OAuth browser tab. A CMAP round is N independentconsultprocesses, so N tabs stranded — 12-15 in the reported case.This adds a cross-process auth-state cache consulted before the spawn. Unauthenticated
agyis now spawned at most once per TTL window instead of once per consult.Measured with a fake
agythat logs every invocation (spawn counts are observed, not mocked):consult -m gemini, unauthenticatedRoot Cause
runAgyConsultation(packages/codev/src/commands/consult/index.ts) spawnedagyunconditionally, andAGY_OAUTH_MARKERSdetection lives in the child's stdout/stderr handler — i.e. strictly after the spawn. Upstreamagyopens the OAuth tab before printing the URL those markers match, so the kill can never prevent the tab. The skip semantics were already correct; only the timing was wrong.A module-level memo cannot help: each
consultinvocation is a separate OS process, so the state has to be shared through the filesystem.Amplifiers confirmed in code: CMAP-N dispatches one process per model (
porch/next.ts:529-531), builders run 1-3 consult phases each, and the partial-review re-emit (porch/next.ts:544) re-issues the same consult commands.Fix
New module
packages/codev/src/commands/consult/agy-auth-cache.ts:~/.cache/codev/agy-auth.json(0600 in a 0700 dir, honoursXDG_CACHE_HOME), keyed to the resolved agy binary, written via temp-file + rename so a concurrent reader never sees half a document.unauth(the sign-in recovery window), 30 minauth. Both env-overridable.flock-style lock (openSync'wx', with stale-lock reclaim) elects one prober per cold window. Losers poll the cache, not the lock — the prober holds it for its whole run, which may be minutes, but publishes the verdict within seconds.Two integration points:
runAgyConsultationpre-flights beforespawn, anddoctor.verifyAgyboth reports a fresh verdict without probing and records the verdicts its own probe produces.Deliberate deviation from the issue's proposal
The issue proposes a dedicated
--print-timeout 5sprobe on a cold cache. Implemented instead: the real consult run doubles as the probe, publishing the verdict as soon as it is knowable (OAuth marker →unauth; real output, or 3s of marker-free silence →auth). This avoids a second agy spawn per TTL window in the authenticated common case, and keeps marker detection in one place rather than duplicating it in a path that could classify differently. The observable guarantee the ACs ask for is unchanged.Failure bias
Only positive marker evidence records
unauth, and a waiter that never sees a verdict proceeds anyway. Both choices push failures toward "one unnecessary browser tab" (the status quo) and away from "silently skipped a working reviewer", which would corrupt a CMAP round.Test Plan
packages/codev/src/commands/consult/__tests__/agy-auth-cache.test.ts— 25 tests. The headline case drives the real lane (_runAgyConsultation) against a fakeagythat logs every invocation, so a 5-wide unauthenticated burst is asserted to produce exactly one spawn. A companion case asserts 3 spawns with the cache disabled, which is what proves the burst test measures the cache rather than something incidental.Also covered: per-state TTL expiry, bin-path and credential-mtime invalidation, corrupt/unknown entries, lock election, stale-lock reclaim, sign-in recovery after the unauth TTL, and that an authenticated burst still runs every lane.
npm test), build green. Both verified again by porch's ownfixphase checks.consultprocesses, not in-process): 1 spawn unauthenticated, 5 authenticated with all 5 reviews delivered.auth, delivers a review. No manual cache clear.Note on test isolation
The cache is a user-global side effect. Unpinned, the suite wrote a verdict into the developer's real
~/.cache/codev, and onedoctorcase's verdict made a sibling case stop spawning the thing it asserted on (one genuine failure, caught and fixed). The consult/doctor suites now pinCODEV_AGY_AUTH_CACHE_DIRinto their temp dirs, and the module is inert underVITESTunless a cache dir is explicitly set — the guard is what keeps future tests from silently re-acquiring the hazard. Verified: a full suite run no longer creates~/.cache/codev.Scope
Net diff is ~960 lines across 12 files, above BUGFIX's ~300 guideline. The composition: ~310 lines of new module (roughly half comments), 74 lines across the two integration points, ~370 lines of tests, ~58 of docs. Shape is what the guideline actually targets — one new module plus two call sites, no architectural change, no new abstractions imposed on callers. Flagging it rather than quietly ignoring it; happy to split the docs out if you'd prefer a smaller diff.
Notes
packages/codev/src/commands/doctor.tsalso carries theformatBytesalias from the hotfix in hotfix(doctor): resolve formatBytes duplicate-identifier collision from #1242 × #1243 #1248, which arrived via amainmerge — not a change of mine.consult.md(mechanism, recovery, env vars), mirrored intocodev-skeleton/; CLAUDE.md / AGENTS.md gemini-lane bullet, kept byte-identical.Fixes #1077