Repository navigation
fix(core): run V2 turns only after the location's plugins boot - #350
Merged
Merged
Conversation
) Plan for swxtchio/swx-abbe#1952. Root cause: the V2 Config layer (packages/core/src/config.ts) reads only the global config directory and discovered project files. It ignores OPENCODE_CONFIG and OPENCODE_CONFIG_CONTENT. The jail supplies its whole config through those two variables (Dockerfile.jail sets OPENCODE_CONFIG, opencode_server.py renders OPENCODE_CONFIG_CONTENT), so the V2 catalog never sees the swxtch-egress provider or a configured default model. V1 (packages/opencode/src/config/config.ts) reads both, which is why V1 turns run on the egress provider. Change: - Config takes explicit file and content sources, defaulting to Flag.OPENCODE_CONFIG and Flag.OPENCODE_CONFIG_CONTENT, with a layerWith seam for tests (mirrors Global.layerWith and upstream's Config.Options file/content from 43c0838). - Precedence matches V1 and upstream: global directory, then the OPENCODE_CONFIG file, then project files, then project .opencode directories, then OPENCODE_CONFIG_CONTENT last. - V1-format sources migrate through the existing loader path. Tests: - Config loader orders explicit file and content entries. - A jail-shaped V1 config supplied as content reaches the catalog and SessionRunnerModel resolves the named swxtch-egress model to an OpenAI-compatible route at the proxy URL. Claude-Session: https://claude.ai/code/session_01HGGfNzV3BHB7W49PTT9MVp
The V2 Config layer read only the global config directory and discovered project files, ignoring OPENCODE_CONFIG and OPENCODE_CONFIG_CONTENT. The OpenCode jail supplies its whole config through those variables, so the V2 runner's catalog never held the swxtch-egress provider or the configured default model: a named egress model failed with ModelUnavailableError and an unnamed one fell to another provider (swxtchio/swx-abbe#1952). Config now takes explicit file and content sources, defaulting to the two flags, and orders them as V1 does: global directory, explicit file, project files, .opencode directories, then inline content. V1-format sources migrate through the existing loader path. Config.nodeWith names the sources directly for tests. Tests drive the real loader: precedence of every source, and the jail's V1 config (placeholder file plus rendered content) through the config provider plugin and catalog to SessionRunnerModel, for a named model and for the configured default against a competing provider. Claude-Session: https://claude.ai/code/session_01HGGfNzV3BHB7W49PTT9MVp
Review round 1: with the env-config fix alone, a location-only V2 session in the jail (create_session_v2 names no model, and the jail config sets none) still resolved a credential-free catalog model such as opencode/hy3-free ahead of the config-declared swxtch-egress model, so egress turns still failed. V1's Provider.defaultModel takes its default from the config-declared providers when no model is configured. The V2 catalog now does the same: the config provider plugin records the declared providers in config order, and Catalog.model.default picks the newest available model of the first one that has any, before its newest-available fallback. An explicit session model or configured `model` still wins. The preference registers on the internal catalog under the plugin's scope, because the public plugin catalog API has no such hook. Tests: the jail path through the production location runtime (every internal plugin, the recorded models.dev corpus, the jail config via OPENCODE_CONFIG and OPENCODE_CONFIG_CONTENT) resolves a location-only session to the egress model; and the preference skips an unavailable declared provider and beats a newer undeclared model. Both are red without the preference. Claude-Session: https://claude.ai/code/session_01HGGfNzV3BHB7W49PTT9MVp
A Location boots its internal plugins in a forked fiber, and nothing on the turn path waited for it. The jail's adapter creates a session and prompts it at once, so the drain that boots the Location starts its first turn while the catalog is still empty or only partly built. On the jail image running v1.18.32-swxtch.774d90c7 the first turn failed with ModelNotSelectedError (or ModelUnavailableError when the egress model is named), or was sent to a bundled model such as nano-gpt/..., and the egress provider received nothing. The internal plugin layer now exposes a readiness signal that completes when its boot ends, whether it loaded every plugin or failed, and every runner turn waits on it before it selects the agent, model, tools and guidance. Refs swxtchio/swx-abbe#1952 Claude-Session: https://claude.ai/code/session_01HGGfNzV3BHB7W49PTT9MVp
Claude-Session: https://claude.ai/code/session_01HGGfNzV3BHB7W49PTT9MVp # Conflicts: # packages/core/test/location-layer.test.ts
Author
|
Session ID: 34738ff3-3dcf-4587-8fde-7c58d5478796 |
brentyates-swx
marked this pull request as ready for review
October 23, 2026 18:38
This was referenced Oct 23, 2026
brentyates-swx
added a commit
that referenced
this pull request
Oct 23, 2026
* docs(release): plan the cd3f0ce source-pinned release cut Records the pickup identity and ancestry for source cd3f0ce (#350 on top of #347) and the execution plan, including Abbe's jail V2 egress rig as a publication gate on the unpublished packaged bytes. Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1 * chore(release): record v1.18.32-swxtch.cd3f0ce2 native, jail-rig and publication evidence Adds the published release body and the raw arm64 (vm14-001) and x64 (Actions run 40015678366) build and probe observations, the lease receipts, the focused test runs, the Abbe jail V2 egress rig run on the unpublished arm64 candidate, and the publication read-back and downloaded-byte checks. Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1 * docs(release): record cd3f0ce release evidence, delivery and changeset entry Adds RELEASE-CUT-4-EVIDENCE.md and the raw-evidence index, the SaaS wake-log append receipt and peer outbox record, the unestablished Abbe B-16.10 recipient, and the #351 CHANGESET entry. Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1 * docs(release): record the Abbe handoff as delivered by firstmate Firstmate relayed the cd3f0ce release notice to Abbe through its peer channel after this task's own sends were refused. The evidence now cites and retains that relay, noting that it points to the published checksum companions rather than quoting the digests. Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1 * docs(release): record Abbe's acknowledgment and bump-brief citation as the handoff read-back Abbe ruled that peer homes never write each other's runtime state, which withdrew the task wake-log append. The evidence now records the two read-back receipts Abbe named: acknowledgment p1792786679-2709706-0cf3 and the bump brief's verbatim citation of the relay, plus the ruling's proof. Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1 * docs(release): complete the cd3f0ce delivery receipt chain Appends the delivery record to this task's producer wake log and keeps its read-back receipt and verbatim copy. Records firstmate's disposition that the Abbe captain's ruling waived the Abbe task wake-log append, and keeps the rerun relay identity check against the archived and delivered copies. Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1
brentyates-swx
added a commit
that referenced
this pull request
Oct 24, 2026
* docs(release): plan the d7f7888 source-pinned release cut Records the pickup identity and ancestry for source d7f7888 (#354 on top of #350 and #347), the brief's #353 discrepancy, and the execution plan: x64 via the release-candidate job, arm64 and Abbe's jail V2 egress rig (with the compact mode) on vm14-001 under the lease, as the publication gate. Claude-Session: https://claude.ai/code/session_013Se6FG9EEPtmFRtg1hLDMm * chore(release): record v1.18.32-swxtch.d7f78881 native, jail-rig and publication evidence Adds the published release body and the raw arm64 (vm14-001) and x64 (Actions run 40052511379) build and probe observations, the three lease laps' receipts, the focused test runs, Abbe's jail V2 egress rig in all four modes (including compact) on the unpublished arm64 candidate, with the two setup-failed laps kept as found, and the publication read-back and downloaded-byte checks. Claude-Session: https://claude.ai/code/session_013Se6FG9EEPtmFRtg1hLDMm * docs(release): record d7f7888 release evidence and changeset entry Adds RELEASE-CUT-5-EVIDENCE.md and the raw-evidence index, and the #357 CHANGESET entry. Records the brief's #353 discrepancy, the three lease laps, the arm64 jail rig pass in all four modes with the x64 caveat, why the V2 turn modes' completed waits say nothing about #352, and that delivery is firstmate's. Claude-Session: https://claude.ai/code/session_013Se6FG9EEPtmFRtg1hLDMm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs swxtchio/swx-abbe#1952. Follows #347, which is merged as
774d90c7and published asv1.18.32-swxtch.774d90c7.Problem
On the published
v1.18.32-swxtch.774d90c7, V2 turns in the OpenCode jail still never reachswxtch-egress. Abbe measured this at the jail boundary (swxtchio/swx-abbe#1952, 2026-10-23):ModelNotSelectedError.ModelUnavailableError./waitanswers 503.Cause
A Location loads its internal plugins (config, providers, agents, models.dev) in a background fiber, and nothing on the turn path waited for that. The jail's adapter creates a session and prompts it at once, so the drain that boots the Location runs its first turn while the catalog is still empty or only partly built. Depending on timing, the first turn:
ModelNotSelectedError);ModelUnavailableError); ornano-gpt/...orvivgrid/....A turn a few seconds later does reach egress. #347's tests waited for the catalog before resolving, so they hid the race.
Fix
PluginInternalnow provides a service with areadysignal. It completes when the background boot ends, whether the boot loaded every plugin or failed.SessionRunnerLLMwaits forreadyat the start of every turn, before it selects the agent, model, tools and guidance, since the same boot fills all of them.PluginInternal.readyLayeris the seam for tests and embedders whose Location services are already loaded. The runner tests now use it.Upstream has the same race and no fix. Fork-only.
Proof at the jail boundary
The rig:
opencode-variant-jail:b16-10-774d90c7-arm64, which isjail/Dockerfile.jailat swxtchio/swx-abbe#1951 head03d7e234.docker runarguments fromopencode_container_spawn.py:--read-only, the tmpfs mounts,OPENCODE_CONFIG=/jail/opencode.json,OPENCODE_DISABLE_MODELS_FETCH=1, andserve --pure --print-logs.--internalDocker network, with a fake OpenAI-compatible provider where the egress proxy listens.opencode_client.pysends them:POST /api/sessionwith onlylocation(or withmodel), then/promptwithidandprompt, then/wait.774d90c7ModelNotSelectedErrorwith a 503 wait ×2, a bundled model with transport failure ×1ModelUnavailableError×3accounts/fireworks/models/deepseek-v4p1-flashwithBearercapabilityUnit test
The test is in
packages/core/test/location-layer.test.ts: "runs a location-only V2 turn admitted while its location boots on the jail's egress model". It follows the jail's real path:OPENCODE_CONFIGwith placeholders, and the renderedOPENCODE_CONFIG_CONTENT);SessionExecutionLocal, with a session created with only a location, then prompted and resumed immediately, with no waiting;Without the fix, the turn was sent to
vivgrid/gpt-5.6-luna(red). With the fix it goes toswxtch-egressat the proxy URL. The earlier jail resolution test now waits onreadyinstead of polling the catalog.Focused runs, all with no provider keys in the environment:
packages/core:test/session-runner*.test.ts,test/location-layer.test.ts,test/catalog.test.ts,test/config/,test/plugin/andtest/effect/layer-node/, 451 pass.packages/opencode:test/server/httpapi-session.test.ts(78 pass) andhttpapi-wait-lifecycle.test.ts(1 pass).bun typecheckis clean.Abbe side
No producer change is needed. A session created with only a location, as
create_session_v2sends it, and a session that names the egress model both reach egress.A turn now waits for its Location's plugin boot. If an external plugin's boot hangs (none in the jail, since it runs
--pure), the turn waits until the client deadline or an interrupt, instead of running on a partial catalog.🤖 Generated with Claude Code
https://claude.ai/code/session_01HGGfNzV3BHB7W49PTT9MVp