Skip to content

feat(models): read model definitions at start-up so a new model needs no release - #663

Merged
nubbymong merged 11 commits into
betafrom
feat/model-definitions-data
Nov 14, 2026
Merged

nubbymong merged 11 commits into
betafrom
feat/model-definitions-data

Conversation

@nubbymong

@nubbymong nubbymong commented Nov 14, 2026 •

Copy link
Copy Markdown
Owner

What this does

The model list can be updated without a new version of the app (planning #307, "model registry as no-release data"). This is the last PR of 2.1.3-beta.1 and carries its release entry and version bump.

  • Fetch (src/main/model-registry-remote.ts): once at start-up, in the background. It reads model-registry.v1.json from the model-data branch of this repository on raw.githubusercontent.com.
    • 5 s timeout, 256 KB cap.
    • A redirect is followed only if it stays under this repository's path.
    • No sign-in.
  • Strict check (src/main/model-registry-data.ts): a zod schema with a format version. The format is also in the file name, so a later format is published as its own file and never locks an older app out.
  • Fallback: the last good copy is kept in the config folder. When the fetch fails or the file is refused, the app uses that copy, then the bundled resources/model-registry.json.
  • Fable 5.2 is added to the bundled list, with a cache write price of 10.

The naming rule (what a fetched file may and may not do)

A fetched file can add models and correct labels and prices. It never removes a model, and never changes what a name the build already resolves points to:

  • A shipped model's patterns and aliases always come from the bundled file. A fetched change to them is left out.
  • Shipped entries are tried before fetched-only ones in the id-prefix and pattern steps. The picker order is unchanged.
  • A fetched-only model is refused, with the whole file and one log line naming the shipped entry, when any of these holds:
    • its id is a shipped id or alias;
    • its id is a dated (-YYYYMMDD) or [..] form of any shipped id;
    • an alias is a shipped id or alias, or a dated or [..] form of a shipped id;
    • an alias is placed on a shipped entry and does not start with the new id (or that shipped id itself starts with the new id).
  • Taken: claude-fable-5-3 with its [1m] and dated aliases, claude-opus-5-6, claude-opus-5-5-2, gpt-5.5-pro, and claude-opus-6.

Review

  • ADR-009 (model names reach --model): attackers ran on the build. The fix batch (9 findings) had a narrow check.
  • Resolution rule: four narrow fix-and-check rounds closed the class.
    • Shipped patterns and aliases are taken from the bundled file.
    • Fetched-only names are refused when they claim a shipped name.
    • New version ids are allowed.
    • Dated and [1m] forms are tested against every shipped id.
  • Final check, a sweep: 86,964 generated files (4,469 accepted). Each accepted file was merged with and without a user overlay, and resolved on 122 probe names. That gave 0 differences from the bundled list alone. The same sweep on the previous commit found 124, so it can fail.
  • Tests: a property test (tests/unit/model-data-resolution-rule.test.ts) plus schema, fetch, merge, resolve and service tests. 277 tests in 10 files pass, and every mutant of the changed rule lines is killed. One mutant survives: the [ branch, which is defence in depth because ids cannot contain [. Typecheck and the WP1 ledger are clean.
  • Pricing lookup: a private assessment of the related tokenomics price lookup found no vulnerability.

Release (2.1.3-beta.1)

  • src/renderer/changelog.ts gets the 2.1.3-beta.1 entry, and CHANGELOG.md is regenerated.
  • package.json and the root version in package-lock.json are 2.1.3-beta.1.
  • Surface sweep:
    • Feature Guide: new "The model list" section, the Sentinel signed-out message, hooks off stays off and supervised restarts, the privacy network list, and the Windows start-up known issue.
    • Tips: the Network Activity tip lists the model list read. No new tip.
    • Tour and README need nothing.
  • What's New: two upgrade-only lines tagged 2.1.3-beta.1 (the model list refreshes without an update; Fable 5.2), required by the drift guard. summaryContent now drops curated lines newer than the running build, as the changelog side already does; a shipped build sees no change. Narrow check passed; its one MINOR (pin the 2.1.2 to 2.1.3 lines) is fixed.

Decisions taken on stated defaults

  • A file that breaks the rule is refused whole (logged), not partly used. Revisit before the first release whose bundled file moves a family alias to a new model, because older installs would refuse that file.

Owner steps (needed before the feature fetches anything; the release does not wait on them)

  1. Create branch model-data with a ruleset: only you can push, no Actions bypass, no force-push, no deletion.
  2. Put resources/model-registry.json there as model-registry.v1.json.

Until then the fetch gets a 404 and the app uses the bundled list.

Not tested

The app itself; a live fetch. Owner check on the released beta: Settings shows Fable 5.2 in the model picker.

🤖 Generated with Claude Code

@nubbymong nubbymong added ci-run Run full CI matrix (Win+macOS) on this PR release-2.1 Targets the 2.1 release line labels Nov 14, 2026
Copilot AI balanced review requested due to automatic review settings November 14, 2026 04:34
@nubbymong

Copy link
Copy Markdown
Owner Author

ADVERSARIAL-REVIEW v1 | PASS | pr=#663 | head=d51e22c32ab5eda2d5fa18ee867a67d57250fc0e | content=901bf43e3f39eee1ed0dbc80c937a23fc87a6dc4b3d4484e1130fde61aefe5a6 | 0 blockers, 0 majors open

Lenses: attackers on the build (fix batch of 9, narrow check), then four narrow fix-and-check rounds on the naming rule; the final check was a sweep of 86,964 generated files with 0 differences from the bundled list (positive control 124 on the previous commit). Details in the PR body.

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

A critical prefix-matching collision resolves dated Fable 5 names as Fable 5.2 and assigns incorrect fallback prices.

4 open issues
What changed in this PR

This PR lets AI Code Conductor refresh its model registry at startup without an app release, while retaining cached and bundled fallbacks. It also adds Fable 5.2 and prepares 2.1.3-beta.1.

Changes:

  • Adds bounded background fetching, strict validation, caching, and registry reloads.
  • Preserves shipped model naming rules and integrates updates with Sentinel.
  • Adds regression tests and updates release notes, versioning guidance, and network disclosures.
File Description
tests/​wp1/​legacy-codex-ledger.json Records new registry modules and tests.
tests/​unit/​model-registry-remote-service.test.ts Tests refresh, caching, fallback, and revisions.
tests/​unit/​model-data-schema.test.ts Tests validation, merging, and bundled models.
tests/​unit/​model-data-resolution-rule.test.ts Tests naming and overlay precedence rules.
tests/​unit/​main/​sentinel-model-findings-retire.test.ts Tests notice retirement and overlay retention.
tests/​unit/​main/​model-data-fetch.test.ts Tests transport limits and failure handling.
src/​shared/​model-registry.ts Adds resolution ordering and literal pattern matching.
src/​shared/​app-knowledge.ts Updates the Feature Guide and release-related guidance.
src/​renderer/​tips-library.ts Discloses the startup model-list request.
src/​renderer/​changelog.ts Adds the beta release entry.
src/​main/​sentinel/​index.ts Handles registry updates and preserves durable overlays.
src/​main/​model-registry-service.ts Loads, caches, and broadcasts model data.
src/​main/​model-registry-remote.ts Fetches model data with bounded transport rules.
src/​main/​model-registry-data.ts Validates and merges fetched definitions.
src/​main/​index.ts Starts the background registry refresh.
resources/​model-registry.json Adds format metadata, revision, and Fable 5.2.
README.md Updates outbound-traffic disclosure.
PRIVACY.md Documents the model-list request and cache.
package.json Bumps the version to 2.1.3-beta.1.
package-lock.json Synchronizes root package versions.
docs/​wp1/​legacy-codex-manifest.json Refreshes the coverage manifest.
docs/​versioning.md Documents registry revision and publication steps.
CONTEXT.d/​2026-11-14-model-definitions-data.md Records the feature design and limitations.
CONTEXT.d/​2026-11-14-model-definitions-data-review.md Records review-driven changes.
CHANGELOG.md Adds generated beta release notes.

Review effort: Balanced

"revision": 20261114,
"models": [
{ "id": "claude-fable-5-1", "patterns": ["fable"], "aliases": ["fable"], "family": "fable", "label": "Fable 5.1",
{ "id": "claude-fable-5-2", "patterns": ["fable-5-2"], "aliases": ["fable", "fable[1m]"], "family": "fable", "label": "Fable 5.2",
// config-manager at start-up, long before a read can end. It is never
// made here: a folder made here would not be owner-only.
if (!fs.statSync(path.dirname(file)).isDirectory()) throw new Error('no config folder')
atomicWriteFileSync(file, text)
export function sentinelModelDataChanged(): void {
if (!state) return
try {
state.retireOpen(registryCoveredModelFindingIds(state.snapshot().findings, getRegistry()))
Comment on lines +215 to +216
const at = (m: ModelEntry): number => rank.get(m.id) ?? rank.size
const standing = models.filter((m) => !isModelDataEntry(m)).sort((a, b) => at(a) - at(b))
nubbymong and others added 11 commits November 14, 2026 05:06
… no release

A new model needed a release, and until one shipped Sentinel kept saying
the model was not in the model picker.

- The model file is read once at start-up, in the background, from the
  model-data branch of this repository: https only, one host, 256 KB at
  most, an answer within 5 s. Start-up and the window never wait for it.
- It is checked strictly before use: only the bundled file's fields,
  bounded plain strings, ids from a small alphabet, numbers in range, a
  format version. A pattern is plain text and is never compiled.
- A good file is used at once by every reader through the existing
  service, and kept as the last good copy in the config folder. On any
  failure the last good copy is used, else the bundled file, with one log
  line that says why.
- The file can add a model or correct one; it cannot take one away.
- Sentinel closes the "not in the model picker" notice as soon as the
  file carries the model.
- Fable 5.2 is added to the bundled file as the latest Fable.
- PRIVACY.md and the README list the new request.

To add a model: edit model-registry.json on the model-data branch.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…uild's own

Review fixes for the model-data change, before the branch file is first
published. Each has a test that failed first.

- Each model file carries a revision (the date of its last edit). Model
  data older than the bundled file is not used: a read is logged as not
  read, and an older last good copy is removed at start-up. Before, a copy
  kept by an earlier version, or a branch that lagged behind a release,
  put back the earlier aliases, labels and prices with no log line. A
  test pins the bundled file's content beside its revision.
- The format is in the file's name (model-registry.v1.json), so a later
  format is published beside this one and no installed version is locked
  out of new models by a field it cannot read.
- The file can no longer empty a picker: whether a shipped model is
  offered is the bundled file's to say, and alias rows and effort levels
  join by name, so the file adds and corrects and never takes away.
- Tighter rules: a price is at most 1000 per million tokens; a model the
  build does not ship needs a full name (its family and a version); a new
  pattern is at least four characters; a redirect is followed only inside
  this repository's path on the host.
- A fetched file no longer deletes an overlay entry Sentinel added. The
  overlay is reconciled against the bundled file alone, so only a release
  retires an entry; the notice still closes at once.
- Fable 5.2 no longer takes partner-platform ids of Fable 5.1 and 5: it
  has its own pattern and the fable and fable[1m] aliases, and the plain
  fable pattern sits on the last Fable entry.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…y has

A model the bundled file does not ship could carry a pattern the bundled
file has on one of its own entries, such as `opus`. The fetched file's
models are matched first, so every Opus name no entry lists was read, and
priced, as the new entry.

Such a file is now refused. The pattern is compared by its text, so the
same text with or without `^` is refused too, and the one log line names
the bundled entry that has the pattern. A new model still brings a pattern
of its own (`opus-6`), and a shipped model keeps every pattern the bundled
file gives it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fetched model file could still change what a name the build already
resolves resolves to. A new model with a near-variant of a shipped pattern
(`opus-`, `claude-opus`), or an id that starts a shipped model's dated
names, took those names; and the file could give a shipped model another
shipped entry's pattern, or list the shipped entries in another order.

The rule now: a fetched file never changes what a name the build already
places resolves to.

- A shipped model's patterns are always the bundled file's. A fetched
  change to them is left out, not refused: a file edited for a later build
  may have other patterns on a model this build ships, and refusing it
  would lock this build out of every later file. Its label, prices and
  other fields are still corrected.
- A name that is no entry's id or alias is tried against the shipped
  entries, in the bundled file's order, and the user's own overlay
  entries, before a model only the fetched file has. So such a model is
  its exact id and its aliases, and beyond those only a name nothing else
  places (a new family still takes its own names).
- The order is held in memory (`shippedOrder`, set by the merge), never in
  a file. The models list, which is the picker order, is still the
  fetched file's.

The refusal of a new model that carries a shipped pattern stays, with its
log reason: it tells of a mistake in the file.

A dated or partner-platform form of a NEW model's id is no longer the new
model by id-prefix or pattern when a shipped pattern places that name: a
dated form needs an alias in the file, and a partner platform's form stays
with its family's shipped entry until a release. Three assertions that
pinned the earlier behaviour are updated.

Property test (tests/unit/model-data-resolution-rule.test.ts): over 200 names
against 11 fetched files that pass every check; red on b5a0aed4 for all
11. The WP1 ledger and manifest gain the new test file.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…el file

The rule (a fetched model file never changes what a name the build
already places resolves to) was still open on two points.

- A fetched file could move or drop a shipped model's aliases: give
  `opus` to another shipped entry, or leave `fable[1m]` out. A shipped
  model's aliases are now the bundled file's, exactly as its patterns
  are: the file's are left out, not refused, and an entry the bundled
  file gives no aliases has none. Its label, prices and other fields
  are still corrected.
- A model only the fetched file has could carry an id or an alias the
  bundled file alone already gives to a shipped entry by an id or an
  alias: a shipped alias (`opus`), a shipped id, or a longer form of a
  shipped id (a dated id, `[1m]`). An exact id or alias is looked up
  first, so the name left the shipped entry. Such a file is now
  refused, in the style of the shipped-pattern refusal beside it, and
  the reason names the shipped entry. The question is asked of the
  matcher itself (resolveModelInfo on the bundled file), so the two
  cannot drift apart.

A name the bundled file only reaches by a pattern, or not at all, is
still free: `claude-opus-6` is taken as before. A new model whose id
starts with a shipped id (`claude-fable-5-3` beside the shipped
`claude-fable-5`) is now refused: the bundled file places that name by
its id-prefix step.

Property test (tests/unit/model-data-resolution-rule.test.ts): 21 fetched
files, 13 put to use and 8 refused, each held to the rule as the app
would treat it; the 11 new or changed files are red on 9afc6188. Two
earlier files that took a shipped alias, or a longer form of a shipped
id, and were expected to succeed are now expected to be refused.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…d or alias

A fetched model file was refused when the bundled file reached a new model's
id by its id-prefix step, so claude-fable-5-3 (reached through claude-fable-5)
could not be added without a release. A new model's alias that the bundled file
reached only by a family pattern was not checked at all.

The rule for a model the build does not ship is now:
- its id is refused only when the bundled file places it by an exact id or an
  alias; placement by id-prefix or pattern leaves the id taken
  (claude-fable-5-3, claude-opus-5-6, gpt-5.5-pro, claude-opus-6);
- each alias is refused when the bundled file places it by an exact id or an
  alias, or by an id-prefix or a pattern when the alias does not start with the
  model's own id (claude-fable-5-3[1m] is its own name; opus and
  claude-opus-latest-x are not).

Refusal of the whole file, with one log line naming the shipped entry, is
unchanged. Tests: the accepted and refused cases above, with the property
helper following the same rule; every changed condition is killed by a mutant.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… model's id or alias

A model only the fetched file has could still take a dated or bracketed form
of a shipped id: as an alias (claude-opus-4 with claude-opus-4-7-20260101 or
claude-opus-4-7[1m], claude-haiku-4 with claude-haiku-4-5-20251001, which pass
because they start with the new id) and as an id (claude-opus-4-5-20260101,
gpt-5.5-20260101, which the id rule let through because the bundled file
reaches them only by an id-prefix). The file's model then won that exact name
from the shipped one.

For the id and every alias, a name that is a shipped id followed by -YYYYMMDD or
[ is now refused whatever way the bundled file places it. And an alias that
starts with the model's own id is exempt only when the shipped id does not
also start with it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nly the one the matcher placed it on

idHeldBy and aliasHeldBy tested the dated or [1m] form only against the
shipped entry the matcher placed the name on. claude-fable-5-20260101 is
the dated form of claude-fable-5 but the matcher places it on
claude-fable-5-2 by id-prefix, so it was taken as a new model's id and won
that exact name. Same for claude-fable-5-20251001, and as an alias.

Both now test every shipped id first (formOfShipped), whether or not the
bundled file places the name. The accepted new-model shapes (claude-fable-5-3
with its [1m] and dated aliases, claude-opus-5-6, claude-opus-5-5-2,
gpt-5.5-pro, claude-opus-6) are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n the build

The What's New drift guard requires curated lines for a release with a
feature: add two upgrade-only lines tagged 2.1.3-beta.1 (the model list
refreshes without a new version; Fable 5.2 in the picker) and register them
in the frozen tag table.

summaryContent now drops curated lines newer than the running build, as
entriesSince already caps the changelog. A shipped build is always the
newest of its sets, so users see no change; the fixes-only test that renders
an older build against the real sets keeps its meaning. The anchor test now
uses the newest curated tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nubbymong
nubbymong force-pushed the feat/model-definitions-data branch from d51e22c to db9afed Compare November 14, 2026 05:06
@nubbymong

Copy link
Copy Markdown
Owner Author

ADVERSARIAL-REVIEW v1 | PASS | pr=#663 | head=db9afed156e4033dfd5a163c7e7c31f770302ded | content=e9336b8ebae37798d6dfb469164331ba129e10f553f3b1104851cfd25b471367 | 0 blockers, 0 majors open

Rebased onto beta after #659 and #662 (WP1 ledger regenerated). Added since the last marker: two What's New lines for 2.1.3 and a cap so the page never lists a line newer than the running build (renderer only, not an ADR-009 path), with a narrow check (no blocker or major; its one MINOR, a pinned 2.1.2-to-2.1.3 test, is fixed in db9afed). The model-data code is unchanged.

@nubbymong
nubbymong merged commit 6fc6062 into beta Nov 14, 2026
19 checks passed
@nubbymong
nubbymong deleted the feat/model-definitions-data branch November 14, 2026 05:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-run Run full CI matrix (Win+macOS) on this PR release-2.1 Targets the 2.1 release line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants