Skip to content

fix(antigravity): re-discover the effort matrix when a turn names an unknown model - #370

Open
r-uben wants to merge 4 commits into
pleaseai:mainfrom
r-uben:fix/366-antigravity-matrix-refresh
Open

fix(antigravity): re-discover the effort matrix when a turn names an unknown model#370
r-uben wants to merge 4 commits into
pleaseai:mainfrom
r-uben:fix/366-antigravity-matrix-refresh

Conversation

@r-uben

@r-uben r-uben commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses #366 on the subprocess transport — deliberately not Fixes, since the
maintainer is keeping #366 open as the cross-transport tracking issue. effort_matrix returned the process-global cache unconditionally whenever it
was Some, so the one-shot agy models discovery was frozen for the lifetime of the
gateway: a model that appeared upstream after boot resolved to "no --effort flag" and
kept failing until restart. Reproduced with gemini-3.7-flash, which — unlike 3.6 and 3.5
flash — requires an effort value, so every turn 502'd on a gateway that had discovered
its matrix before 3.7 shipped.

The fix keeps the cache as the fast path and adds exactly one trigger: when a turn names a
model the cached matrix does not contain, spawn a background re-discovery. The turn itself
is not blocked — it proceeds on the current matrix, and the next one sees the refreshed
catalogue. A 60s floor (UNKNOWN_MODEL_REFRESH_INTERVAL) keeps a route naming an id agy
genuinely does not have from spawning one subprocess per turn forever; DISCOVERING
already collapses a concurrent burst, but not a steady stream.

The throttle stamp is a u64 of milliseconds since a OnceLock<Instant> epoch, compared
with saturating_sub, so a non-monotonic reading denies the refresh rather than wrapping
into a permanent one.

Scope note re: #368

This is the subprocess transport, which #368 deprecates. Filing it anyway on the maintainer's
stated rule that deprecation is not removal — this is a live failure on what ships today.
The agy-specific mechanism does not carry forward, but per the #366 comment the requirement
does: whatever supplies per-account capability hints on the native HTTP path (POST /v1internal:fetchAvailableModels) needs invalidation designed in, or it reproduces this
exact bug. Nothing here should be ported verbatim.

Milestone / spec

None — antigravity has no frozen milestone spec in docs/ (the provider was added in
#233/#234 and hardened in #325 without one). Tracking issue: #366; superseded-by
relationship recorded on #368.

Checklist

  • cargo build passes
  • cargo test passes (new behavior is covered; tests run without network/loopback where possible)
  • cargo clippy --all-targets -- -D warnings clean
  • cargo fmt --all --check clean
  • Source files stay under 500 lines
  • English only; matches surrounding style
  • Frozen spec in docs/ updated if this change deviates from it — n/a, no antigravity spec exists
  • User-facing docs updated — site/src/content/docs/guides/providers.mdx
  • Any new GitHub Action is pinned to a full commit SHA — none added

Test plan

cargo test --all-features --workspace: 1362 passed, 0 failed in the lib suite, plus 17
integration suites all 0 failed. cargo fmt --all --check clean, cargo clippy --all-targets --all-features -- -D warnings clean.

Five new unit tests cover the throttle in isolation (no subprocess, no clock dependency):

  • a_never_refreshed_matrix_is_immediately_due — the zero sentinel
  • a_second_unknown_model_within_the_interval_is_not_due
  • the_interval_boundary_is_due>=, not >
  • a_backwards_clock_denies_rather_than_wrapping
  • the_first_claim_wins_and_the_next_is_throttled — the compare-and-swap race

Pre-existing flake, unrelated to this diff (which touches no code it exercises):
tests/antigravity_process.rs::streaming_turn_translates_stub_events_to_sse hits the 20s
REQUEST_GUARD timeout under parallel load. Isolated: 1 failure in 3 runs (20.01s vs 0.15s
on the passes).

Notes for reviewers

  • The throttle is the load-bearing part, not the refresh. Without the 60s floor a
    misconfigured route becomes an unbounded subprocess spawner. Worth checking the
    compare-exchange in claim_unknown_model_refresh — a lost race must mean "someone else
    is refreshing", never "refresh twice".
  • Docs: only the English guides/providers.mdx changed. The ja/, ko/, and zh-cn/
    copies have no antigravity section at all (0 mentions), so there is nothing to translate.
    Flagging explicitly given feat(xai): add grok-4.6 and refresh the Grok model surface #343.
  • Discovery cost measured as a gateway subprocess on this machine: 1346–1840 ms. The
    ~20s figure in the models.rs rustdoc did not reproduce, though one shell run did
    hang >120s on Fetching available models..., so the fast numbers are likely warm-cache.

Summary by cubic

Re-discovers the antigravity effort matrix when a turn names an unknown model so models added after startup resolve on the next turn. Previously the matrix never invalidated; new models omitted --effort and failed until restart.

  • On a cached-matrix miss for the turn’s model, schedule background agy models; do not block; the next turn uses the updated matrix.
  • Rate-limit unknown-model refreshes to once per 60s (UNKNOWN_MODEL_REFRESH_INTERVAL) via compare-and-exchange; a non‑monotonic clock denies rather than wraps.
  • Add a 10s cold-cache backoff after a failed discovery (FAILED_DISCOVERY_BACKOFF) to avoid re-forking per turn while the matrix is empty; boot-time races do not consume the slot.
  • Change effort_matrix to effort_matrix(&Path, &str) and pass route.upstream_model at the antigravity adapter call site; update internal callers accordingly.
  • Update docs to describe runtime discovery, the once-per-minute unknown-model refresh, and the 10s cold-cache backoff.

Written for commit 7ee7df7. Summary will update on new commits.

…unknown model

The matrix was discovered once per process and never invalidated, so a model
`agy` gained after boot stayed unknown for the lifetime of the gateway. Every
turn on it hit the non-authoritative arm of resolve_effort and omitted
--effort, which ids that require one reject outright — an outage lasting until
restart, not the documented single first turn.

A miss against a warm cache now schedules a background discovery, rate-limited
to one subprocess a minute so an id that genuinely does not exist cannot spawn
one per turn. The refresh stays off the request path, matching the rest of the
module: the turn that first names the model still passes through unvalidated,
so the model becomes unknown once rather than forever.

The rate-limit policy is split into a pure `refresh_is_due` so it is testable
without a process-wide clock, including the backwards-clock case where a
wrapping subtraction would have allowed every turn through.

Closes pleaseai#366
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.33333% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/adapters/antigravity/models.rs 97.29% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces rate-limited background discovery for unrecognized models in the Antigravity adapter. When a request queries a model not present in the cached effort matrix, a background refresh is scheduled, restricted to at most once every 60 seconds to prevent excessive subprocess spawning. The changes include documentation updates explaining this behavior, implementation of the rate-limiting logic using atomic variables, and comprehensive unit tests. I have no feedback to provide as the implementation is robust and well-tested.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files

Architecture diagram
sequenceDiagram
    participant Client
    participant GW as Gateway (Antigravity Adapter)
    participant M as models module
    participant Cache as Process-global matrix cache
    participant Sub as agy CLI subprocess
    
    Note over Client,Sub: Model Effort Discovery and Resolution Flow
    
    Client->>GW: Request with model
    GW->>M: effort_matrix(agy_bin, model)
    M->>Cache: cached() lookup
    alt Cache miss (empty matrix)
        Cache-->>M: None
        M->>Sub: spawn agy models (background)
        Sub-->>Cache: Update matrix
        M-->>GW: Empty matrix (pass-through)
    else Cache hit
        Cache-->>M: Matrix
        alt Model not in matrix
            M->>M: claim_unknown_model_refresh()
            alt Refresh slot claimed
                M->>Sub: spawn agy models (background)
                Sub-->>Cache: Update matrix
            else Throttled (within 60s window)
                Note over M: No refresh spawned
            end
            M-->>GW: Stale matrix (this turn passes through)
        else Model in matrix
            M-->>GW: Matrix with model
        end
    end
    
    GW->>M: resolve_effort(matrix, model)
    alt Model found in matrix
        M-->>GW: Effort level
    else Model not found
        M-->>GW: EffortChoice::Unspecified
    end
    GW->>Sub: Execute turn with --effort flag (if specified)
    Sub-->>GW: Response
    GW-->>Client: Turn result
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/adapters/antigravity/models.rs
@codspeed-hq

codspeed-hq Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 83 untouched benchmarks


Comparing r-uben:fix/366-antigravity-matrix-refresh (7ee7df7) with main (e59d844)

Open in CodSpeed

Follow-up to the cubic P2 on this PR. The finding was real: while the cache is
empty, `refresh` clears `DISCOVERING` on failure so the next turn may spawn
`agy models` again, and nothing throttled that path — a persistently failing
binary could be re-forked once per turn.

The suggested fix was to share `LAST_UNKNOWN_MODEL_REFRESH` between the warm
and cold paths. That is declined and this is the alternative offered in the
review thread, because sharing the claim burns it at boot: a turn arriving
while `warm`'s discovery is in flight would take the slot, then no-op inside
`spawn_refresh` because `DISCOVERING` is already held. A failing boot discovery
would leave the process cold *and* throttled for a full interval on a claim
that never ran a discovery — and boot is exactly when `warm` and the first
turns race.

Stamp the failure instead of the attempt. `LAST_FAILED_DISCOVERY` is written
only on the `None` branch of `refresh`, and the cold-cache gate is a plain read
that takes no claim, so an attempt that never fails never throttles the turn
after it.

The backoff is 10s, not the unknown-model minute. A cold matrix is not a steady
state: every effort-taking model requires the flag, so routes pinning no effort
hard-fail until discovery lands, and a minute of that would turn a blip into an
outage. A few multiples of the measured ~1.3-1.9s discovery cost bounds the
forking without meaningfully extending the cold window.

No reset on success: `MATRIX` is only ever assigned, never cleared, so a
success makes the cold path unreachable and the stamp unreadable thereafter.

The three global-state assertions live in one test rather than three, since
`LAST_FAILED_DISCOVERY` is process-wide and the harness runs tests in parallel.

Refs pleaseai#366
@r-uben

r-uben commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Implemented the alternative I flagged above, so this no longer waits on a decision from you.
Pushed 1f1258c.

LAST_FAILED_DISCOVERY is stamped inside refresh on the None branch only, and the
cold-cache gate is a plain read that takes no claim. That is the property that matters: an
attempt which never fails never throttles the turn after it, so a turn arriving while
warm's boot discovery is in flight can consult the gate without burning it — the
interleaving that made sharing LAST_UNKNOWN_MODEL_REFRESH the wrong shape.

The backoff is 10s rather than the unknown-model minute. A cold matrix is not a steady
state — every effort-taking model requires the flag, so routes pinning no effort hard-fail
until discovery lands — and a minute of that turns a blip into an outage. A few multiples of
the measured ~1.3-1.9s discovery cost bounds the forking without meaningfully extending the
cold window. the_cold_backoff_is_shorter_than_the_unknown_model_interval pins that
relationship so a later edit to either constant cannot quietly invert it.

No reset on success, deliberately: MATRIX is only ever assigned and never cleared, so a
success makes the cold path unreachable for the life of the process and the stamp can never
be read again. Noted in a comment at the assignment rather than left for a reader to derive.

The three global-state assertions are one test, not three. LAST_FAILED_DISCOVERY is
process-wide and the harness runs tests in parallel, so split tests would race each other's
stamp — the sequence is open, still-open-when-consulted, then closed by an observed failure.

Also updated the one sentence in the provider guide that described this exact behavior
("a failed discovery is retried rather than cached"), which was accurate before and would
have been misleading after.

Local gate: fmt and clippy clean, full workspace suite passes, 0 failures.

Happy to drop the commit if you would rather keep this PR to the #366 fix and take the
backoff separately — it is self-contained and reverts cleanly.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f1258c693

ℹ️ 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".

Comment on lines +139 to +140
if !matrix.contains_key(model) && claim_unknown_model_refresh() {
spawn_refresh(agy_bin);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Claim the refresh slot only after scheduling succeeds

When the first request for a newly released model races with completion of startup discovery, it can read the just-published stale matrix while DISCOVERING is still true. This condition then claims the 60-second throttle slot, but spawn_refresh immediately returns without spawning because discovery is still marked in flight; subsequent requests are throttled, so the new model can continue failing for a minute instead of being available on the next turn. Make claiming and scheduling atomic, or stamp the throttle only after a refresh subprocess is actually scheduled.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR refreshes Antigravity’s cached model/effort catalogue after an unknown-model lookup while keeping discovery off the request path.

  • Adds a one-minute throttle for unknown-model refreshes.
  • Adds a ten-second retry backoff after cold-cache discovery failures.
  • Passes the routed upstream model into effort-matrix lookup and documents the behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/adapters/antigravity/models.rs Adds throttled background catalogue refresh and failed-discovery backoff.
src/adapters/antigravity/mod.rs Supplies the routed upstream model to the effort-matrix lookup.
site/src/content/docs/guides/providers.mdx Documents runtime catalogue refresh and cold-cache retry behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Antigravity turn] --> B{Effort matrix cached?}
  B -- No --> C{Cold discovery due?}
  C -- Yes --> D[Schedule background agy models]
  C -- No --> E[Use empty matrix]
  B -- Yes --> F{Model present?}
  F -- Yes --> G[Resolve effort from cached matrix]
  F -- No --> H{Unknown-model refresh due?}
  H -- Yes --> D
  H -- No --> I[Use stale matrix]
  D --> J[Current turn continues without waiting]
  E --> J
  I --> J
Loading

Reviews (3): Last reviewed commit: "Merge branch 'main' into fix/366-antigra..." | Re-trigger Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/adapters/antigravity/models.rs">

<violation number="1" location="src/adapters/antigravity/models.rs:134">
P2: With concurrent cold requests, one can read the gate as due just before another refresh records failure, then spawn after `DISCOVERING` is cleared. Claim the discovery flag before rechecking the failure stamp, and release it without spawning when the backoff is closed.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

/// model becomes unknown *once* rather than forever.
pub async fn effort_matrix(agy_bin: &Path, model: &str) -> EffortMatrix {
let Some(matrix) = cached() else {
if cold_discovery_is_due() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: With concurrent cold requests, one can read the gate as due just before another refresh records failure, then spawn after DISCOVERING is cleared. Claim the discovery flag before rechecking the failure stamp, and release it without spawning when the backoff is closed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/adapters/antigravity/models.rs, line 134:

<comment>With concurrent cold requests, one can read the gate as due just before another refresh records failure, then spawn after `DISCOVERING` is cleared. Claim the discovery flag before rechecking the failure stamp, and release it without spawning when the backoff is closed.</comment>

<file context>
@@ -103,7 +131,9 @@ static LAST_UNKNOWN_MODEL_REFRESH: AtomicU64 = AtomicU64::new(0);
 pub async fn effort_matrix(agy_bin: &Path, model: &str) -> EffortMatrix {
     let Some(matrix) = cached() else {
-        spawn_refresh(agy_bin);
+        if cold_discovery_is_due() {
+            spawn_refresh(agy_bin);
+        }
</file context>

Two assertions in the new tests were written wider than rustfmt's target and
CI's format check rejected them. No behavior change.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e19dd0dfc3

ℹ️ 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".

Comment on lines +136 to +137
discovery in the background, at most once a minute. That request still passes through to
`agy` unvalidated; the one after it resolves normally. Without this, a model released

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the next-turn success guarantee

When a second request arrives before the background agy models subprocess completes, effort_matrix still returns the stale cached matrix immediately, so that request can also pass through unvalidated and fail. The documentation should promise normal resolution only for requests made after discovery has finished, rather than guaranteeing that the very next turn succeeds.

AGENTS.md reference: AGENTS.md:L44-L46

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ee7df7d0d

ℹ️ 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".

Comment on lines +152 to +154
and `agy` validates them itself — a failed discovery is retried rather than cached, after
a short backoff so a persistently failing `agy` is not re-forked once per turn while the
matrix is still empty.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the localized provider guides too

Because this English provider-guide change documents new user-visible Antigravity refresh/backoff behavior, the maintained locale copies of the same page need the same update. I checked site/src/content/docs/{ja,ko,zh-cn}/guides/providers.mdx; those pages still have no Antigravity section, so non-English docs now omit this behavior even though the repo requires localized site copies to stay current with English.

AGENTS.md reference: AGENTS.md:L57-L64

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants