Skip to content

feat: mid-session provider switching with provider-owned history seeding - #133

Merged
saucam merged 2 commits into
mainfrom
feat/provider-switch
Jul 9, 2026
Merged

feat: mid-session provider switching with provider-owned history seeding#133
saucam merged 2 commits into
mainfrom
feat/provider-switch

Conversation

@saucam

@saucam saucam commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What

The "clean design" for harness switching: a generic switch loop in the daemon plus provider-owned seeding fidelity/provider pi mid-conversation, /provider claude to come back, same session id / scrollback / transcript / identity throughout.

The generic loop (Session.switchProvider)

  1. Fail-closed registry resolve (unknown id → invalid_request naming the available backends; same-id → ack no-op)
  2. Rejected while a turn, tool approval, or provider dialog is in flight — interrupt first (a half-executed tool batch is not a switchable state)
  3. Serialized on the session's send chain, so a racing prompt completes against the OLD backend and everything after runs on the NEW one
  4. Teardown → fresh backing id (an incoming backend must never try to resume the outgoing one's native state — a Claude session id means nothing to pi and vice versa) → model reset to the incoming backend's default (model ids are provider-specific) → rebuild via the ProviderRegistry
  5. Audited + announced in the transcript with structured metadata (provider.switched, from/to/seeded); transcript meta persists the new providerId so restarts resume on the right backend

Provider-owned fidelity (seedFromHistory?)

New optional on the provider interface, called once on the incoming backend with the session's canonical history:

  • Stateless backends (gemini, openai) don't implement it — they consume TurnOpts.history natively every turn, so switching to them is inherently correct
  • Warm backends (claude, pi) prepend a rendered transcript (renderHistorySeed: turn-by-turn with flattened tool calls, oldest turns elided past ~24k chars with a note) to their first post-switch prompt — the same mechanism rotation's task anchor uses
  • Best-effort by contract: a throwing seed degrades to an unseeded switch with seeded: false in the announcement — never a wedged session

The fidelity contract is stated where users see it (protocol docs, docs/providers-pi.md, the transcript message): the incoming backend receives a faithful transcript, not a native continuation — tool_use structures, prompt-cache state, and extension state don't cross.

Exposure

session.set_provider verb (session-config trust class, same as set_model) and /provider <id> in the shared slash layer (web gets it via dispatchSlash; TUI pickers ride the existing auth.ok.providers advertisement as follow-up).

Tests

  • Session lifecycle over a two-backend mock registry: teardown/rebuild, canonical history delivered to the incoming provider, backing-id re-mint, model reset, broadcast metadata, next-turn-on-new-backend, unknown-id fail-closed, same-id no-op, mid-approval rejection, throwing-seed degradation
  • Manager wire coverage: scope, ownership, unknown id, happy path
  • renderHistorySeed units: envelope/tool rendering, empty history, oldest-first truncation with elision note
  • pi end-to-end: fake-pi echoes the received prompt, proving the seed actually reaches pi and is one-shot
  • /provider parse + dispatch; schema fidelity (compile-time exhaustiveness held)
  • 1183 daemon/protocol/core tests + 130 web tests green; tsc + biome clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new /provider <id> command to switch the active backend during a live session.
    • Preserves the session id, transcript, and scrollback while carrying prior conversation context into the new backend.
    • The session now announces provider switches with structured transcript metadata.
  • Bug Fixes

    • Switching now safely rejects invalid providers and mid-turn changes.
    • If history seeding fails, the session still switches and falls back gracefully.
    • The active model resets to the new provider’s default after a switch.

session.set_provider + /provider <id>: swap a live session's backend
(claude ⇄ pi ⇄ gemini ⇄ openai) keeping the session id, scrollback,
transcript, and identity.

- Generic switch loop in Session.switchProvider: fail-closed registry
  resolve, rejected mid-turn (pending approvals/dialogs included),
  serialized on the send chain against racing prompts, teardown → fresh
  backing id (incoming backend never resumes the outgoing one's native
  state) → model reset to the new default → rebuild via the registry
- Provider-owned fidelity: new optional seedFromHistory(history) on the
  provider interface. Stateless backends no-op (TurnOpts.history is
  native for them); ClaudeProvider and PiProvider prepend a rendered
  transcript (canonical renderHistorySeed, newest-turns-kept truncation)
  to their first post-switch prompt. Best-effort by contract: a seed
  failure degrades to an unseeded switch, never a wedged session
- Every switch audited + announced in the transcript with structured
  metadata (from/to/seeded); transcript meta persists the new providerId
  for restart resume
- Session retains provider-construction inputs (registry, fleet,
  compression, onModels) so #createProvider is callable post-constructor
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@saucam, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 433a91c7-e2f7-47e7-b42f-bfdb6424ac9e

📥 Commits

Reviewing files that changed from the base of the PR and between bf7e851 and fe86946.

📒 Files selected for processing (3)
  • src/daemon/session-manager.ts
  • src/daemon/session.ts
  • src/tests/session-provider-switch.test.ts
📝 Walkthrough

Walkthrough

Adds a session.set_provider protocol message and /provider slash command enabling mid-session backend switching. Session retains provider construction inputs to rebuild providers on switch, providers gain an optional seedFromHistory hook that prepends a rendered <conversation-history> transcript to the next prompt, and SessionManager routes the new message with scope/ownership enforcement. Includes tests and docs.

Changes

Mid-session provider switching

Layer / File(s) Summary
Protocol schema and message types
packages/protocol/src/types.ts, packages/protocol/src/schemas.ts, packages/protocol/src/schemas.test.ts
Adds SessionSetProviderMsg type, sessionSetProviderSchema, wires both into the ClientMessage/clientMessageSchema unions, and adds a round-trip sample test.
/provider slash command
packages/core/src/slash.ts, packages/core/src/slash.test.ts
Adds provider SlashCommand variant, parseSlash case-insensitive id parsing, dispatchSlash sending session.set_provider, and matching tests.
History seed rendering
src/daemon/providers/canonical.ts, src/tests/pi-translate.test.ts
Adds HISTORY_SEED_MAX_CHARS and renderHistorySeed() to build a bounded <conversation-history> transcript from canonical turns, with unit tests for formatting, empty history, and truncation.
Provider seedFromHistory hooks
src/daemon/providers/interface.ts, src/daemon/providers/claude/index.ts, src/daemon/providers/pi/index.ts, src/daemon/providers/mock/session-provider.ts, src/tests/fixtures/fake-pi.ts, src/tests/provider-pi.test.ts
Adds optional seedFromHistory hook to AgentProvider and implements it in Claude/Pi/Mock providers, storing a pending seed prepended to the next prompt; adds a fake-pi echo branch and integration test.
Session rebuild and switch wiring
src/daemon/session.ts
Retains providers/fleet/compressionRegistry/onModels as private fields, refactors #createProvider to accept a provider id and rebuild via retained registry, and updates #switchProviderInner to use it.
SessionManager routing
src/daemon/session-manager.ts
Adds session.set_provider case dispatching to a new #setProvider handler enforcing scope/ownership before calling session.switchProvider.
Tests and docs
src/tests/session-provider-switch.test.ts, CHANGELOG.md, docs/providers-pi.md
Adds integration tests for switch happy-path, failure, and degradation cases plus SessionManager scope tests; documents switching semantics in changelog and provider docs.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • saucam/codeoid#38: Establishes the AgentProvider interface and canonical history machinery extended here with seedFromHistory and renderHistorySeed.
  • saucam/codeoid#125: Also modifies src/daemon/session.ts provider/model switching behavior around teardown and reinitialization.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: mid-session provider switching and provider-owned history seeding.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/provider-switch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/daemon/session-manager.ts`:
- Around line 2036-2048: Update the inline trust/scope comment in `#setProvider`
so it no longer claims parity with `#setModel`, since `#setModel` uses
SCOPES.SESSION_SEND while `#setMode` uses SCOPES.SESSION_APPROVE; adjust the
wording to reflect that only `#setMode` matches the current session-config write
scope, keeping the note accurate for future changes.

In `@src/daemon/session.ts`:
- Around line 719-728: Reset the rotation state when the provider changes in
session switching logic so a stale `#justRotated` flag cannot affect the next
send(). Update the provider-switch path in Session to clear `#justRotated`
alongside `#model` and `#fallbackModel`, and make sure the send()/#buildRotationSeed
flow only seeds once per provider lifecycle so it does not prepend the old
Claude-specific rotation message after a switch.
- Around line 657-772: The mid-turn protection in switchProvider is only checked
before the request is serialized, so a queued send() can start a turn before
`#switchProviderInner` runs and still get torn down. Re-check the
active/waiting/pending guard inside `#switchProviderInner` (or immediately before
`#teardownProvider`) using the same `#status`, `#pendingApprovals`, and
`#pendingUiRequests` conditions, and return the invalid_request error there if a
turn has started.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8acc4bf8-9989-4678-9112-955185370053

📥 Commits

Reviewing files that changed from the base of the PR and between 33effa2 and bf7e851.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • docs/providers-pi.md
  • packages/core/src/slash.test.ts
  • packages/core/src/slash.ts
  • packages/protocol/src/schemas.test.ts
  • packages/protocol/src/schemas.ts
  • packages/protocol/src/types.ts
  • src/daemon/providers/canonical.ts
  • src/daemon/providers/claude/index.ts
  • src/daemon/providers/interface.ts
  • src/daemon/providers/mock/session-provider.ts
  • src/daemon/providers/pi/index.ts
  • src/daemon/session-manager.ts
  • src/daemon/session.ts
  • src/tests/fixtures/fake-pi.ts
  • src/tests/pi-translate.test.ts
  • src/tests/provider-pi.test.ts
  • src/tests/session-provider-switch.test.ts

Comment thread src/daemon/session-manager.ts
Comment thread src/daemon/session.ts
Comment thread src/daemon/session.ts
…on-seed clear, scope comment

- Re-run the busy guard INSIDE the chain-serialized switch: a send()
  queued ahead starts its turn and returns mid-stream, so the pre-check
  alone could tear down an actively-running provider. New S6 test
  reproduces the race deterministically (stalling provider + racing
  switch) and proves the rejection
- Clear the pending rotation seed on switch: a post-rotation switch
  would otherwise stack the Claude-worded rotation anchor on top of the
  new provider's own transcript seed (S7 proves the prompt is clean)
- Correct the set_provider scope comment: set_model gates on
  SESSION_SEND, not SESSION_APPROVE — only set_mode is the true peer
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.10314% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.54%. Comparing base (33effa2) to head (fe86946).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/daemon/providers/claude/index.ts 81.81% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #133      +/-   ##
==========================================
+ Coverage   80.60%   81.54%   +0.94%     
==========================================
  Files          94       94              
  Lines       16215    16376     +161     
==========================================
+ Hits        13070    13354     +284     
+ Misses       3145     3022     -123     
Flag Coverage Δ
daemon 81.54% <99.10%> (+0.94%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/core/src/slash.ts 97.79% <100.00%> (+0.17%) ⬆️
packages/protocol/src/schemas.ts 100.00% <100.00%> (ø)
packages/protocol/src/types.ts 100.00% <ø> (ø)
src/daemon/providers/canonical.ts 100.00% <100.00%> (ø)
src/daemon/providers/interface.ts 100.00% <ø> (ø)
src/daemon/providers/mock/session-provider.ts 100.00% <100.00%> (ø)
src/daemon/providers/pi/index.ts 90.23% <100.00%> (+0.23%) ⬆️
src/daemon/session-manager.ts 65.38% <100.00%> (+0.56%) ⬆️
src/daemon/session.ts 95.16% <100.00%> (+7.13%) ⬆️
src/daemon/providers/claude/index.ts 97.13% <81.81%> (-0.37%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@saucam
saucam merged commit 55a0e9d into main Jul 9, 2026
6 of 7 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.

1 participant