feat: per-child provider/model on dispatched workers (collab sessions P0) - #247
Merged
Conversation
… P0) P0 of docs/collaborative-session-design.md §11: a dispatched worker can now run on a chosen non-default backend, so a collaborative session's roles can sit on different vendors. Before this, spawnWorker() passed no providerId, so every dispatched worker silently landed on the daemon default regardless of intent. - models.ts: resolveModelIdForProvider() + CLAUDE_PROVIDER_ID. Model defaults arrive from provider-agnostic places (config.session.defaultModel, a role override, a dispatch task) but resolveModelId expands aliases against the Claude catalog — so "opus" on a gemini session became claude-opus-4-8 in a Google API call. Claude-shaped values now resolve to null on other backends and the provider picks its own default; provider-native ids pass through. - store.ts: provider/model columns on dispatch_tasks. Declared inline on the CREATE for fresh databases plus an additive ALTER for existing ones; NULL keeps the pre-collaboration "daemon default" meaning. - dispatch.ts: Dispatcher.enqueue carries the selection and audits it. - fleet.ts: fleet_spawn takes optional provider/model, validated fail-closed through a new resolveBackend dep BEFORE the task is queued, so the conductor can correct itself in the same turn instead of burning a task attempt. The conductor prompt now teaches deliberate cross-backend spawning. - session-manager.ts: spawnWorker honors task.provider/task.model, and re-validates the provider at claim time. ProviderRegistry.resolve() warns and falls back to the default rather than throwing, and the API backends register only with their env key present — so a task that outlived a restart could run on the wrong vendor while reporting success. It now fails terminally, alongside the existing workdir re-check. - session.ts: resolve the model default against the provider the session will actually be built from, not the requested id. Those differ whenever the caller omits a provider; they coincide today only because the built-in registry hardcodes claude as its default. Tests: new src/tests/dispatch-per-child-provider.test.ts drives the real two-backend registry rather than _testProviderFactory (which injects one mock into every session and bypasses Session#createProvider — the path under test). Mutation-checked: reverting the wiring fails 4 of the 8 new tests, and the unregistered-provider task comes back "running", confirming the silent wrong-vendor spawn is real. Plus store round-trip, fleet tool, and resolver unit coverage. Full suite 1842 pass / 0 fail, typecheck + biome + build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yash Datta <yd2590@columbia.edu>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
rsharath
approved these changes
Jul 26, 2026
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.
P0 of
docs/collaborative-session-design.md§11, off the design merged in #244.Exit criterion: a dispatched worker runs on a chosen non-default backend. Met and proven — see Verification.
Before this,
spawnWorker()passed noproviderId, so every dispatched worker silently landed on the daemon default no matter what was intended. This makes the backend a per-child property, which is what lets a collaborative session put its roles on different vendors.What changed
models.tsresolveModelIdForProvider()+CLAUDE_PROVIDER_IDstore.tsprovider/modelcolumns ondispatch_tasks(inline CREATE + additive ALTER)dispatch.tsDispatcher.enqueuecarries and audits the selectionfleet.tsfleet_spawntakesprovider/model; new fail-closedresolveBackenddepsession-manager.tsspawnWorkerhonors the selection + claim-time re-validationsession.tsThree findings beyond the stated scope
1.
ProviderRegistry.resolve()doesn't throw on an unknown id — it logs a warning and falls back to the default. Combined with the API backends registering only when their env key is present, a task naming a provider that no longer registers (any restart where a key went missing) would have run its worker on claude while reporting success. Now re-validated at claim time and failed terminally, next to the existing workdir re-check.2. Claude aliases leaked across backends.
config.session.defaultModelis a global, butresolveModelIdexpands against the Claude catalog — soopuson a gemini session becameclaude-opus-4-8inside a Google API call. Claude-shaped values now resolve tonullon other backends (provider picks its default); provider-native ids pass through untouched.3. Latent inconsistency in
session.ts. Model resolution keyed off the requested provider id while construction usedregistry.defaultId. Those differ whenever the caller omits a provider, and coincide today only becausecreateDefaultProviderRegistryhardcodes"claude". Both now use the effective id.Verification
bun buildall clean.src/tests/dispatch-per-child-provider.test.tsdrives a real two-backendProviderRegistryrather than_testProviderFactory— that injects one mock into every session and bypassesSession#createProvider, which is the path under test.running— confirming the silent wrong-vendor spawn is a real failure mode, not a hypothetical.Scope notes
fleet_senddeliberately takes no provider: it targets an existing session that already owns its backend.MODEL_CATALOGis stale (opus→claude-opus-4-8, "Opus 4.8"). build(deps): bump @anthropic-ai/claude-agent-sdk to 0.3.220 for Claude Opus 5 #246 bumps the SDK for Opus 5 but touches onlypackage.json/bun.lock. Left alone — changing whatopusresolves to affects every session, well outside P0. Follow-up.NULLpreserves the pre-collaboration "daemon default" meaning for queued tasks written by an older daemon.collaborationonSessionCreateMsg), which triggers thecodeoid-uicrates/codeoid-protocollockstep. Non-Claude orchestrators remain Support non-Claude orchestrators for collaborative sessions (mountable fleet MCP) #245.🤖 Generated with Claude Code