Skip to content

fix(desktop): rebuild the Claude subscription model inventory from the registry - #2482

Merged
Astro-Han merged 1 commit into
apache:mainfrom
Joob1n:fix/claude-subscription-stale-model-inventory
Aug 9, 2026
Merged

fix(desktop): rebuild the Claude subscription model inventory from the registry#2482
Astro-Han merged 1 commit into
apache:mainfrom
Joob1n:fix/claude-subscription-stale-model-inventory

Conversation

@Joob1n

@Joob1nJoob1n commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Newly curated models showed up in the Claude OAuth connection sheet but could not be selected. The settings catalog reads the curated list, while sync rebuilt the selection against the inventory persisted on disk, so a newly picked model was filtered back out on the next refresh.

Sync now rebuilds from the same curated authority the catalog projects from, through connectionFallbackModelIds.

A stored id that was renamed rather than retired now migrates instead of being repaired away — without it, an existing claude-haiku-4-5-20251001 default was moved to claude-opus-5, across model family and price tier. The rename table is selected per provider by modelIdAliasesForProvider, so reconcileConnectionAfterModelFetch still treats ids as opaque for every other provider, including relays that serve claude-* ids of their own. Both the desktop sync and the Runtime Host catalog resolve it through that one lookup.

Retirement is unaffected: claude-opus-4-1-20250805 is still repaired against the live list, and a test asserts via lifecycle metadata that no deprecated model can enter the table.

Fixes nothing tracked — found while using the app.

Verification

  • @maka/core 833/833, @maka/storage 754/754, @maka/desktop 1612/1612, @maka/runtime-host 729/729
  • Four-project typecheck, lint and format:check clean
  • Reproduced the original defect and each regression against a real workspace record before and after

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — a stored selection whose id was renamed now follows the model instead of being replaced. Retired ids are still repaired.
  • No

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks — the diagnosis and overall direction look right. A persisted fallback snapshot should not shadow the current curated registry, and rebuilding it on sync is a small, appropriate fix for newly added models disappearing after selection.

I found one compatibility edge case that seems important to handle before merge:

P2 — Existing Haiku 4.5 users can be silently switched to Opus 5

The previous inventory included claude-haiku-4-5-20251001, while the new curated inventory contains claude-haiku-4-5. These are not different or retired model families: Anthropic documents the dated value as the Claude API model ID and the shorter value as its alias.

Because reconciliation currently compares IDs literally, an existing connection with:

defaultModel: claude-haiku-4-5-20251001
enabledModelIds: [claude-haiku-4-5-20251001]

is repaired to the first item in the new list, currently claude-opus-5. That changes the user's selected model family instead of migrating an equivalent identifier.

Please normalize the known dated ID to its current alias before reconciliation:

claude-haiku-4-5-20251001 → claude-haiku-4-5

A focused regression test should assert that an existing Haiku default remains Haiku after sync. The current “default belongs to the curated list” assertion is too broad and would allow this cross-family replacement.

P3 — The settings catalog and sync inventory still have separate authorities

Sync reads PROVIDER_DEFAULTS['claude-subscription'].fallbackModels, while the settings catalog reads CURATED_CATALOG_FALLBACK_MODELS['claude-subscription']. They happen to contain the same values today, but a future update to only one list would recreate the original “selectable, then removed on sync” bug.

It would be cleaner to project both paths from one curated authority and add a small alignment test.

P3 — The Runtime Host 401 note appears unrelated to this change

This PR does not modify the Runtime Host refresh path, and #2336 already marked the provider as fallback-only so model refresh should not make the network request. I suggest removing or narrowing that claim in the PR description.

The production change can remain small; it just needs explicit alias migration and a single inventory authority. Thanks!

Joob1n added a commit to Joob1n/maka-agent that referenced this pull request Aug 8, 2026
Review on apache#2482 caught a regression this PR introduced: an existing
`claude-haiku-4-5-20251001` selection was repaired to `claude-opus-5`.
The dated id and the curated `claude-haiku-4-5` name one model — Anthropic
publishes a pinned release id and a "latest" alias — but reconciliation compares
ids literally, so the stored form read as a model the catalog had dropped and
repair fell through to the first live id. That moved the user across model
family and price tier with no prompt. The curated list caused it by mixing
forms: it kept a pinned `claude-sonnet-4-5-20250929` while switching Haiku to
its alias, so only Haiku users were orphaned.
Resolve the id against the inventory before matching. Whether an id is
superseded is a property of the inventory, not of the id: the API-key Anthropic
catalog deliberately lists a pinned release beside its alias so users can pin,
while the subscription catalog lists aliases only. So the rewrite applies only
when the stored id is absent and its alias present — the exact case a literal
comparison misreads. Retirement stays untouched: `claude-opus-4-1-20250805` is
still repaired against the live list, and a test asserts via `lifecycle` metadata
that no deprecated model can be added to the alias table.
Also collapses the two inventory authorities the review flagged: sync read
`PROVIDER_DEFAULTS[...].fallbackModels` while the settings catalog read
`CURATED_CATALOG_FALLBACK_MODELS`, agreeing only while both were edited
together. Both now project from `connectionFallbackModelIds`, with a test
asserting the sync inventory equals what the catalog offers.
Tests tightened accordingly: the retired-default case asserted only that the new
default was in the curated list, which the Haiku regression satisfied.
@Joob1n

Copy link
Copy Markdown
ContributorAuthor

Thanks — all three land, and P2 was a real regression this PR introduced. Reproduced it against a live workspace record before fixing: an existing claude-haiku-4-5-20251001 default came out of sync as claude-opus-5.

P2 — fixed, and I'd argue it was worse than P2. The switch crosses price tiers (Haiku → Opus 5) silently and takes effect on the next message, so the cost lands before the user has any way to notice. Worth calling out because the root cause is broader than the one id: my curated list mixed forms — it kept a pinned claude-sonnet-4-5-20250929 while switching Haiku to its alias — so only Haiku users were orphaned, and any future edit swapping a pinned id for its alias would have reproduced it on a different family. The list is now one form throughout.

On the normalization itself, I made resolution inventory-relative rather than a global rewrite, because your P3-1 point turned out to constrain it: the API-key anthropic catalog deliberately offers claude-sonnet-4-5-20250929 alongside its alias so users can pin a release. A global map would have un-pinned them. So the rewrite applies only when the stored id is absent from the inventory and its alias is present — the exact case a literal comparison misreads. Verified across all four paths (subscription/pinned-Haiku, subscription/pinned-Sonnet, subscription/retired-Opus-4.1, API-key/pinned-Sonnet).

Retirement stays separate: claude-opus-4-1-20250805 is still repaired against the live list. To keep that boundary from eroding, a test asserts through lifecycle metadata that no deprecated model can be added to the alias table — the membership rule is machine-checked rather than a comment.

P3-1 — fixed. Both paths project from connectionFallbackModelIds now, with a test asserting the sync inventory equals what the catalog offers. Worth noting this landed first and immediately paid for itself: the alignment test is what surfaced the pinned-vs-alias conflict above, by failing when a global alias map contradicted the anthropic catalog.

P3-2 — correct, removed.#2336 marked the provider fallback-only, so that path stopped making the request there, not here. My misattribution.

You were also right that the old assertion was too broad — default belongs to the curated list is exactly what let the Haiku case through. Replaced with family-preserving assertions over both superseded ids, alongside the retirement case.

Verification: @maka/core 831/831; @maka/desktop main 1792/1812 (the 20 are pre-existing on this checkout — CSS contract tests and @maka/runtime./voice-service export resolution, unchanged by this PR and confirmed by stashing); full four-project typecheck and biome lint clean.

@Joob1n

Copy link
Copy Markdown
ContributorAuthor

Correction to my verification note above: I reported "20 pre-existing failures" in the desktop suite. That was wrong, and it was my own build hygiene rather than anything in the repo.

I had been running npm run build:main directly to iterate. That is incremental — tsc does not remove output for sources that no longer exist — so orphaned artifacts from an older tree (e.g. dist/main/voice-ipc-main.js, whose source is long gone) kept executing as phantom tests and failing on imports that no longer resolve. The test script cleans first, which is why it never showed up there.

Through the documented entry point, this branch is 1612/1612, zero failures:

npm --workspace @maka/desktop test # clean:main && build:main && node --test

The stash comparison I cited was still valid as far as it went — those failures were not caused by this change — but characterising them as pre-existing in the repo was not something I had actually verified. Apologies for the noise; the PR description now carries the corrected number.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks again for the careful alias migration work. I found one scope issue in the follow-up, and I’m sorry I did not make this boundary explicit in the previous review.

The alias migration itself is necessary, but SUPERSEDED_MODEL_ID_ALIASES is Anthropic-specific while reconcileConnectionAfterModelFetch() is shared by every fetched provider. A custom relay may legitimately use Claude-looking model IDs as opaque identifiers. If its inventory contains the alias but not the dated ID, the generic reconciliation now rewrites the user’s default/selection; for compatible relays, the subsequent pruning can also remove the old ID’s relay profile.

Could we keep the generic reconciliation provider-agnostic and pass the alias map as an explicit optional input only from the claude-subscription sync path? A focused regression test showing that the same IDs are not rewritten without that alias context should be enough.

This is a small scope correction to the migration requested in my previous review, not a change in the overall direction. Sorry for the extra round, and thank you for handling the earlier feedback so thoroughly.

@Joob1n

Copy link
Copy Markdown
ContributorAuthor

You're right, and the boundary was mine to see — thanks for catching it.

Fixed in 1bce08f. reconcileConnectionAfterModelFetch treats ids as opaque again: nothing is rewritten unless a caller passes aliases, and only syncClaudeSubscriptionConnection does. Renamed the table to CLAUDE_SUBSCRIPTION_MODEL_ID_ALIASES so the scope is legible at the use site rather than only in its doc comment.

The relay-profile consequence you flagged is worse than a rewritten selection, and I confirmed it: pruneRelayModelProfiles filters on the freshly computed enabledModelIds, so a rewritten id orphans the profile keyed on the old one and it is dropped on the same write. The user's own declarations — vision, context window, thinking levels — disappear with no trace. What makes this clearly my error rather than a judgement call is that the repository already states the rule I broke, in the comment right above that prune: "a model id that coincides across endpoints is a different model." I had read that comment earlier in this work and still generalised the opposite way.

On the regression test: my first attempt asserted the right value for the wrong reason. With the alias absent, a stored claude-haiku-4-5-20251001 falls through to the first live id — which, in the fixture I had written, happened to be the alias. It would have passed with the global table still in place. The test now orders the live list so the alias is not first, making aliased and unaliased behaviour produce different answers, and asserts both directions.

Verification: @maka/core 832/832, @maka/desktop 1612/1612, four-project typecheck, lint and format clean.

No apology needed for the extra round — the two rounds each removed a real defect, and this one would have been expensive to find in the field.

@Joob1n

Joob1n commented Aug 8, 2026

Copy link
Copy Markdown
ContributorAuthor

Two follow-ups, found by re-checking the call sites rather than by review:

  • Moved the table to model-metadata.ts, beside the curated list every alias target has to appear in. (e7f313884)
  • Scoping it to the desktop sync left the Runtime Host path behind — same defect, different route. Both now resolve it through modelIdAliasesForProvider, and a test holds every other provider at undefined. (3d1843346)

core 833, storage 754, desktop 1612, runtime-host 729 — green. Description updated.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for working through the follow-ups. main has moved quite a bit since this started: #2420 moved the flow from Desktop to Runtime Host. Could you rebase and remove the now-obsolete Desktop sync changes and test?

The provider-scoped alias fix makes sense. The rebased PR could likely stay focused on the Haiku compatibility and Runtime Host wiring, without the catalog helper or Sonnet rewrite. A small Storage regression test would be nice if convenient.

Happy to take another look once it’s rebased.

An existing `claude-haiku-4-5-20251001` selection was repaired onto
`claude-opus-5`. The curated inventory lists that model under its `claude-haiku-4-5`
alias, but reconciliation compares ids literally, so the stored form read as a
model the catalog had dropped and repair fell through to the first live id —
across model family and price tier, with nothing on screen.
The rename table is passed in rather than assumed, and selected per provider by
modelIdAliasesForProvider. Reconciliation is shared by every provider that
commits a fetched inventory, and a relay may serve `claude-*` ids as its own
opaque identifiers — the rule connection storage states where it prunes relay
profiles across endpoints. Under a global table such a relay would have had its
selection rewritten and the profile keyed on the old id pruned with it.
Retirement is untouched: an id the table does not name is still repaired against
the live list, and a test asserts through `lifecycle` metadata that no deprecated
model can be added to it.
@Joob1n
Joob1nforce-pushed the fix/claude-subscription-stale-model-inventory branch from 92ed85a to 7f02265CompareAugust 9, 2026 04:33
@Astro-Han
Astro-Han merged commit 893ee3e into apache:mainAug 9, 2026
11 checks passed
Sign up for freeto 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

@Joob1n@Astro-Han