Skip to content

Honor Muse Mint Error Envelopes and Back Off on 429 - #126

Merged
mstallone merged 3 commits into
mainfrom
fix/muse-mint-backoff
Sep 5, 2026
Merged

Honor Muse Mint Error Envelopes and Back Off on 429#126
mstallone merged 3 commits into
mainfrom
fix/muse-mint-backoff

Conversation

@mstallone

Copy link
Copy Markdown
Owner

TL;DR

Treat Muse's /muse-code/key call as a key-mint, honor HTTP 200 error envelopes, and back off 15 minutes on 429 so Runway stops breaking Muse Code's own credential.refresh.

What was happening

  • Runway polled POST https://api.meta.ai/muse-code/key every refresh. That endpoint mints a Model API key; subscription meters are a side effect, not a poll-safe usage API.
  • Meta's gateway often returns HTTP 200 with {title, detail, status} (401/429-style) instead of mint JSON. The mapper treated that as a malformed usage document (Usage response invalid).
  • Hammering mint then produced real HTTP 429s, which also blocked Muse Code's credential.refresh. CLI chat could still work from a cached key while mint itself failed.

What this changes

  • Honor the gateway envelope's status when HTTP is 2xx and the body has no is_subs_active / subs_usage.
  • Unwrap { "data": { … } } mint payloads.
  • On 429, keep last-good meters with a wait warning, skip the network for 15 minutes (or Retry-After, capped at 1 hour), and log that extra refreshes make the limit worse.
  • Retry mint once if 401/403 and Muse Code has rotated the Keychain access token. Still no OAuth refresh and no Keychain write-back.
  • Send x-client-id: tbh:tui, the same surface header Muse Code uses for this call.

Heads-up

  • Targets mstallone/runway, not upstream.
  • First fetch after a 429 still errors until a last-good snapshot exists or the cooldown lifts.
  • Session expiry still needs muse login. Runway does not write the Muse Keychain item.

Tests

  • swift test --filter Muse

POST /muse-code/key mints an API key; polling it every refresh treated
HTTP 200 {title,detail,status} envelopes as malformed usage, then hit
real 429s that also blocked Muse Code's own credential.refresh.
@mstallone
mstallone requested a lite review from Copilot September 5, 2026 18:09
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

Copilot AI 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.

🟡 Changes recommended

The cooldown path currently skips credential re-evaluation and the Retry-After parsing is incomplete, both of which can produce incorrect or premature behavior during rate-limit windows.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates the Muse provider to treat /muse-code/key as a key-mint endpoint (not a poll-safe usage API), correctly interpret Meta gateway “error envelopes” even when delivered over HTTP 200, and apply a rate-limit cooldown so Runway stops amplifying 429s and interfering with Muse Code’s own credential mint/refresh behavior.

Changes:

  • Muse: detect and honor {title, detail, status} gateway envelopes (including when HTTP is 2xx) and unwrap { "data": { … } } payloads.
  • Muse: add x-client-id: tbh:tui, retry once on locally-rotated access tokens after 401/403, and implement 429 cooldown serving last-good meters with a “wait” warning.
  • Tests/docs: expand Muse tests around envelope handling, wrapped payloads, retry behavior, and cooldown; update Muse docs + privacy wording to match the key-mint semantics.
File summaries
File Description
Tests/RunwayTests/MuseProviderTests.swift Adds coverage for gateway envelopes, wrapped payloads, token-rotation retry, and 429 cooldown behavior.
Sources/Runway/Providers/Muse/MuseUsageMapper.swift Adds envelope status detection, payload unwrapping, and more specific logging for missing fields.
Sources/Runway/Providers/Muse/MuseUsageClient.swift Adds the Muse surface header and clarifies that the endpoint is mint-driven and rate-limited.
Sources/Runway/Providers/Muse/MuseProvider.swift Implements cooldown + last-good serving, envelope-effective status handling, and one-time token rotation retry.
docs/providers/muse.md Documents mint semantics, surface header, envelope handling, and cooldown behavior.
docs/privacy.md Aligns privacy wording to “key-mint endpoint” and clarifies the minted key is not persisted.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Sources/Runway/Providers/Muse/MuseProvider.swift
Comment thread Sources/Runway/Providers/Muse/MuseProvider.swift
Last-good meters during a 429 window should not outlive a logout or
unusable Keychain item.

Copilot AI 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.

🟡 Changes recommended

The 429 cooldown cache is not keyed to the current Muse token, so an account/token change during cooldown can serve last-good meters from the previous login.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Sources/Runway/Providers/Muse/MuseProvider.swift:104

  • In the 401/403 path, the retry reload is only used when it returns a new .token. If the Keychain/token disappears or becomes unreadable between the initial load and the retry (e.g., user logs out while a refresh is in-flight), this currently falls through to sessionExpired, which is a different (and misleading) state than .none/.invalid/.unreadable.

Handle the non-.token reload outcomes explicitly so the UI reports the correct auth state and clears lastGood/cooldown when credentials are gone.

            if status == 401 || status == 403 {
                if allowRetry {
                    let reloaded = await loadOffMainActor { [authStore] in
                        authStore.loadCredentials(allowKeychainInteraction: allowInteraction)
                    }
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread Sources/Runway/Providers/Muse/MuseProvider.swift
A 429 cooldown should not keep showing the previous login after muse
login rotates the Keychain item.

Copilot AI 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.

🔵 Needs a closer look

The cooldown path in MuseProvider.refresh() currently calls now() multiple times, which can cause boundary-time inconsistencies (including skipping one extra refresh after cooldown) and should be made time-consistent.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

Sources/Runway/Providers/Muse/MuseProvider.swift:77

  • refresh() calls now() twice in the cooldown path (once for the < until check and again for the remaining-seconds calculation). With a real Date() clock, time can advance between those calls and (at the boundary) produce a negative/zero remaining time and skip one extra refresh even after the cooldown has elapsed. Capture the current time once and reuse it for both comparisons/calculations.
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@mstallone

Copy link
Copy Markdown
Owner Author

Stopping the Copilot loop here. The remaining now() double-call note is a boundary-clock nit (microseconds on a 15-minute cooldown) and does not change the mint backoff. Integer Retry-After plus the 15-minute default is enough for Meta’s mint envelopes.

Local Bugbot is clean on the token-keyed last-good path.

@mstallone
mstallone merged commit 9a52d21 into main Sep 5, 2026
3 checks passed
@mstallone
mstallone deleted the fix/muse-mint-backoff branch September 5, 2026 18:31
mstallone added a commit that referenced this pull request Sep 5, 2026
## TL;DR

Muse now shows local token history and estimated Spark API-rate spend
from session logs, while Five-Hour Usage and Weekly Usage stay always
visible.

## What was happening

- Muse only had subscription meters. There was no token tracking, usage
trend, or priced spend, so Spark usage could not be valued at Meta's
published rates.
- Adding spend tiles the usual way would seed them into On Demand and
risk hiding Weekly again for existing layouts.

## What this changes

- Reads `model_completed` events from Muse Code journals
(`~/.local/share/muse/sessions/`, including nested subagent logs).
- Prices Standard Spark at $1.25 / $0.15 cached / $4.25 output per
million tokens, and Contributor SKUs at $0.10 / $0.002 / $0.20. Cache
writes bill at the input rate. Reasoning is not added on top of output.
- Usage Trend and Today / Yesterday / Last 30 Days start on demand.
Five-Hour and Weekly stay above the fold and starred.
- Spend still loads when mint cannot (Connect, expired session,
logs-only, or 429 with no last-good meters). The mint backoff from #126
is unchanged.
- Session logs alone can auto-enable the provider.

## Heads-up

- Targets `mstallone/runway`, not upstream.
- These dollars are estimated API-rate value, not billed subscription
spend.
- Contributor SKUs must not resolve to Standard rates; alias rules put
contributor first.

## Tests

- `swift test --filter Muse`
- `swift test --filter 'ModelPricing|PricingBundledResource'`
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