Repository navigation
feat(models): read model definitions at start-up so a new model needs no release - #663
Conversation
|
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. |
There was a problem hiding this comment.
🟡 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())) |
| 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)) |
… 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>
d51e22c to
db9afed
Compare
|
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. |


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.
src/main/model-registry-remote.ts): once at start-up, in the background. It readsmodel-registry.v1.jsonfrom themodel-databranch of this repository on raw.githubusercontent.com.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.resources/model-registry.json.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:
-YYYYMMDD) or[..]form of any shipped id;[..]form of a shipped id;claude-fable-5-3with its[1m]and dated aliases,claude-opus-5-6,claude-opus-5-5-2,gpt-5.5-pro, andclaude-opus-6.Review
--model): attackers ran on the build. The fix batch (9 findings) had a narrow check.[1m]forms are tested against every shipped id.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.Release (2.1.3-beta.1)
src/renderer/changelog.tsgets the 2.1.3-beta.1 entry, andCHANGELOG.mdis regenerated.package.jsonand the root version inpackage-lock.jsonare 2.1.3-beta.1.summaryContentnow 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
Owner steps (needed before the feature fetches anything; the release does not wait on them)
model-datawith a ruleset: only you can push, no Actions bypass, no force-push, no deletion.resources/model-registry.jsonthere asmodel-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