Repository navigation
test: restructure the suite - #116
Conversation
- Stop the run on a Node below engines.node, before collecting a file. Below the floor, failures came from the engine (node:sqlite, Intl.DurationFormat, ICU strings) and looked like code regressions. .node-version names the Node CI uses. - Add a CI job that runs the unit and mock suites on Node 24, the floor engines.node promises but no job ran. - The zh fallback duration unit is now 分钟, the platform's own narrow output, so the meter row reads the same with or without Intl.DurationFormat. The DurationFormat tests restore the engine as they found it, rather than leaving a stand-in installed or asserting the engine ships the constructor. - Move the node:sqlite store fixture out of the helpers that the jsdom tests share, into test/support/opencode-store-fixture.ts. - Add an architecture test that walks the client bundle's runtime import graph from settings-page.tsx. It forbids node built-ins, the cordis runtime, pi, and process.env. Checked by planting a violation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…n discovery - A rejected internal-route mount ran from a `void` IIFE with no catch, so a Google bridge failure became an unhandled rejection, which Node's default mode answers by exiting the process. mountInternalRoutes now owns the mount and its withdrawal, and reports a rejection. - Three early exits in registerResponsesProvider (internalRoutes off, every route declared, no llm-pi-ai) answered `undefined` after the Gemini bridges had already registered. That left them with no disposer, and the next reload failed DUPLICATE_ADAPTER. They now hand back the bridges' disposer. - A mount that failed to start, or that captured nothing, kept its fiber and every route it had registered. Both paths now release the routes and dispose the fiber. - Model discovery was registered on every activation, and the host's duplicate refusal was swallowed, so the first activation's settings answered for the life of the process. It is now registered once per root and answers from the latest activation's settings. Each namespace registers on its own, so one refusal no longer skips the rest. - Move three doc blocks back above the functions they describe. Each fix has a regression test. The two mount tests fail without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…e the suite Request path and meter: - Price a turn on its route's plane. Go-first lookups priced every Zen turn on deepseek-v4-pro at 0.66/1.98 instead of 1.74/3.48. - The listing and model-info patches now act only on the routes this plugin claims. Before, a direct anthropic or openai route lost rows the OpenCode routing called "unservable", and a refused foreign id was answered with OpenCode's catalog. - A relay's Go /models is answered with the Go catalog. The enrichment re-checked the URL against the default gateway markers and fell back to Zen. - The free-model check reads a Request-carried body. That is how DSH sends a turn, and every keyless free turn was reported as a missing credential. The body is now read once for both uses. - The meter's session fallback is the session that most recently ran a turn, not the most recently created one. - A captured key's tier comes from the observed URL, then the route, and never the key's prefix. OpenCode issues oc_sk_ keys for Go, so a working Go key was filed under Zen. An OpenAI sk- key on a claimed relay was filed under Go. Suite isolation: - The unit and mock suites refuse non-loopback requests. Each unit run sent 54 requests to models.opencode.ai through the catalog's background revalidation, so on a networked runner a result could depend on the vendor's live catalog. - Captured keys, session spend and the live catalog are reset after every test (resetCatalogState is new). Shuffled runs failed catalog cases on seeds 42 and 1234. test:shuffle runs the suite in random order, and the Node 24 CI job uses it. Each behaviour change has a test that fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…egration - The card's store-inspection row called remote.opencodeStore.inspect, but no client contribution carried that namespace's descriptor. The row reported "unavailable" on a host that had the service. storeRemote now carries it, with a whitelist codec, and the client mounts it. - The host/client parity test missed this because it matched only a string literal in super(ctx, "name"), and the store passes a constant. It now resolves constants, and checks every declared Remote method against a client descriptor, not just every namespace. - inspectStore parses through the whitelist instead of a cast, so a field the host did not mean to send (a token) is dropped. An ok:false envelope reports the remote's own message. - The credential reader's legacy-row fallback was uncorrelated: one flagged opencode-go row hid every legacy opencode row, and the Zen key disappeared. The fallback is now per integration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… keeps - AGENTS.md's repo map described nine modules this build does not have (the Console login, token store, org cache and sign-in row). They are replaced with one line saying sign-in is v2 work. The go-limits duplication note is gone, because attachAllowance calls allowanceFor. adapter-replacement.ts is marked as not wired. The client-bundle line names the boundary test. - CONTRIBUTING.md documents the engine floor, test:shuffle and test:mock, the offline guard, the per-test state reset, where host-only fixtures live, and that a regression test must fail without its fix. - The no-credential warning no longer tells the reader to "sign in through the settings card", which this build does not have. It points at readOpencodeStore instead, and a test asserts the advice. - CLIENT_BUDGET is raised from 128 to 132 KB for the store descriptor and its parser (about 1.2 KB of source, not a dependency), following the gate's own instruction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
docs/improvement-plan.md groups the review's findings by cause and orders the fixes in nine phases after this one. Each phase lists its problems, its steps, the test that has to fail first, its acceptance criteria and any decision the owner needs to make. Findings are marked "measured" (reproduced) or "traced" (read in the code). Among the measured ones: the picker and dispatch derive reasoning levels differently for 9 of 95 catalog models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Replace docs/improvement-plan.md with the plan of record. It now holds: - the owner's decisions (D1-D11); - what the DeepSeek Harness host supports, with references into packages/llm, llm-pi-ai, session-controller, gateway and ui-settings-models; - the defects this audit found that are not yet fixed; - the target architecture and the phases, one PR each, with a status table that each PR updates. The decisions that change the code: - Settings offers the whole catalog, and the composer offers only the declared models. - The plugin never writes the user's profile and follows the user's configuration at dispatch. - The adapter patch exists to merge provider modes and nothing else. It is generalized to every mode, and its catalog fill is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Investigation of the host's source and of the pi catalog it pins (1.1.0) changed the routing design. - llm-pi-ai resolves `api` and `baseUrl` per model, and pi's installed `opencode` catalog already spans Anthropic, Google, Completions and Responses with per-protocol endpoints. One route serves every mode for every model pi knows. - The plugin fills only three gaps: models newer than pi's catalog (including all Gemini on Go), endpoint drift, and profile-level `api`/`baseURL` collapsing the modes. - The new design is one unregistered catalog adapter per plane, keyed by the user's route id, and the adapter patch (merge-modes) delegates only gap models to it. That deletes the hidden routes, the listing hide, the stream redirect, the child mount and the Gemini bridge registration. - A real-host test harness (P-T1) comes before any of it. - One upstream change (per-entry `api`/`baseURL` in llm-pi-ai, U1) would remove the routing layer entirely. It would also let Settings save a model pi doesn't know, which pi-ai's strict write validation rejects today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
pi-ai registers exactly the providers in its resolved config, and the fiber keeps whatever the internal/config waterfall returns, while the config editor writes the raw value. A prepended listener that resolves pi-ai's config without opencode/opencode-go therefore keeps pi-ai from claiming them, without touching the user's file, and the plugin registers its own PiAiAdapter under the same ids through the public API. Measured on the real host with pi 1.1.0 and the harness's patch: both routes held by the plugin, pi-ai's other routes unchanged, every protocol on both planes (Gemini on Go included) sent to its own endpoint, a catalog-unknown model id saved, listed and dispatched, and both routes handed back to pi-ai on release. Replaces the upstream asks (no upstream changes) and the save-gate design, which is kept only as the fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
A nine-lens review of the routing design, with two independent skeptics per finding (78 of 82 upheld), changed five things before any code moves: - Settings reads pi-ai's resolved config, so a read-only wrapper on settings.describe puts the plane entries back into the displayed value; writes were never affected, the Models page edits by path. - The route claim gets an explicit lifecycle: commits are told from validations by identity, reconcile runs on commit, mount and teardown are ordered, and pi-ai restarts at most once per transition. - The plugin's adapter is built from public pieces only, with pi-ai's credential rule and no captured keys on the adapter path. - The protocol comes from the catalog map, never from a guess at the model's name; metadata and levels are catalog first. - Releases stay on 1.x, phases land as stacked pull requests, and a real host harness plus a binary e2e job pin the behaviour first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
- The deprecated @viztor/dsh-opencode wrapper re-exported a `default` the
barrel does not have, an ESM link error on every host load. It now
forwards `export *` only, written by one shared helper that a test links
against the built barrel in a real Node process.
- isZenProvider("opencode-google") answered false: "go" inside "google"
read as the Go plane. It now uses the plane rule the catalog already has.
- enrichModelsResponse answered a 401, 403, 429 or 5xx with a 200 catalog.
Those statuses now pass through; a 404 is still filled from the catalog,
because the Go plane's inference base has no /models of its own.
- The warning colour named a token the host does not define, so warnings
rendered as caption text. They now use --dsw-alias-state-warn-label.
- The settings spinner only animated while the meter was mounted; the
settings surface now carries its own keyframes.
- The stream hook wrote debugFile with debug off. debug is the switch and
debugFile only where it writes, the same gate the fetch patch uses.
- The purge row reported the host's own { ok: false } failure as "not a
purge result" and cast the answer; it now shows the host's message and
parses through the contract's whitelisting parser.
- The endpoint copy described modern as /inference/v1, which the plugin
never sends. It now names the per-protocol inference family, and auto as
its alias, which is what the code already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
- Delete adapter-replacement.ts and its test. Nothing imported it, and deleting another plugin's registry entry is not a host API; the engineering notes record why, and what the plugin does instead. A gate fails the build if src/ writes the llm registry map directly. - Delete the stream hook's `prepared` handling: the host passes the prepared dispatch as its own argument and never reads options.prepared. - Untrack .freebuff/ and ignore it. - The settings partition test walks Config.dict, which is how debugFile went missing from it; a manifest test checks cordis.patch.yml against the schema and the field registry, and the manifest's phantom showUsagePrice, wrong marker text and incomplete lists are fixed. - utils/retry.ts takes its timer and logger as parameters, and the utils purity gate now also rejects console and global timers, walking the folder recursively. - tool-fallback reads free-tier ids from the request's own plane (Go iff opencode-go or opencode-go-*) and loses its unreachable Anthropic branch. A Zen-only free model is no longer treated as free on Go. - test/support/src.ts is the one door tests use into src/, so later module moves touch one file; CONTRIBUTING.md states the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The barrel is the host's contract and nothing more: apply, Config, inject, name and the config types. Everything else is internal, and test/public-api.test.ts pins the list, so widening it is a reviewed change. The 1.x line keeps its version scheme: nobody consumes the removed exports (D6). - Every test reaches src through test/support/src.ts, importing each name from the module that defines it; a gate rejects a static src import anywhere else outside its named exemptions. - package.json no longer exports ./src/*, and check.ts fails when an exports target is outside the published files. - resolveRoutedKey is deleted: it was exported and never called. - The internalRoutes setting is deleted; the per-protocol routes always mount until the plugin owns the plane routes. A row that still carries it validates and the key is ignored. - @viztor/dsh-opencode is no longer published. The deprecation message already on npm stays as its final word, and check.ts asserts that the release workflow verifies exactly what publish-scoped.ts publishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The test install now runs the host users run. - pi is pinned to 1.1.0, the version the harness's own lockfile pins, with the harness's patch applied verbatim (patches/, hash recorded by pnpm). A pnpm override holds every copy in the tree to it, not just the direct devDependency. - The @deepseek-ai packages move from 0.2.1-alpha.5 to 0.2.1-rc.2, the version a registry install of the published dsh resolves (cordis 4.0.5, loader 1.0.6, schemastery 3.18.5). The boot-tier packages the real-host harness needs next (dsh-llm, dsh-app-boot, dsh-config-editor, dsh-settings, cordis-plugin-loader) are added as devDependencies. - test/support/host-versions.ts names those versions once. test/host-versions.test.ts checks that the pi the plugin loads at runtime is 1.1.0 and the same copy the tests import, that the patch is applied, and that the installed cluster matches. - mount-protocols now checks that every catalog SDK maps to a protocol pi's exported api modules implement, and records as a characterization row that pi 1.1.0's opencode-go provider has no Google API. No src/ behaviour changes: the rc.2 builds of llm-pi-ai, cordis and schemastery are code-identical to alpha.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Add an in-process harness that runs the plugin inside the real host,
so the routing work that follows can be measured against today's
behaviour rather than against fakes. Additive only: no src/ change.
test/support/host.ts gives two tiers. Tier A is a bare cordis Context
with LlmRuntime and llm-pi-ai mounted with { providers }, for the
config waterfall and pi-ai's credential paths. Tier B is dsh-app-boot's
boot() with a temporary HOME, a test bundle, a fake credentials service
and a launch-environment snapshot, for the config editor,
settings.describe and the volatile commit. Tier B's rows carry package
names through a stub module loader, and it fixes the apply order,
because the loader starts rows in parallel.
On top of it:
- contract tests for the waterfall, the profile and Config shapes,
credentials, and the config editor (host-waterfall, host-shapes,
host-credentials, host-editor-contracts), plus a tier A smoke test;
- a characterization of today's behaviour on pi 1.1.0, each row tagged
where P-R1 is expected to flip it, with S0_TODAY shared by the
binary e2e;
- the keyless 401-vs-404 matrix: a pure classifier, its unit test and
a gated live run;
- a binary e2e gated on DSH_HOST_E2E=1 with its probe bundle, the
e2e-host CI job (a pinned, patched cell on PRs that touch the host
seams or vite.config.ts, and a floating cell nightly and on
dispatch), and the test:e2e:host script.
The binary e2e now reports why a run failed: a timeout only when its
own deadline killed the host, the signal for a crash, and the probe's
fatal message whenever there is one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Add src/host/patch-method.ts (patchMethod), now the only way the plugin wraps a method on a host object, and move the four instance patch sites in src/models-discovery.ts onto it: the three listings hidden by hideResponsesRoute, decorateModelDiscovery, decorateModelInfo and decoratePlaneAdapters. On the real host the three llm-level disposers never restored: ctx.llm is cordis's traceable proxy, every method read returns a new shadow proxy, so the `===` identity checks were never true and each reload stacked a wrapper over the last. The helper reads identity from own descriptors instead. A dispose restores only while its wrapper is still current (otherwise it becomes a pass-through), deletes the own property when the original was inherited, and a same-site reinstall wraps the unpatched method rather than the earlier install, so either disposal order ends at the exact original. decoratePlaneAdapters becomes one patch per (adapter, method) that composes the adapter's plane routes in registry order, which keeps today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is built and tested but wired to no site yet. test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and the reverse order) for every site on own and inherited originals, on the real host (tier A) and through apply(); test/patch-method.test.ts covers the helper's contract. The existing per-site pins in test/models-discovery.test.ts stay unchanged; five new cases there record the deliberate edge changes on hosts that refuse the patch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The vendor added kimi-k3.1-pro to the Go plane, so `catalog freshness` went red on every branch of this stack. This takes the two generated files exactly as the nightly refresh produced them (#111), with no hand edits, and updates the counts that hard-code the catalog's size: the Go shim test (28 -> 29), the plane table in redirect-planes.md (Go served 28 -> 29, the no-SDK Go cell 17 -> 18), and the Go completions list in both READMEs. The listed half of "29 served / 33 listed" is models.dev's count, inferred from the one added entry, because models.dev cannot be reached from here. It no-ops once main carries #111. P-T2 turns these counts into invariants so a refresh stops needing this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The plugin now serves the opencode and opencode-go routes itself, through supported host paths only, and the hidden-route workaround is gone. A prepended internal/config listener hands pi-ai a copy of its config without the two plane entries, so pi-ai keeps every other route. On a commit the plugin reconciles: it builds one plane adapter, a subclass of the host's PiAiAdapter, over pure profiles (utils/plane-profile, plane-model, model-metadata and protocols) and registers only the planes that are declared. A Settings candidate has the plane entries it changes validated strictly. A read-only settings.describe graft puts the committed plane entries back, so the host still sees them. Deleted: responses-provider, responses-routes, google-bridge, pi-provider and utils/google-replay; the listing hide and the discovery decoration; the stream redirect; the D19 settings; and the hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml. What users see: any saved id is accepted and served, and an id no catalog knows dispatches as completions. The protocol comes from the catalog and model info follows the user's own row first. A model whose reasoning is only on or off offers Off/On. The default endpoint is the per-protocol inference family. Keys resolve by pi-ai's own rule, and a plane lists its declared ids even when no key resolves. When several pi-ai rows declare planes, the row the plugin serves stays the same while it is live and still declares one, so no row is left stripped with nobody serving it. If the host's pi-ai module cannot be read, the plugin steps aside even before any plane is declared, so a later Settings save goes to pi-ai's own check. The characterization rows this changes are flipped to new baselines; every other row stays green unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
# Conflicts: # README.md # README.zh-CN.md # docs/redirect-planes.md
A measurement phase: the default endpoint family stays `modern`, so no plugin behaviour changes and no characterization row flips. - Name the default in one constant, AUTO_ENDPOINT_FAMILY in src/utils/plane-model.ts, which is what `auto` resolves to, and pin it in test/endpoint-family.test.ts. - Extend the keyless matrix to three URL shapes (legacy, root, modern) over four protocols and two planes, add LEGACY_DISPATCH, and add one keyless free-model probe with its own pure classifier. - Add minimal keyed probes (pure test/support/keyed-probes.ts and its e2e): one tiny request per plane, protocol and shape, sent with the plane's own key and the plugin's identity from one shared constant, PROBE_IDENTITY. Only the cells the default sends to are asserted; the rest are recorded. - Gate the live job by tier with OPENCODE_E2E_KEYED (none, probes, full; unset means full). CI arms `probes` on a pull request that touches what the probes send, `full` nightly and on dispatch, and passes the secrets only when the tier is not `none`. - Write a dated endpoint-family record from both live suites to the run summary and an `endpoint-families` artifact, and add §13 to docs/redirect-planes.md with every cell pending the first CI run. The live part runs in CI only; no live result is invented here. A failing keyed modern cell in CI is the evidence for a later `fix:` that flips the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
For the opencode and opencode-go routes, the Settings model list now shows pi-ai's own rows first, in pi's order, and then every model the plane's catalog can serve that pi does not list and the catalog has not retired. Ids that pi's registry has retired are dropped, a field a row lacks is filled from the catalog, and every row is cut down to the five fields the host's discovery contract accepts, with absent values omitted rather than set to undefined. While pi-ai restarts and its discovery is withdrawn (NO_DISCOVERY), the list answers the catalog rows alone instead of the error. This rewrites the existing discoverModels wrapper rather than adding a second one. Other routes keep today's enrich-only merge, because pi-ai serves them and cannot serve an id it does not list. The plugin's own model-discovery namespaces, which Settings never asked, are deleted with their lifecycle tests, and RouteClaim gains a claiming() seam. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Phase P-T2 of docs/improvement-plan.md. Test-only: no source file changes and no behaviour changes. - Every socket-bound case is in the mock suite. The nine unit files that booted the mock gateway through testing-kit.ts are merged into test/mock/request-shapes.mock.ts and a new protocol-endpoints.mock.ts table. The two offline e2e files that bind a port (patched-fetch-headers, refusal-classification) moved there too. The device-login routes left the gateway mock. - createMockContext, HarnessSimulator and the rest of testing-kit.ts are gone. Services are built on a real cordis Context, and lifecycle.test.ts mounts the plugin the way the Loader does (test/support/composition.ts). degradedHost remains only for the degraded-composition diagnostics. - 43 cases that tested only the harness or the mock gateway are deleted. 33 duplicated cases are merged and 16 restatements are deleted; the plan names each one. - Hard-coded catalog counts and vendor ids are now invariants, or a row picked by shape, so a vendor catalog refresh no longer breaks the suite by construction. - The seven largest test files are split by subject, the live suites read their environment through test/support/e2e-env.ts, and the duplicated helpers and hard-coded ses_ ids are removed. - src/utils/session-id.ts, src/opencode-store-contract.ts and the e2e environment reader have their own tests, and src/lifecycle.ts, src/stream-hook.ts and src/fetch-patch.ts have per-file coverage floors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… planes The claim reconciled on `loader/partial-dispose` only for pi-ai's row. A live edit of the plugin's own row's `isolate`, or of a group around either row, swaps a realm map in place with no config run, no restart and no registry change, so the plane stayed where it was while the live `steppedAside()` already disagreed: joining pi-ai's realm left the plane on PiAiAdapter with this plugin's decorations on its turns, and leaving it kept the plane on PlaneAdapter, resolving the route's key in the process-wide realm. Every row's partial dispose now schedules the reconcile, whose give/take restart moves the plane once. Tier B cases, both apply orders: the plugin's row joining and leaving pi-ai's realm, and a group around the plugin's row moving into it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…he commit run On the loader's volatile path the served row's raw config already holds the candidate when its commit run arrives. Once the marker leaves that row nothing the claim takes, `servedRow()` moves to another live pi-ai row (a plane-less one ahead of it, or one declaring a plane of its own), and `claim()` returned before releasing the plane the plugin still held: pi-ai's registration of it, right after the run, met a duplicate. The release is now decided for the row the registration was built from, before the served-row check, and a reconcile is scheduled. Stand-in cases for both neighbour shapes beside the single-row known answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…plugin Settings -> Models -> Fetch on a stepped-aside plane (by realm, or by marker beside an unmarked plane) answers pi-ai's own list on a real host, a catalog-retired id included, while the plugin's own plane drops it (the known answer); dropping the accessor in `installDiscovery` now fails. The meter's realm rule is performed alone: unmarked entries in a realm the plugin does not share, with the realm's token exported, are neither adopted nor replayed, which a disabled realm skip in go-discovery now fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…rue 1.x history The plan's D27 row and route-claim text name the reconcile on every row's partial dispose and the in-run release decided for the row the registration was built from. engineering-notes says the listener strips only the planes it takes. Both READMEs describe what the released 1.x actually did on a Console route: its hidden-route turns failed for want of the Console's token, and the rest carried this plugin's headers and had their key recorded for the meter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
) into claude/vigilant-mendel-bctz9n-p-r2 Brings P-R1's merge of main (#170, the nightly push fix; #159, the catalog refresh to Zen 87 / Go 32) and D27 (the route claim steps aside for a plane entry another plugin owns, by realm or marker) under P-R2. The merged tree is P-R2's own change on top of the parent. Conflicts: - AGENTS.md, the pure-utils line for the plane's decisions. P-R1 added `src/utils/plane-ownership.ts` to the list and the D27 clause (which entries the claim takes and which another plugin owns, read by realm and marker; the meter asks the same questions). P-R2 added the family `auto` resolves to (`AUTO_ENDPOINT_FAMILY`, P-R2) to the plane-model clause. Kept the parent's line and carried P-R2's clause into it. Every other file merged cleanly; src/catalog-data.ts, src/go-limits-data.ts and CHANGELOG.md are the parent's exactly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
P-R2's `endpoint` family (`auto`, `modern`, `legacy`) decides the base of every plane the route claim takes. After P-R1's D27, a plane entry another plugin owns is not taken: pi-ai serves it at its own base. P-R2's pins only read the pure dispatch, so nothing held that the setting stays off such a plane. test/endpoint-family.test.ts gains a tier-A block: under each setting, with one plane marked `x-opencode-plugin` and one of the user's own, a turn on the taken plane reaches the completions cell of the family the setting names, and a turn on the marked plane leaves at the URL pi-ai alone sends (the known answer, measured first on a host without the plugin). A second case shows the move would be visible: that URL is not the legacy cell `endpoint: legacy` would send it to. docs/redirect-planes.md (section 1's endpoint passage, the gate table's row and section 13's keyed reading) and the P-R2 entry in docs/improvement-plan.md say the same: the setting moves only the planes the claim takes, and the keyed probes do not measure a plane another plugin owns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
) into claude/vigilant-mendel-bctz9n-p3 The merged tree is P3's own change (the discoverModels wrapper that complements a served plane's Settings fetch with the catalog) on top of P-R2 with P-R1's D27 and main #170/#159. Conflicts, and how each was resolved: - src/host/route-claim.ts: P3 wrapped restartHolder in a state.restarting marker and added claiming()/heldByPiAi; D27 split restartHolder into restartServed(fiber, key, still) with restartHolder and restartLender over it, and added steppedAside(). Kept D27's split and put P3's restarting marker into restartServed, so claiming() reads engaged while either restart of the served row is in flight. Kept both claiming() and steppedAside() on the returned claim. heldByPiAi now reads heldPlanes (the planes the claim takes) instead of resolvedPlanes, as D27 did at every other site: a plane pi-ai holds for another plugin must not make the claim read stood down for the plane beside it. - src/lifecycle.ts: P3 deleted installModelDiscovery, toDiscovered and the registerModelDiscovery namespaces and hands the wrapper the claim; D27 threaded claim.steppedAside into the stream hook and the old decorator. Kept P3's discovery (installDiscovery(ctx, config, claim)) and D27's stream hook wiring. - src/models-discovery.ts: P3's wrapper and doc kept; D27's decorator parameter dropped with the decorator it changed. The wrapper does not read the step-aside yet: the follow-up commit adds it, red first. - test/models-discovery.test.ts: P3's cases kept; D27's unit case kept, on P3's signature (the claim with a steppedAside accessor) and with P3's answer on the plane the claim takes (pi's row, then the servable catalog ids pi lacks). - docs/improvement-plan.md: D27's settings.describe line; P3's landed discoverModels record, with D27's requirement on it stated as still to meet; P3's landed item 9, with D27's requirement as a bullet. - docs/deep-dive.md: main's catalog name in item 3; P3's item 4 (the native registration item 5 is gone with P3's deletion). - docs/redirect-planes.md: D27's endpoint-family row and P3's host-discovery row, both. - CONTRIBUTING.md: P3's tier A (hostModules: null) and tier B (host-discovery) text, plus D27's realm option. Adapted where the parents meet outside a conflict: - test/host-planes-foreign.test.ts: D27's Fetch case expected the unmarked plane to answer the 1.x merge; under P3 it is pi's ids without the retired one, then every servable, non-deprecated catalog id pi lacks. - test/host-discovery.test.ts: main #170's catalog makes the first Zen gap row a Gemini model (gemini-4-carbon), whose request names the model in the URL path and whose 401 pi reports as PI_AI_ERROR (as the characterization measures); the adopted-gap case reads the model and the finish per protocol. Known red on this tree: D27's discovery step-aside cases (the unit case and the tier-B Fetch case, both orders), since P3's wrapper ignores the step-aside. The next commit makes them green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
D27 made the 1.x discovery decoration answer pi-ai's list unchanged on a plane the route claim steps aside for (a pi-ai row in a credentials realm the plugin does not share, or an entry carrying x-opencode-plugin). P3's discoverModels wrapper, which replaced that decoration, did not read the step-aside: Settings -> Models -> Fetch on such a plane was complemented with catalog models pi-ai cannot serve, and pi-ai's NO_DISCOVERY during a restart was answered with the catalog. The wrapper now reads the claim's steppedAside per call (DiscoveryClaim gains it) before the namespace or the claim's state, and hands such a request to pi-ai untouched: no complement, no merge, NO_DISCOVERY and every other error rejecting as the host sent them. A plane the plugin serves keeps P3's complement. Red first: test/host-planes-foreign.test.ts performs it through the applied plugin in tier B, a marked plane beside an unmarked one and realm-only planes, in both apply orders. On the merged tree all four cases failed at the first stepped-aside fetch (87 rows complemented against pi-ai's own 85), as did D27's existing Fetch case (both orders) and both wrapper cases in test/models-discovery.test.ts; all pass with the change. Each case also checks that the fetch equals the host's own unwrapped answer, that P3's complement would have added ids (non-vacuous), that the taken plane beside a marked one is still complemented, and that during a pi-ai restart the stepped-aside planes reject NO_DISCOVERY while a taken plane answers its catalog. Docs: the improvement plan records P3's D27 requirement as met; the deep dive, README and README.zh-CN say a Console route's Fetch is pi-ai's own list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
On a pi-ai row that resolves credentials in a realm the plugin does not share, D27 leaves every plane entry to pi-ai, declared or not. The discovery wrapper only passed through the planes such a row already declared (the claim's steppedAside), so Settings -> Models -> Fetch on the other plane route, the draft Settings opens when the user adds it, was complemented with catalog ids pi-ai then refuses at save, its rows were rewritten catalog-first, and pi-ai's NO_DISCOVERY during a restart was answered with the catalog. The route claim gains a read-only wouldTake(plane): whether a plain entry of that plane saved on the row it serves would be taken, false only when that row is in a foreign realm (true on a composition with no row to judge, so its fetch keeps the enrich-only merge). The wrapper reads it per call through DiscoveryClaim and hands a plane route asked on the served namespace that the claim would not take to pi-ai untouched, as it does a stepped-aside plane. Red first: test/host-planes-foreign.test.ts adds two tier B shapes, a realm row declaring only one plane with the other fetched, in both apply orders; all four failed on the undeclared plane (87 rows against pi-ai's own 85, and a catalog-first rewrite on Go) and pass with the change, each checking the host's own answer and NO_DISCOVERY in the restart window. test/models-discovery.test.ts pins the wrapper alone (red before the change), and test/route-claim.test.ts pins wouldTake on the stand-in, including the degraded composition. Docs: the improvement plan's P3 and D27 notes and the deep dive say a plane the claim would not take on the served row is pi-ai's answer too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… into claude/vigilant-mendel-bctz9n-p-t2
The merged tree is P-T2's own change on top of P3's tip: src/ is P3's
exactly apart from P-T2's two source edits (models-catalog.ts,
stream-hook.ts), and every case the parents added since the last
restack sits in P-T2's layout. Conflicts, and how each was resolved:
- AGENTS.md: P-T2 points the route claim's stand-in at
test/support/route-claim-stand-in.ts and drops host-harness from the
tier-B entry; D27 adds tier B's realm option and the step-aside to the
route-claim and purge entries. Kept D27's text with P-T2's file names.
- CONTRIBUTING.md: tier A keeps P-T2's case list (host-harness retired);
tier B takes D27's realm option sentence.
- test/support/host.ts: P-T2 files the plugin's row through pluginRow
(id, package, config); D27 adds the realm option. profilePatchText
takes both the row id and the realm label; D27's host-planes-foreign
passes its config as pluginRow: { config }.
- test/catalog.test.ts: #159 restated the shim's counts and the renamed
display name; P-T2 replaced the counts with invariants and picks rows
by shape, so P-T2's side stands (no title is lost: the counts were
assertions inside existing cases, and the priced-differently case was
already P-T2's "agrees about free" plus the seeded stream-hook case).
- test/usage-service.test.ts: #159's display-name change was to a case
P-T2 moved to usage-service-readings, where the name is read off the
allowance row by shape; P-T2's side stands.
- test/stream-hook.test.ts: imports unioned; D27's case uses the file's
static createStreamHook, resolveConfig and AsyncLocalStorage imports
instead of dynamic ones.
- test/fetch-patch.test.ts: D27's "another plugin's request" describe
sits in fetch-patch.test.ts (the credentials part, which holds key
capture), ahead of P-T2's "patchFetch · credentials"; SESSION_HEADER
added to its door import.
- test/route-claim.test.ts: P-T2's split stands. D27's standing-down
assertions merged in place; its listener case moved to the listener
describe of route-claim-listener.test.ts, and its four second-row
cases (with the Neighbours table) to that file's "a second pi-ai row",
on the shared stand-in (FakeFiber imported from it).
Auto-merged parent cases verified in place: go-discovery (D27 describe
on loader-rows' real contexts), models-discovery, purge-service, reload,
plane-profile, endpoint-family, host-discovery, freshness-scope, and the
new plane-ownership and host-planes-foreign files through the door.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
|
Merged P3, which now carries D27, P-R2's follow-up and main's #170 and #159. Every new test now sits in this PR's layout. The branch moved fast-forward to
Live job: Generated by Claude Code |
|
The nightly refresh is #171. I'm porting its follow-ups and merging it up the stack (#113 → #114 → #115 → this PR), which turns the check green.
Generated by Claude Code |
The nightly refresh (#171) regenerated src/catalog-data.ts: Zen 87 -> 88 with gemini-4-hydrogen ("Gemini 4 Hydrogen", @ai-sdk/google, paid), Go unchanged at 32, src/go-limits-data.ts unchanged. Every document and test that pins what moved follows it: - README.md: the routing table's @ai-sdk/google row 9 -> 10, and the stated total 87 -> 88 (both mentions) with its date 2026-11-12 -> 2026-11-13; the catalog tests parse both the table and the sentence. - README.zh-CN.md: the same three changes, kept in step with the English README for the docs-pairing gate and the bilingual prose-total test. - docs/protocol-routing-and-merge.md: every "N of 87" becomes "of 88", the Google row 9 -> 10, and the counts-as-of note names #171 and its date. - docs/redirect-planes.md: Zen "87 served" -> 88, the shapes table's Google Zen cell 9 -> 10, "0 of 87 on Zen" -> 88, and both counts-as-of notes name #171; the listed halves stay the ones #156 recorded, as #159 left them, since the registry was not re-read. - test/catalog.test.ts: the bundled Zen shim's pinned length 87 -> 88. The free count (12), the Responses/Anthropic buckets and the both-planes overlap are unchanged, because the new model is paid, Google and Zen-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… restating them The header still stated the split of an 85-model Zen catalog (28/30/18/8/1); the shipped catalog is 88 (27/32/18/10/1). Restating the counts here is a second copy the refreshes stopped keeping true, so the header now names the SDKs and points to docs/redirect-planes.md section 2, which test/catalog.test.ts holds to the catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The routes module header said the counts are stated once, in docs/redirect-planes.md §2. The README pair and docs/protocol-routing-and-merge.md §1 state them too, each held to the catalog by its own test in test/catalog.test.ts. The header now names §2 as the per-plane table and says the test holds that table, the README's and the protocol doc's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…READMEs The intro's model list and the opencode-responses example named muse-spark-1.3-contributor-free, which the shipped catalog no longer carries. Both READMEs now name muse-spark-1.3, a served Zen @ai-sdk/openai model with the example's context window, output limit and modalities, and the example's display name drops "Free". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The refresh adds the Zen model gemini-4-hydrogen (@ai-sdk/google), so Zen
goes from 87 to 88 served models and its Google count from 9 to 10; Go and
the Go limits are unchanged. Its port on main also replaces the retired
muse-spark-1.3-contributor-free id in both READMEs and stops the routes
module header from restating per-SDK counts. Conflicts, resolved by keeping
P-R1's text and carrying the refresh's facts into it:
- README.md, README.zh-CN.md: P-R1 rewrote the protocol table as three
columns (no "served from" column, Mistral filed as Chat Completions) and
replaced the declare-the-route-yourself section and its YAML example with
"Settings stays the host's". Kept P-R1's table with the Google row at 10,
and P-R1's section, so the example's muse-spark id change has no passage
to land in. The 88 / 2026-11-13 total sentence and the intro's
muse-spark-1.3 merged cleanly from the port.
- docs/protocol-routing-and-merge.md: P-R1 moved the endpoints to the
modern family ("shown on the default (modern) family", /inference/...
paths) and refiled Mistral as Chat Completions; the port moved the counts
to N of 88, Google to 10, the date to 2026-11-13 and added #171 to the
refresh history. Kept P-R1's paths and rows with the port's counts, date
and history clause.
- docs/redirect-planes.md: P-R1 rewrote the plane table's credential
column (the entry's apiKeyEnv, then pi's OPENCODE_API_KEY) and refiled
Mistral as openai-completions in the shape table; the port moved Zen to
88 served and Google to 10. Kept P-R1's columns and protocol with the
port's counts; the dated prose and the 0 of 88 sentence merged cleanly.
- src/responses-routes.ts: P-R1 deleted the module (its table lives in
src/utils/protocols.ts); the port only rewrote its header comment. Kept
the deletion.
src/catalog-data.ts and test/catalog.test.ts (Zen length 88) merged without
conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The refresh's gemini-4-hydrogen is a Zen @ai-sdk/google reasoning model whose effort list is low/medium/high/xhigh. xhigh is outside Google's four thinking levels, so usesCatalogThinkingLevels answers false for it, and the bundled-catalog oracle in test/thinking-levels.test.ts, which expected every Google reasoning entry to be level-only, failed on it: the flip its comment anticipates for "a future Google entry with ... an xhigh". The oracle now names the exception by id, with the value that excludes it, and checks that the exception is still a Google reasoning entry the catalog lists with that value in its effort list, so a renamed or reshaped entry fails rather than leaving a stale exception. Every other entry is held to the same expectation as before, and hydrogen is still asserted (false). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…claude/vigilant-mendel-bctz9n-p-r2 P-R1 took the nightly catalog refresh (#171: Zen 87 -> 88, adding the @ai-sdk/google reasoning model gemini-4-hydrogen; Go unchanged at 32) and followed it through the README pair, docs/protocol-routing-and-merge.md, docs/redirect-planes.md, test/catalog.test.ts and the bundled-catalog oracle in test/thinking-levels.test.ts. Resolution: no conflicts. docs/redirect-planes.md is the one file both sides touched; git merged it cleanly, because P-R1 changed the §1 counts paragraph, the §2 stamp, the Google row and the 0-of-88 sentence, while P-R2 changed the §1 endpoint-family paragraph, later sections and §13, and the hunks do not overlap. The merged tree is P-R2's own change (b2d5a86..d563282) applied on top of ce889ef unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…claude/vigilant-mendel-bctz9n-p3 Brings in the nightly catalog refresh (#171: Zen 87 -> 88, adds gemini-4-hydrogen; Go unchanged at 32) as P-R2 carries it, with P-R1's follow-up of the docs and tests. Resolution: no textual conflicts. git auto-merged README.md, README.zh-CN.md, docs/protocol-routing-and-merge.md, docs/redirect-planes.md and test/catalog.test.ts, where P3's own edits and the refresh's count and list edits touch different lines. The merged tree is exactly P3's own change (d563282..8b2fa44) applied on top of 65b8443: the two diffs differ only in blob ids and in the one context line of docs/protocol-routing-and-merge.md that carries the Zen count (87 -> 88). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…aude/vigilant-mendel-bctz9n-p-t2 Brings the nightly catalog refresh (#171: Zen 87 -> 88, adds gemini-4-hydrogen on @ai-sdk/google; Go unchanged at 32) through P3 into P-T2's test layout. Resolution: - test/catalog.test.ts (the one conflict): P3 re-pinned the Zen shim length from 87 to 88; P-T2 had replaced that pinned count with an invariant (non-empty, ids unique), and the per-SDK counts are compared with the docs by catalog-docs.test.ts. Kept P-T2's invariant; the file is P-T2's text unchanged. - docs/protocol-routing-and-merge.md, docs/redirect-planes.md: merged cleanly. P3's count refresh (88 Zen, Google 10, 2026-11-13, #171) sits beside P-T2's renamed guard references (catalog-docs.test.ts, catalog-routing.test.ts, the host-planes*.test.ts files). - README.md, README.zh-CN.md, src/catalog-data.ts: P3's side only (the generated file and both READMEs in step). - test/thinking-levels.test.ts: P3's side only (the gemini-4-hydrogen xhigh exception); P-T2 did not move or touch this file, so it stays where it is in P-T2's layout. The parent added no new test titles (git diff 8b2fa44 285aeaf -- test/ adds no it/test/describe line), so nothing needed relocating. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
main is P-R1 (#113) squashed; its tree equals the P-R1 head this branch already merged (ce889ef), so the merge changes no file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…to claude/vigilant-mendel-bctz9n-p3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… claude/vigilant-mendel-bctz9n-p-t2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
main is P3 (#115) squashed; its tree equals the P3 head this branch already merged (5faf253), so the merge changes no file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Phase P-T2 of docs/improvement-plan.md, stacked on #115.
This phase changes only tests and docs. No file under
src/is touched. Section 10 of the plan lists each moved, merged and deleted case by name.What changed
testing-kit.ts(auth-api-key,client-headers-affinity, sixendpoint-*,response-errors) are merged intotest/mock/request-shapes.mock.tsand a newtest/mock/protocol-endpoints.mock.ts. That file is oneit.for(PROTOCOLS)table, with models taken from the catalog. The two offline e2e files that bind a port (patched-fetch-headers,refusal-classification) moved into the mock suite and are no longer gated. The device-login routes left the gateway mock, because this build has no login.createMockContext,HarnessSimulatorand the rest oftesting-kit.tsare gone. Services are built onnew Context().lifecycle.test.tsmounts the plugin the way the Loader does (test/support/composition.ts,mountPlugin) and checks after every case that no fetch patch is left. The one case that leaves one is markedKNOWN_DEFECT P6.degradedHoststays only for the degraded-composition diagnostics. The named exceptions (route-claim stand-in, thellmseam objects, the jsdom client tests) are documented in AGENTS.md, CONTRIBUTING.md and plan §10.harness-context-remotes(4) andharness-loader-versions(4), which testedHarnessSimulator.gateway-contract.mock.ts(10) and two mock-only describes in request-shapes (7), which made raw fetches with no plugin. Five of the seven cases inhost-harness(the other two moved tohost-planes). Thirteen cases in the moved files that asserted only the mock's verdict.usage.test.tscases repeatedgo-discovery, and fourGoUsageServicecases repeatedusage-service-readings/-failures.free-tier-contract's big-pickle case and lifecycle's usage-registration case were also restatements.is_freerule, a paid row per served protocol, and Go-only/Zen-only ids computed as set differences. A case that needs a row picks it by shape (findCatalogRow,freeZenModel, the first priced row) and reads its expected value off that row. A vendor refresh can then retire any one id without failing an unrelated case.test/support/.test/support/e2e-env.ts(readE2eEnvironment,E2E) is now the only place the live suites read their environment. The offline sweep indispatch-levelsbecame a unit file, because it binds no port.ses_idsurlOf,routeIds,toolNamesOfand others) are hoisted into one place each. Everyses_id that the gateway, the mock or a live probe reads is now derived. This includesprotocol-routing's id, which had 23 characters, so the gateway refused it on every keyed case.src/utils/session-id.ts,src/opencode-store-contract.tsand the e2e environment reader have their own test files.src/lifecycle.ts,src/stream-hook.tsandsrc/fetch-patch.tshave per-file floors, and their unreachable branches are named.Final counts: 108 unit files with 1437 tests, and 6 mock files with 72 tests.
Behaviour changes
None. This is a pinning phase.
Review
The phase had three review rounds with three lenses each (correctness, tests, rules). Before round 1, 28 findings carried over from the phase's earlier review were checked again: 24 were upheld and fixed. Each round's upheld findings were fixed before the next round ran, and none were declined.
Verification
pnpm run checkvp test runvp test runshuffled, seeds 1, 42 and 1234vp test run --config vitest.mock.config.tspnpm run buildnode --experimental-strip-types scripts/check.tscheck passed, exit 0vp test run --coveragevp test run, shuffled with seed 7catalog freshnessfails on every branch: the vendor renamed a Go model, and the regenerated tables arrive with the nightly refresh.Rebased onto main 1.9.2 and the latest hosts
The branch is now this PR's own change on its parent (P3, itself rebased onto
main1.9.2): one rebase commit plus the adaptation commits, with the pre-rebase tip kept as a second parent so the branch moved forward without a force-push.test: restructure the suite: the rebase commit, the whole pre-rebase change replayed on P3test: the route claim's stand-in models both cordis cores in one placedocs: record P-T2's rebase onto P3 and where the new core's cases landedtest: the refusal oracle classifies a 403 through the installed pi-aitest: the refusal oracle's control reads the probe's words, and logLines states the new core's levels(review fix)docs: place the route claim's stand-in in its support file and record P-T2's oracle adaptation(review fix)Dropped because the parent already has it. The PR's
route-claim · a claim that never engagesdescribe. P3, on the new core, replaced it with the fourSettings' fetch follows the claim's engagementcases, which are kept.Host adaptation.
nextPrerelease,NEXT_ALPHA,withCut,PATCHED,ALPHA_NEXT) and moved, exported, intotest/support/host-drift-fixture.ts. The parent's next-dist-tag and graduation cases are kept. The drift CLI's cases stay intest/mock/host-drift-cli.mock.ts.test/mock/refusal-classification.mock.ts) reads the code the installed pi-ai finishes a turn with, not one bridge's source, which the 0.2.2-rc.2 bridge (a leading-HTTP-status classifier) broke. The probe's words read asINVALID_REQUESTat 422, a status no classifier branch names, and asAUTHat 403.logLinesdocuments what the default exporter keeps on the new core: up toinfo, neverdebug.test/support/route-claim-stand-in.tsmodels both cordis cores (the old core'spiFiberandoldRun, the new core's call shape) in one place. The split route-claim files keep P3's case bodies, and the new core's call-shape cases live intest/route-claim-listener.test.ts.test/host-planes.test.tskeeps P3's two new cases (the row disposed during the mount's restart, and pi-ai's row removed in tier B) next to the PR's split intohost-planes-configandhost-planes-credentials.test/host-waterfall.test.tskeeps P3's new-core cases with the sharedrouteIds.dsh-llm-pi-ai0.2.2-rc.2 (cordis 4.1.0) and alpha 0.2.2-alpha.3 (cordis 4.0.6-alpha.2), both on the new cordis core.Measured cells (published host binary with the probe):
internal/configdispatchesfiber) shapeconfig) shape; the claim servedThe two skips in every cell are
[HOST_LIMITATION]rows: the host's pi sends the Go Gemini level model a thinking budget (reported upstream, D26), and the config-editor save row, which skips because that limitation is absent (saves work). The legacy cell ran with a recorded override that lifts only the record-version and family-floor checks, reverted afterwards; its third skip is the in-process match row, because npm resolved pi 1.5.0 rather than the record's 1.5.2.Gate on the new head.
vp check: 0 errors, 12 warnings (limit 18). Unit: 125 files, 1891 passed, 1 skipped (a[HOST_LIMITATION]row on alpha). Shuffled seeds 1 and 42, and Node 24 with seed 7: the same. Mock: 7 files, 83 passed. Build and bundle gate:check passed. Coverage: statements 98.35%, branches 95.77%, functions 98.55%, lines 98.35%. On rc 0.2.2-rc.2: the same unit and mock counts. Floor runs (pi 1.3.0) on both channels over the host, route-claim, host-versions and harness-channel files: 18 files, 272 passed, 1 skipped.What remains. The two
[HOST_LIMITATION]rows wait on the upstream fixes (D26), and theKNOWN_DEFECT P6fetch-patch case waits on P6. The base PR (#115) lands first.Generated by Claude Code