Skip to content

feat: per-child provider/model on dispatched workers (collab sessions P0) - #247

Merged
saucam merged 1 commit into
mainfrom
feat/collab-p0-child-provider
Jul 26, 2026
Merged

feat: per-child provider/model on dispatched workers (collab sessions P0)#247
saucam merged 1 commit into
mainfrom
feat/collab-p0-child-provider

Conversation

@saucam

@saucam saucam commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

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 no providerId, 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

File Change
models.ts resolveModelIdForProvider() + CLAUDE_PROVIDER_ID
store.ts provider/model columns on dispatch_tasks (inline CREATE + additive ALTER)
dispatch.ts Dispatcher.enqueue carries and audits the selection
fleet.ts fleet_spawn takes provider/model; new fail-closed resolveBackend dep
session-manager.ts spawnWorker honors the selection + claim-time re-validation
session.ts model default resolved against the effective provider

Three 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.defaultModel is a global, but resolveModelId expands against the Claude catalog — so opus on a gemini session became claude-opus-4-8 inside a Google API call. Claude-shaped values now resolve to null on 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 used registry.defaultId. Those differ whenever the caller omits a provider, and coincide today only because createDefaultProviderRegistry hardcodes "claude". Both now use the effective id.

Verification

  • Full suite 1842 pass / 0 fail (1827 baseline, +15), typecheck + biome + bun build all clean.
  • New src/tests/dispatch-per-child-provider.test.ts drives a real two-backend ProviderRegistry rather than _testProviderFactory — that injects one mock into every session and bypasses Session#createProvider, which is the path under test.
  • Mutation-checked, so the tests aren't vacuous: 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 a real failure mode, not a hypothetical.

Scope notes

🤖 Generated with Claude Code

… 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>
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@saucam
saucam merged commit f2d81c6 into main Jul 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants