Honor Muse Mint Error Envelopes and Back Off on 429 - #126
Conversation
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.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
There was a problem hiding this comment.
🟡 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.
Last-good meters during a 429 window should not outlive a logout or unusable Keychain item.
There was a problem hiding this comment.
🟡 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 tosessionExpired, 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
A 429 cooldown should not keep showing the previous login after muse login rotates the Keychain item.
There was a problem hiding this comment.
🔵 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()callsnow()twice in the cooldown path (once for the< untilcheck and again for the remaining-seconds calculation). With a realDate()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
|
Stopping the Copilot loop here. The remaining Local Bugbot is clean on the token-keyed last-good path. |
## 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'`
TL;DR
Treat Muse's
/muse-code/keycall as a key-mint, honor HTTP 200 error envelopes, and back off 15 minutes on 429 so Runway stops breaking Muse Code's owncredential.refresh.What was happening
POST https://api.meta.ai/muse-code/keyevery refresh. That endpoint mints a Model API key; subscription meters are a side effect, not a poll-safe usage API.{title, detail, status}(401/429-style) instead of mint JSON. The mapper treated that as a malformed usage document (Usage response invalid).credential.refresh. CLI chat could still work from a cached key while mint itself failed.What this changes
statuswhen HTTP is 2xx and the body has nois_subs_active/subs_usage.{ "data": { … } }mint payloads.Retry-After, capped at 1 hour), and log that extra refreshes make the limit worse.x-client-id: tbh:tui, the same surface header Muse Code uses for this call.Heads-up
mstallone/runway, not upstream.muse login. Runway does not write the Muse Keychain item.Tests
swift test --filter Muse