Skip to content

feat: Virtuosos — multi-account multiplexing and capacity-aware routing#391

Open
openasocket wants to merge 43 commits intoRunMaestro:mainfrom
openasocket:feat/virtuosos
Open

feat: Virtuosos — multi-account multiplexing and capacity-aware routing#391
openasocket wants to merge 43 commits intoRunMaestro:mainfrom
openasocket:feat/virtuosos

Conversation

@openasocket
Copy link
Contributor

@openasocket openasocket commented Feb 16, 2026

Virtuosos

Virtuosos is Maestro's multi-account multiplexing system. It allows users to register multiple AI provider accounts (e.g., several Claude API keys or Pro subscriptions) and have Maestro intelligently distribute agent sessions across them.

Intent

Power users and teams frequently hit rate limits or usage caps on a single account, especially during intensive coding sessions with many concurrent agents. Virtuosos solves this by treating multiple accounts as a pooled resource — Maestro automatically selects the account with the most remaining capacity when spawning or restarting agents, and can seamlessly switch accounts mid-session when throttling is detected.

Value Proposition

  • Uninterrupted workflows: Automatic failover when an account hits rate limits — agents switch to a healthy account and resume without user intervention
  • Capacity-aware routing: New agent spawns are directed to the account with the most headroom, maximizing throughput across all registered accounts
  • Usage visibility: Per-account usage dashboards with token breakdowns, sparkline trends, P90 predictions, and time-range toggles (24h/7d/30d/monthly) give users real-time insight into consumption patterns
  • CLI integration: Batch playbook runs can distribute work across accounts for higher parallelism
  • Graceful degradation: When all accounts are exhausted, Maestro surfaces recovery options and pauses Auto Run rather than failing silently

What's Included (32 commits, 148 files, ~14k lines)

Core Infrastructure

  • Account registry store with CRUD operations and persistence
  • Account directory setup service with login orchestration
  • Environment variable injection per account at spawn time
  • IPC handlers and preload bridge (window.maestro.accounts.*)
  • Stats database v4 migration for per-account usage tracking

Multiplexing Engine

  • Capacity-aware selectNextAccount() using historical usage data
  • Throttle detection from agent error streams triggering automatic account switches
  • Account switch execution with process restart and session resume
  • Session/assignment persistence for restart recovery
  • Auth recovery service and polling for expired credentials
  • All-accounts-exhausted handling with recovery IPC

UI Surfaces

  • VirtuososModal: Dedicated modal with tabbed Configuration/Usage views
  • AccountSelector: Inline account picker for agent creation and manual switching
  • AccountSwitchModal: Confirmation dialog for manual account changes
  • AccountUsageDashboard: Per-account usage charts with sparklines, trend indicators, and P90 predictions
  • AccountTrendChart: Time-range toggle sparklines (24h/7d/30d/monthly)
  • AccountRateMetrics: Rate-of-consumption metrics display
  • Inline usage badges on SessionItem and ProcessMonitor
  • Account display in SymphonyModal and session merge/transfer flows

CLI & Batch

  • maestro accounts CLI command for headless account management
  • Batch processor integration for distributing playbook runs across accounts

Testing

  • Comprehensive test suites for AccountRegistry, AccountRecoveryPoller, AccountSetup, AccountSwitcher, AccountThrottleHandler, AccountEnvInjector, and UI components

Test Plan

  • Register multiple accounts via VirtuososModal and verify persistence across app restart
  • Spawn agents and confirm capacity-aware account selection routes to the least-used account
  • Trigger rate limiting on one account and verify automatic failover to another
  • Verify usage dashboard displays accurate per-account token breakdowns and trend charts
  • Run npm run test — all new and existing tests pass
  • Run npm run lint — no type errors
  • Test CLI maestro accounts subcommands
  • Verify batch playbook runs distribute across accounts

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Full "Virtuosos" multi-account support: manage, discover, create, and configure Claude accounts with per-account limits, rotation and capacity-aware selection.
    • UI: account selector, Account Switch modal, Virtuosos modal, Accounts panel, usage dashboards, charts, history, and session/account badges.
    • CLI: list accounts and options to select/rotate accounts for playbooks.
    • Automatic throttling handling, auth-recovery flow, provider-switch workflow, and richer per-query telemetry (tokens & cost).
  • Bug Fixes

    • Unified "Re-authenticate" label for auth-recovery actions.

openasocket and others added 30 commits February 15, 2026 01:41
Add foundational type definitions for the account multiplexing system:
- AccountProfile, AccountUsageSnapshot, AccountAssignment for core data models
- AccountSwitchConfig and AccountSwitchEvent for auto-switching behavior
- AccountCapacityMetrics for capacity planning
- AccountId, AccountStatus, AccountAuthMethod, MultiplexableAgent type aliases
- ACCOUNT_SWITCH_DEFAULTS and DEFAULT_TOKEN_WINDOW_MS constants
- accountId and accountName optional fields on Session interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the persistence layer for account multiplexing:
- AccountStoreData schema type for electron-store
- maestro-accounts store instance in initializeStores()
- AccountRegistry class with full CRUD, assignment management,
  round-robin/least-used selection, and switch config
- 37-test suite for AccountRegistry covering all operations
- Updated instances.test.ts store count assertion (8→9)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion)

- Bump STATS_DB_VERSION from 3 to 4
- Add migration v4: account_id + token/cost columns to query_events,
  account_usage_windows table, account_throttle_events table
- Extend QueryEvent interface with accountId, token counts, and costUsd
- Update insertQueryEvent INSERT to include new columns
- Add account-usage.ts module with upsertAccountUsageWindow,
  getAccountUsageInWindow, insertThrottleEvent, getThrottleEvents
- Wire new module into StatsDB class following delegated pattern
- Update row-mappers for new QueryEventRow fields
- Fix test expectations for version 4 and new INSERT parameters

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Registers 16 IPC handlers for the accounts: namespace covering CRUD,
assignments, usage queries, throttle events, switch config, and account
selection. Extends the preload accounts API with corresponding invoke
methods alongside the existing event listeners from ACCT-MUX-04. Wires
the account usage listener into the process listener module for
real-time usage tracking and limit notifications.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ation

Adds account-setup.ts with directory creation, symlink management,
account discovery, email extraction, and SSH remote validation.
Registers 9 IPC handlers and preload bridge methods for the full
account setup lifecycle. Includes 25 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds Accounts tab to Settings modal with:
- Account list with status badges, default indicators, and inline config
- Discover existing accounts and import them
- Create new account flow with login command generation
- Per-account token limit, window duration, and auto-switch toggle
- Symlink validation and repair buttons
- Global switch configuration (thresholds, strategy, prompt toggle)
- CLAUDE_CONFIG_DIR conflict detection banner for manual overrides
- MaestroAPI type declaration for window.maestro.accounts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix TypeScript errors in AccountUsageDashboard (TS2783 duplicate property
overwrites), add 'accounts' to DashboardSkeleton viewMode type, remove
unused lucide imports, and update UsageDashboardModal tests for the new
Accounts tab (tab count 4→5, wrap-around nav, accounts IPC mocks).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add AccountThrottleHandler that bridges rate-limit error detection with
account switching. When rate_limited or auth_expired errors are detected
on sessions with account assignments, records the throttle event in stats
DB, marks the account as throttled, and notifies the renderer with switch
recommendations (prompt or auto-switch depending on config).

Also adds auto-recovery from throttle state when the usage window advances,
and preload bridge events for throttle/switch/status-changed notifications.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nd resume

Implements the core account switch mechanism (ACCT-MUX-10):
- AccountSwitcher service: kill process → reassign account → respawn with new CLAUDE_CONFIG_DIR → re-send last prompt
- Prompt recording in process:write handler for seamless resume after switch
- IPC handlers for execute-switch and cleanup-session
- Renderer respawn handler with toast notifications for switch lifecycle events
- Preload bridge with typed events (switch-started, switch-respawn, switch-completed, switch-failed)
- Session cleanup on agent deletion (removes account assignment and prompt tracking)
- Type declarations in global.d.ts for all new accounts API methods
- Comprehensive test suite for AccountSwitcher (7 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nt selector

Implements the UI layer for account multiplexing switches:
- AccountSwitchModal: confirmation modal for prompted switches (throttle/limit events)
  with reason-based headers, account status display, and action buttons
- AccountSelector: compact dropdown in InputArea footer for manual account switching
  per session, with status dots, usage bars, and Manage Accounts link
- App.tsx: onSwitchPrompt listener shows modal, onSwitchCompleted toast notification
- Added ACCOUNT_SWITCH priority (1005) to modalPriorities.ts
- Added 'accounts' to SettingsTab type for View All Accounts navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire CLAUDE_CONFIG_DIR injection into every code path that spawns Claude Code
agents: standard process:spawn, Group Chat participants/moderators/synthesis,
Context Grooming, and session resume. Add missing onAssigned type declaration
to global.d.ts to fix lint error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y for account multiplexing

Ensures account assignments survive app restarts by reconciling accounts on
session restore. Adds typed accountId to spawn config, reconcileAssignments()
to AccountRegistry, and an accounts:reconcile-sessions IPC handler called
after session restore to validate/correct account state and CLAUDE_CONFIG_DIR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds CLI-side account reading, --account and --account-rotation flags
for the playbook command, CLAUDE_CONFIG_DIR injection in agent-spawner,
round-robin account rotation in batch-processor, and a new 'accounts'
command to list configured accounts from the terminal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ations

Wire up account multiplexing support for the MergeSessionModal and all
merge-related operations:

- Display source session account (green dot indicator) in token preview
- Show target session account in tab list items during selection
- Warn when merging across different accounts (info note about symlinks)
- Propagate accountId/accountName to new sessions created by merge
- Thread accountId through the full context grooming chain:
  renderer → preload → IPC handler → context-groomer utility
- Add getAccountRegistry to ContextHandlerDependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…and session UI surfaces

- ProcessMonitor: account badge on process list items and account info card in detail view
- SymphonyModal: account badge next to session name link on contribution cards
- SessionList: account info in tooltip content and non-clickable account entry in context menu
- All account UI gracefully hidden when no account is assigned

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests cover: timer-based recovery of throttled accounts past their window,
IPC event broadcasting (status-changed + recovery-available), multi-account
recovery with correct counts, edge cases (zero lastThrottledAt, missing
tokenWindowMs), and start/stop lifecycle including idempotency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add `selectByRemainingCapacity()` method that routes to the account
  with the most remaining token capacity in its current usage window
- Extract shared `getWindowBounds()` utility to `account-utils.ts`
  (was duplicated in throttle handler, usage listener, and IPC handlers)
- Accept optional `AccountUsageStatsProvider` in `selectNextAccount()`
  for capacity-aware routing; falls back to LRU when unavailable
- Apply 50% capacity penalty to recently-throttled accounts (within 2
  windows) to prevent ping-ponging
- Accounts without configured limits are deprioritized behind accounts
  with known remaining capacity
- Round-robin strategy remains deterministic and ignores usage stats
- Add 7 new tests covering capacity-aware selection scenarios

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ty-aware routing

All call sites that invoke selectNextAccount() now pass the optional statsDB
parameter, enabling capacity-aware account selection when the stats database
is available. This allows the least-used strategy to route to accounts with
the most remaining token capacity rather than just LRU ordering.

Callers updated:
- account-env-injector: accepts optional getStatsDB function, passes to selectNextAccount
- account-throttle-handler: passes its existing getStatsDB to selectNextAccount
- accounts IPC handler (accounts:select-next): passes getStatsDB singleton
- process handler (process:spawn): passes getStatsDB via injectAccountEnv

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… Auto Run recovery indicator

- Subscribe to account:throttled events with noAlternatives flag to pause
  Auto Runs with a specific rate_limited error when all accounts exhausted
- Add accounts:check-recovery IPC handler for manual recovery poll trigger
- Wire AccountRecoveryPoller into registerAccountHandlers dependencies
- Add recovery-specific UI in AutoRun.tsx: pulsing accent indicator with
  "Waiting for account recovery — will auto-resume" and "Check Now" button
- Subscribe to account:recovery-available for auto-resume of paused runs
- Add 6 tests for accounts:check-recovery handler (all passing)
- All 19,490 tests pass, lint clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s and promote to own modal

Rename all user-facing "Accounts" terminology to "Virtuosos" with "AI Account Providers"
subtitle. Move Virtuosos panel from Settings tab to its own top-level modal accessible from
the hamburger menu. Internal code (variable names, IPC channels, interfaces) unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lector, and SessionItem badge

Create shared useAccountUsage hook with real-time IPC subscription, derived
metrics (burn rate, time-to-limit), and adaptive countdown (30s default,
5s when any account is within 5 minutes of window reset).

- AccountsPanel: usage bar, token count, cost, queries, reset timer, burn rate per account
- AccountSelector: fix hardcoded 0% usage bar with live data and compact stats
- SessionItem: enhance badge tooltip to show usage percentage
- Add accounts mock to test setup for complete IPC coverage
- 17 new tests covering hook, formatTimeRemaining, and formatTokenCount

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rends, and plan presets

Implements ACCT-MUX-20: per-account daily/monthly aggregation queries,
IPC handlers for historical usage, P90-weighted predictive analysis with
exponential decay, plan preset picker (Pro/Max5/Max20), collapsible
historical usage view with 7d/30d/monthly modes, throttle frequency
tracking, and window history caching for predictions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AccountSwitcher was fully implemented but never instantiated in index.ts,
leaving manual account switches from the renderer completely broken. This
adds the instantiation with correct dependencies and passes the getter to
both registerAccountHandlers and registerProcessHandlers, enabling the
accounts:execute-switch IPC and process:write recordLastPrompt paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ounds

- Add AccountAuthRecovery class: automatic OAuth re-authentication
  when agents encounter expired tokens (kill → login → respawn)
- Add 21 unit tests covering happy path, timeout, credential sync
  fallback, concurrent recovery guard, spawn errors, and edge cases
- Wire auth recovery into error-listener for auth_expired errors
- Consolidate getWindowBounds() duplicate: remove local copy from
  account-usage-listener.ts, import from account-utils.ts
- Update error-listener, process-listeners/index, types for auth
  recovery dependency injection
- Add syncCredentialsFromBase fallback in account-setup.ts
- Update renderer components and hooks for auth recovery state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace blocking alert() calls with toast notifications in AccountsPanel,
add console.warn to 5 silent catch blocks for Sentry breadcrumb visibility,
replace all hardcoded color hex values with theme.colors.error/warning,
tighten 7 loosely-typed Record<string, unknown> event handlers in global.d.ts
with specific payload interfaces, and expand test coverage for AccountSelector
(8 tests) and AccountSwitchModal (9 tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert VirtuososModal from single-panel to two-tab layout:
- Configuration tab: existing AccountsPanel (unchanged)
- Usage tab: new VirtuosoUsageView with aggregate summary, per-account
  usage cards, predictions section, historical expandables, and throttle
  event timeline
- Keyboard navigation (Cmd/Ctrl+Shift+[/]) following UsageDashboardModal
  pattern
- Dynamic modal width (720px config, 900px usage)
- Sessions prop passed from App.tsx for session assignment display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The usage view was collapsing all token types into a single totalTokens
value. The backend already stored inputTokens, outputTokens,
cacheReadTokens, and cacheCreationTokens separately but the renderer
never extracted or displayed them. Additionally, real-time usage-update
events were only broadcast for accounts with token limits configured,
leaving unlimited accounts with stale data.

Key fixes:
- Broadcast account:usage-update for ALL accounts, not just those with limits
- Add token breakdown fields to AccountUsageUpdate interface and IPC event
- Propagate inputTokens/outputTokens/cacheReadTokens/cacheCreationTokens
  through useAccountUsage hook and AccountUsageMetrics interface
- Add per-account token breakdown grid (In/Out/Cache R/Cache W) in usage cards
- Add stacked color-coded bars with legend in AccountUsageHistory
- Fix daily/monthly SQL to query account_usage_windows instead of
  query_events (which lacked account_id population)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e metrics to Virtuosos usage view

- Add RateMetrics interface and calculateRateMetrics() to useAccountUsage hook
  with tokens/hr, tokens/day, tokens/week, period-over-period deltas, and
  linear regression trend detection
- Create AccountTrendChart SVG component with full chart and compact sparkline modes
- Create AccountRateMetrics panel component with rate display and trend indicator
- Integrate 7-day sparklines into per-account usage cards
- Add burn rate trend arrows (up/down unicode indicators)
- Add new Trends & Analytics section between Predictions and Historical Usage
- Increase window history fetch from 40 to 68 for 2-week delta calculations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…onent

Removes the unnecessary default React import that caused TS6133 since
the project uses the modern JSX transform.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ccountUsageDashboard

Integrates the useAccountUsage hook into the main Usage Dashboard's Accounts tab
to surface burn rate, TTL, prediction confidence, 7-day sparklines, and daily trend
arrows alongside existing overview cards and bar charts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
openasocket and others added 3 commits February 16, 2026 02:50
…eners and token data pipeline

The account-usage-listener and error-listener failed to find account
assignments because usage/error events arrive with compound session IDs
(e.g. "{id}-ai-{tabId}") while assignments were keyed by base session
ID from reconcileSessions on app restore. Added fallback lookup that
strips the suffix to find the base-keyed assignment.

Also: consolidated duplicated session ID suffix regex into a shared
REGEX_SESSION_SUFFIX constant (now includes -terminal), wired token
data through the recordQuery pipeline (stats-listener, ExitHandler,
useAgentListeners, useAgentExecution), removed debug console.logs
from StdoutHandler, and fixed toggle off-state color in AccountsPanel
to use bgActivity instead of border.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…TrendChart sparklines

Rewrote AccountTrendChart to support switchable time ranges with pill toggles
in both compact sparkline and full chart modes. Each range fetches from the
appropriate data source (billing windows for 24h, daily aggregation for 7d/30d,
monthly aggregation for monthly). Updated all callers in VirtuosoUsageView and
AccountUsageDashboard to use the new defaultRange prop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…AccountTrendChart range toggles

Tailwind JIT cannot detect dynamically constructed class names, so the
toggle buttons were invisible. Switched to inline style props with
visible bgActivity background for unselected state and bumped compact
font size from 8px to 9px.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@pedramamini
Copy link
Collaborator

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds full account-multiplexing: account store/registry, setup/repair tooling, StatsDB migration v4 and account usage tracking, throttle handling, auth-recovery and switching services, CLI support and spawn env injection, renderer UI (management, selector, dashboards), IPC/preload APIs, process/listener integration, and many tests.

Changes

Cohort / File(s) Summary
Account core modules
src/main/accounts/... (account-registry.ts, account-setup.ts, account-auth-recovery.ts, account-switcher.ts, account-throttle-handler.ts, account-recovery-poller.ts, account-env-injector.ts, account-utils.ts)
New account management services: registry, setup/repair, auth-recovery, switcher, throttle handling, recovery poller, env injector, and window-bound helper. Public APIs added (classes, functions, interfaces) — review registry selection logic, persistence, and public method signatures.
Stats DB, migrations & usage
src/main/stats/... (migrations.ts, schema.ts, account-usage.ts, stats-db.ts, query-events.ts, row-mappers.ts)
Migration v4: extend query_events with account/token/cost fields; add account_usage_windows & account_throttle_events tables; provide upsert/query helpers and StatsDB delegators. Review SQL, migration ordering, version bump, and delegating methods.
Process integration & listeners
src/main/process-listeners/..., src/main/process-manager/... (account-usage-listener.ts, error-listener.ts, stats-listener.ts, types.ts, ExitHandler.ts)
Wire account usage aggregation, throttle/error delegation (auth_expired/rate_limited) to account handlers, and propagate usage token fields on exit/stats. Types extended for optional account deps; check listener lifecycle and dependency wiring.
Stores & initialization
src/main/stores/... (account-store-types.ts, instances.ts, getters.ts, index.ts)
New accountStore added, defaults introduced, getter getAccountStore() exported and re-exported; initialization updated. Review defaults and store migration implications.
IPC / preload / main wiring
src/main/ipc/handlers/..., src/main/preload/..., src/main/index.ts
New account IPC handlers and preload API (window.maestro.accounts), main startup wires AccountRegistry, handlers, poller, switcher, auth recovery; process handlers extended to inject CLAUDE_CONFIG_DIR and record prompts. Verify handler registration, permission/error paths, and preload typings.
CLI
src/cli/services/account-reader.ts, src/cli/commands/accounts.ts, src/cli/services/agent-spawner.ts, src/cli/commands/run-playbook.ts, src/cli/index.ts, src/cli/services/batch-processor.ts
Add CLI account reader, accounts list command; forward configDir to agent spawns (CLAUDE_CONFIG_DIR); add --account and --account-rotation options and per-task account resolution/rotation. Check option parsing, spawn env injection, and batch-processor rotation cache.
Group-chat & grooming
src/main/group-chat/..., src/main/utils/context-groomer.ts
Propagate accountRegistry and injectAccountEnv into group-chat spawn flows; GroupChat gains optional accountId; context groomer accepts accountId. Review SSH/remote spawn paths for env injection safety.
Shared types & constants
src/shared/..., src/main/constants.ts, src/renderer/global.d.ts
New account-types module, DEFAULT_TOKEN_WINDOW_MS, bump STATS_DB_VERSION → 4, expand QueryEvent with token/cost fields, add REGEX_SESSION_SUFFIX. Inspect public type changes and downstream consumers.
Renderer components & UI
src/renderer/components/... (AccountSelector, AccountSwitchModal, AccountsPanel, VirtuososModal, VirtuosoUsageView, AccountUsageHistory, plus many files)`
New accounts UI surface: selector, switch modal, full management panel, Virtuosos modal, usage dashboard and charts, session/account badges, and wiring for switching/recovery. Large UI additions — review accessibility, keyboard navigation, and event wiring to IPC.
Renderer hooks & stores
src/renderer/hooks/..., src/renderer/stores/... (useAccountUsage.ts, provider-switch, agentStore, modalStore, input processing)`
New useAccountUsage hook (predictions/rates), useProviderSwitch, inject accountId into spawn args, agentStore now triggers auth-recovery via IPC, modalStore adds virtuosos state. Review hook algorithms (predictions), subscriptions, and prop typings.
Usage dashboards & charts
src/renderer/components/UsageDashboard/...
New AccountUsageDashboard, AccountTrendChart, AccountRateMetrics, and tab integration (Virtuosos). Large visual components — review performance, data fetching cadence, and memoization.
Preload, tests & mocks
src/main/preload/accounts.ts, src/__tests__/**, src/__tests__/setup.ts
Preload exposes comprehensive accounts API; tests added/updated across CLI, main, and renderer (many suites). setup.ts expands mockMaestro.accounts. Review test mocks for realism and ensure no accidental export/API regressions.
Misc & UX
various (exit-listener.ts, StdoutHandler.ts, AutoRun.tsx, SettingsModal.tsx, modalPriorities.ts, renderer types)`
Standardize session-suffix regex, remove debug logs, add AutoRun recovery UI, Virtuosos feature flag, new modal priorities, extend renderer session types (accountId/accountName and migration provenance). Verify feature-flag gating and modal priority interactions.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer as Renderer (Client)
    participant PM as ProcessManager
    participant AUS as AccountUsageListener
    participant Reg as AccountRegistry
    participant DB as StatsDB
    participant Notif as SafeSend

    Renderer->>PM: emit usage event (sessionId, tokens)
    PM->>AUS: forward usage event
    AUS->>Reg: getAssignment(sessionId) / try base session
    Reg-->>AUS: accountId
    AUS->>DB: upsertAccountUsageWindow(accountId, windowStart, windowEnd, tokens)
    DB-->>AUS: ack
    AUS->>DB: getAccountUsageInWindow(accountId, windowStart, windowEnd)
    DB-->>AUS: usage metrics
    AUS->>AUS: compute usagePercent & predictions
    AUS->>Notif: emit account:usage-update with metrics
    Notif-->>Renderer: usage update
Loading
sequenceDiagram
    participant Renderer as Client
    participant Auth as AccountAuthRecovery
    participant PM as ProcessManager
    participant Login as ClaudeLoginProcess
    participant Reg as AccountRegistry
    participant Notif as SafeSend

    Renderer->>Auth: recoverAuth(sessionId, accountId)
    Auth->>Reg: setStatus(accountId, 'expired')
    Auth->>PM: kill agent (sessionId)
    PM-->>Auth: killed
    Auth->>Notif: emit account:auth-recovery-started
    Auth->>Login: spawnClaudeLogin(configDir) [CLAUDE_CONFIG_DIR set]
    Login-->>Auth: exit code / timeout
    alt success
        Auth->>Reg: setStatus(accountId, 'active')
        Auth->>Notif: emit account:auth-recovery-completed
        Auth->>Notif: emit account:switch-respawn (lastPrompt)
    else failure
        Auth->>Auth: fallback syncCredentialsFromBase(configDir)
        Auth->>Notif: emit account:auth-recovery-failed
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Virtuosos — multi-account multiplexing and capacity-aware routing' clearly summarizes the main change—introducing a multi-account system with capacity-aware routing, which is the primary focus of this large changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

… addToastRef

Conflicts resolved (5 files):
- group-chat-router.ts: kept both account imports + main's reformatted import
- index.ts: kept both registerAccountHandlers + registerWakatimeHandlers
- ipc/handlers/index.ts: kept both account + wakatime handler imports/exports
- preload/index.ts: kept both accounts + wakatime API factories and type exports
- SettingsModal.tsx: adopted main's encore features architecture (director-notes
  settings now gated under the encore tab toggle system)

Post-merge fix:
- App.tsx: replaced 8 addToastRef.current() calls with notifyToast() from the
  new zustand notificationStore (main refactored toast system from ref to store)

TypeScript compiles clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/renderer/components/MergeSessionModal.tsx (1)

458-465: ⚠️ Potential issue | 🟠 Major

Exception silently swallowed — report to Sentry or surface to user.

The catch block logs the error but doesn't report it to Sentry or provide user feedback. Per coding guidelines, unexpected errors should bubble up for error tracking.

Proposed fix: Report to Sentry and optionally surface error state
+import { captureException } from '../../utils/sentry';
 	const handleMerge = useCallback(async () => {
 		const target = viewMode === 'paste' ? pastedIdMatch : selectedTarget;
 		if (!target) return;

 		setIsMerging(true);
 		try {
 			await onMerge(target.sessionId, target.tabId, options);
 			onClose();
 		} catch (error) {
-			console.error('Merge failed:', error);
+			captureException(error, { extra: { targetSessionId: target.sessionId, targetTabId: target.tabId } });
+			// Consider adding error state to show user feedback
 		} finally {
 			setIsMerging(false);
 		}
 	}, [viewMode, pastedIdMatch, selectedTarget, options, onMerge, onClose]);

As per coding guidelines: "Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/MergeSessionModal.tsx` around lines 458 - 465, The
catch block in MergeSessionModal (around the onMerge call) currently only
console.error's the exception; update it to report the error to Sentry (e.g.,
call Sentry.captureException(error) after importing your Sentry client),
optionally set a local error state or trigger a user-facing notification/toast
to surface the failure to the user, and then rethrow the error so it bubbles to
the global handler; keep the final setIsMerging(false) in finally. Reference:
onMerge, setIsMerging, and the MergeSessionModal component.
src/renderer/components/UsageDashboard/ChartSkeletons.tsx (1)

343-384: ⚠️ Potential issue | 🟡 Minor

Add a skeleton for the new “accounts” view to avoid a blank loading state.

viewMode now allows 'accounts', but DashboardSkeleton doesn’t render anything for it, so the loading UI is empty. Consider mapping it to an existing skeleton or creating a dedicated one.

🧩 Minimal fallback (treat accounts like overview until a dedicated skeleton exists)
-			{viewMode === 'overview' && (
+			{(viewMode === 'overview' || viewMode === 'accounts') && (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/UsageDashboard/ChartSkeletons.tsx` around lines 343 -
384, DashboardSkeleton currently accepts viewMode including 'accounts' but
doesn't render anything for it; update the component (DashboardSkeleton) to
handle viewMode === 'accounts' by mapping it to an existing skeleton (e.g.,
render the same children as 'overview': SummaryCardsSkeleton with
summaryCardsCols, the chart grid with AgentComparisonChartSkeleton and
SourceDistributionChartSkeleton, ActivityHeatmapSkeleton, and
DurationTrendsChartSkeleton) so the accounts loading state shows the same
fallback UI until a dedicated AccountsSkeleton is created; ensure you reuse the
existing props (theme, chartGridCols, summaryCardsCols) and the same structure
used in the 'overview' branch.
src/main/group-chat/group-chat-agent.ts (1)

193-233: ⚠️ Potential issue | 🟠 Major

Account environment injection is lost after SSH wrapping.

The CLAUDE_CONFIG_DIR injected at lines 196-204 is stored in spawnEnvVars, but the SSH wrapping block at lines 209-233 passes the original configResolution.effectiveCustomEnvVars ?? effectiveEnvVars (line 217) to wrapSpawnWithSsh, then overwrites spawnEnvVars with sshWrapped.customEnvVars (line 229). This discards the injected account config for SSH-spawned participants.

🔧 Proposed fix: Pass the updated spawnEnvVars to SSH wrapping
 	// Inject CLAUDE_CONFIG_DIR for account multiplexing
 	if (accountRegistry) {
 		const envToInject: Record<string, string> = spawnEnvVars ? { ...spawnEnvVars } : {};
 		const assignedId = injectAccountEnv(
 			sessionId,
 			agentId,
 			envToInject,
 			accountRegistry,
 			accountId,
 		);
 		if (assignedId) {
 			spawnEnvVars = envToInject;
 		}
 	}
 
 	// Apply SSH wrapping if SSH is configured and store is available
 	if (sshStore && sessionOverrides?.sshRemoteConfig) {
 		console.log(`[GroupChat:Debug] Applying SSH wrapping for participant...`);
 		const sshWrapped = await wrapSpawnWithSsh(
 			{
 				command,
 				args: configResolution.args,
 				cwd,
 				prompt,
-				customEnvVars: configResolution.effectiveCustomEnvVars ?? effectiveEnvVars,
+				customEnvVars: spawnEnvVars,
 				promptArgs: agentConfig?.promptArgs,
 				noPromptSeparator: agentConfig?.noPromptSeparator,
 				agentBinaryName: agentConfig?.binaryName,
 			},
 			sessionOverrides.sshRemoteConfig,
 			sshStore
 		);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/group-chat/group-chat-agent.ts` around lines 193 - 233, The account
env injected by injectAccountEnv into spawnEnvVars is being dropped because
wrapSpawnWithSsh is called with configResolution.effectiveCustomEnvVars ??
effectiveEnvVars instead of the possibly-updated spawnEnvVars; update the
wrapSpawnWithSsh call to pass spawnEnvVars (the variable possibly mutated by
injectAccountEnv) and after the call ensure you merge/retain existing keys when
applying sshWrapped.customEnvVars (i.e., merge sshWrapped.customEnvVars into
spawnEnvVars rather than wholesale replace) so CLAUDE_CONFIG_DIR and other
injected values persist; key symbols: injectAccountEnv, spawnEnvVars,
wrapSpawnWithSsh, configResolution.effectiveCustomEnvVars, effectiveEnvVars,
sshWrapped.customEnvVars.
src/main/group-chat/group-chat-router.ts (1)

523-562: ⚠️ Potential issue | 🟠 Major

SSH spawns drop the injected account env.
At Line 541 and Line 947 the SSH wrapper receives pre‑injection env vars, so CLAUDE_CONFIG_DIR doesn’t reach the remote process. Pass the post‑injection env variables to preserve account assignment.

🔧 Suggested fix
 					const sshWrapped = await wrapSpawnWithSsh(
 						{
 							command,
 							args: finalArgs,
 							cwd: os.homedir(),
 							prompt: fullPrompt,
-							customEnvVars:
-								configResolution.effectiveCustomEnvVars ??
-								getCustomEnvVarsCallback?.(chat.moderatorAgentId),
+							customEnvVars: spawnEnvVars,
 							promptArgs: agent.promptArgs,
 							noPromptSeparator: agent.noPromptSeparator,
 							agentBinaryName: agent.binaryName,
 						},
@@
 					const sshWrapped = await wrapSpawnWithSsh(
 						{
 							command: spawnCommand,
 							args: spawnArgs,
 							cwd,
 							prompt: participantPrompt,
-							customEnvVars:
-								configResolution.effectiveCustomEnvVars ??
-								getCustomEnvVarsCallback?.(participant.agentId),
+							customEnvVars: finalSpawnEnvVars,
 							promptArgs: agent.promptArgs,
 							noPromptSeparator: agent.noPromptSeparator,
 							agentBinaryName: agent.binaryName,
 						},

Also applies to: 927-968

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/group-chat/group-chat-router.ts` around lines 523 - 562, The SSH
wrapper is being called with pre-injection env vars so CLAUDE_CONFIG_DIR
injected by injectAccountEnv is lost; after calling injectAccountEnv (see
injectAccountEnv and spawnEnvVars usage) pass the updated spawnEnvVars into
wrapSpawnWithSsh (instead of the original custom env source) and ensure
sshWrapped.customEnvVars is set back to spawnEnvVars so the remote process
receives the post-injection environment; update both places where
wrapSpawnWithSsh is invoked (the moderator and the other agent flow) to use the
post-injection spawnEnvVars variable.
src/main/ipc/handlers/process.ts (1)

295-458: ⚠️ Potential issue | 🟠 Major

Propagate injected account env into SSH command build.
Line 443 passes effectiveCustomEnvVars, which omits the injected CLAUDE_CONFIG_DIR. Remote sessions will run under the wrong account. Use customEnvVarsToPass instead.

🔧 Suggested fix
 						const sshCommand = await buildSshCommandWithStdin(sshResult.config, {
 							command: remoteCommand,
 							args: sshArgs,
 							cwd: config.cwd,
-							env: effectiveCustomEnvVars,
+							env: customEnvVarsToPass,
 							// prompt is not passed as CLI arg - it goes via stdinInput
 							stdinInput,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/process.ts` around lines 295 - 458, The SSH command is
built with effectiveCustomEnvVars which doesn't include the injected
CLAUDE_CONFIG_DIR (so remote sessions use the wrong account); change the env
passed into buildSshCommandWithStdin to use customEnvVarsToPass (the variable
that may have been updated by injectAccountEnv) instead of
effectiveCustomEnvVars so the injected account env is propagated when creating
sshCommand in this block that calls buildSshCommandWithStdin.
src/renderer/App.tsx (1)

4827-4878: ⚠️ Potential issue | 🟠 Major

Add support for clearing account assignments in Edit Agent modal.

The code only processes if (accountId), which means falsy values (null/undefined/empty) are silently ignored. If the Edit Agent flow is extended to allow users to clear an account assignment, the session fields update but the registry assignment is never cleared — there is no unassign API call, only assign and executeSwitch. Additionally, the AccountSelector component currently offers no UI affordance to unassign an account.

To properly support unassignment:

  1. Add an explicit unassign handler in the account registry IPC (or extend assign to accept null/undefined).
  2. Update the Edit Agent handler to call the unassign method when accountId is falsy.
  3. Consider adding a "Clear" or "No Account" option to the AccountSelector dropdown if users should be able to unassign accounts.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/App.tsx` around lines 4827 - 4878, The Edit Agent flow currently
only handles truthy accountId and never clears registry assignments; update the
handler around setSessions / sessionsRef.current to explicitly handle falsy
accountId by calling a new or existing unassign IPC (e.g.,
window.maestro.accounts.unassign(sessionId) or extend
window.maestro.accounts.assign to accept null) instead of skipping registry
updates; keep the immediate session state update (setSessions) to clear
accountId/accountName, call window.maestro.accounts.executeSwitch when switching
between accounts as before, and call window.maestro.accounts.unassign(sessionId)
when accountId is null/empty; also add a "Clear"/"No Account" option to the
AccountSelector UI so users can choose to unassign.

Comment on lines +80 to +87
interface ThrottleEvent {
timestamp: number;
accountId: string;
accountName?: string;
reason: string;
totalTokens: number;
recoveryAction?: string;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "ThrottleEvent" -g'*.ts' -g'*.tsx' -C3

Repository: RunMaestro/Maestro

Length of output: 17364


🏁 Script executed:

rg -n "getThrottleEvents" -g'*.ts' -g'*.tsx' -C3

Repository: RunMaestro/Maestro

Length of output: 8037


🏁 Script executed:

rg -n "tokensAtThrottle" -g'*.ts' -g'*.tsx' -C3

Repository: RunMaestro/Maestro

Length of output: 8450


🏁 Script executed:

rg -n "totalTokens\|recoveryAction" -g'*.ts' -g'*.tsx' -C2

Repository: RunMaestro/Maestro

Length of output: 44


🏁 Script executed:

sed -n '575,639p' src/renderer/components/UsageDashboard/AccountUsageDashboard.tsx

Repository: RunMaestro/Maestro

Length of output: 2813


🏁 Script executed:

rg -n "totalTokens|recoveryAction" src/renderer/components/UsageDashboard/ -C2

Repository: RunMaestro/Maestro

Length of output: 9969


🏁 Script executed:

# Check if throttleEvents are iterated/rendered
rg -n "throttleEvents\." src/renderer/components/UsageDashboard/ -A2

Repository: RunMaestro/Maestro

Length of output: 1041


Fix field mismatch between throttle event payload and UI interface.

The renderer's ThrottleEvent interface expects totalTokens and recoveryAction, but the backend returns tokensAtThrottle and doesn't provide recoveryAction at all. The throttle history table at lines 607–637 accesses event.totalTokens (line 633) and event.recoveryAction (line 636), which are undefined in the received data, causing the UI to render broken values instead of token counts and recovery information.

Align the backend ThrottleEvent shape in src/main/stats/account-usage.ts to match the renderer's expected fields, or rename the fields in the renderer interface to match what the backend actually returns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/UsageDashboard/AccountUsageDashboard.tsx` around
lines 80 - 87, The renderer's AccountUsageDashboard ThrottleEvent interface
declares totalTokens and recoveryAction but the backend payload uses
tokensAtThrottle and omits recoveryAction; update either side so the shapes
match: either change the backend stats/event emitter to include totalTokens and
recoveryAction (map tokensAtThrottle -> totalTokens and add a recoveryAction
value) or modify the AccountUsageDashboard ThrottleEvent interface and the
throttle history table usage (references to event.totalTokens and
event.recoveryAction) to use event.tokensAtThrottle and handle a missing
recoveryAction (e.g., show placeholder/null guard). Ensure you update the
interface name ThrottleEvent and the table cell render logic accordingly so
rendered values are never undefined.

Virtuosos (multi-account multiplexing) is now an opt-in Encore Feature,
disabled by default. All account event listeners, reconciliation, modals,
menu entries, and UI components are gated so they remain inert until the
user enables Virtuosos in Settings → Encore.

Gate points:
- EncoreFeatureFlags.virtuosos flag + false default (types, settingsStore)
- AccountSelector self-gates via useSettingsStore (returns null when off)
- App.tsx: 6 account event useEffects, reconciliation, default assignment,
  AccountSwitchModal, VirtuososModal, setVirtuososOpen prop
- SessionList: optional setVirtuososOpen prop, hamburger menu conditional
- useSessionListProps: optional setVirtuososOpen in deps interface
- SettingsModal: Virtuosos toggle card in Encore tab
- AccountSelector test mock updated for feature flag

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openasocket
Copy link
Contributor Author

Encore Feature Gating for Virtuosos

Virtuosos is now gated behind the Encore Features toggle system (disabled by default), following the same pattern established by Director's Notes.

Why Encore gating?

Virtuosos (multi-account multiplexing) is a power-user feature that most users won't need out of the box. Gating it behind Encore ensures:

  • Clean default UX — no account-related UI clutter for users who don't use multiple accounts
  • Zero overhead when disabled — all 6 IPC event listeners, session reconciliation, and default account assignment are skipped entirely
  • Gradual adoption — users opt in when ready, reducing confusion for new users

Gate points (8 files, 12 gate locations)

Location What's gated
types/index.ts EncoreFeatureFlags.virtuosos boolean flag
settingsStore.ts Default virtuosos: false
AccountSelector.tsx Self-gates via useSettingsStore — returns null when disabled
App.tsx (×8) 6 account event useEffects, session reconciliation, default account assignment, both modals (AccountSwitchModal, VirtuososModal), setVirtuososOpen prop
SessionList.tsx Optional setVirtuososOpen prop, hamburger menu button conditional
useSessionListProps.ts Optional setVirtuososOpen in deps interface
SettingsModal.tsx Virtuosos toggle card in Encore tab
AccountSelector.test.tsx Mock useSettingsStore with virtuosos: true

Design decisions

  • Frontend-only gating: Backend (main process) account infrastructure stays always-on — only renderer UI and event listeners are gated. This avoids complexity in the IPC layer.
  • Self-gating pattern for AccountSelector: Rather than threading an enabled prop through InputArea and NewInstanceModal, AccountSelector reads the store directly and returns null. This is the same pattern used by Director's Notes.
  • Reconciliation uses getState(): The session restore reconciliation runs in an async callback where the closure value of encoreFeatures could be stale, so it reads directly from the store via useSettingsStore.getState().

✅ TypeScript compiles clean, 482/482 test files pass (20,186 tests)

Copy link

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@src/renderer/App.tsx`:
- Around line 1245-1289: Replace the silent console.error and empty catch usage
around account reconciliation and assignment with Sentry reporting: import and
call captureException from src/utils/sentry inside the catch blocks for
window.maestro.accounts.reconcileSessions and the per-session
window.maestro.accounts.assign promise rejections (and the other listed ranges),
include contextual metadata (e.g., session.id, session.accountId, reconciliation
details) when calling captureException, and either rethrow/return a structured
failure result instead of swallowing the error so callers can handle it; update
handling in the setSessions/reconciliation flow (functions/variables:
setSessions, restoredSessions, reconciliation,
window.maestro.accounts.reconcileSessions, window.maestro.accounts.assign) to
propagate or surface errors after reporting.

In `@src/renderer/components/AccountSelector.tsx`:
- Around line 56-65: The current try/catch in the useEffect around
window.maestro.accounts.list() silently logs failures with console.warn;
instead, handle only expected/recoverable errors explicitly (e.g., check known
error codes from the accounts.list() response) and for any unexpected errors
call Sentry utilities (import captureException or captureMessage from
src/utils/sentry) with context (e.g., component name "AccountSelector" and
operation "fetch accounts"), then rethrow or let the error bubble so it is
tracked; keep the cancelled flag and setAccounts(list) behavior unchanged for
success paths and avoid swallowing unknown exceptions in the useEffect wrapper.

In `@src/renderer/components/SessionList.tsx`:
- Around line 974-977: The tooltip currently shows account info only when
session.accountName exists; update the rendering in SessionList (the JSX block
that renders the div with className "text-[10px] mb-2") to fall back to
session.accountId when session.accountName is falsy so the tooltip content
becomes Account: (session.accountName || session.accountId), and ensure the
element is rendered whenever either value is present while preserving the
existing styling using theme.colors.textDim and theme.colors.accent.

Comment on lines +1245 to +1289
// Reconcile account assignments after session restore (ACCT-MUX-13)
// This validates accounts still exist and updates customEnvVars accordingly
if (useSettingsStore.getState().encoreFeatures.virtuosos) try {
const activeIds = restoredSessions.map(s => s.id);
const reconciliation = await window.maestro.accounts.reconcileSessions(activeIds);
if (reconciliation.success && reconciliation.corrections.length > 0) {
setSessions(prev => prev.map(session => {
const correction = reconciliation.corrections.find(c => c.sessionId === session.id);
if (!correction) return session;

if (correction.status === 'removed') {
// Account was removed — clear session's account fields and CLAUDE_CONFIG_DIR
const cleanedEnvVars = { ...session.customEnvVars };
delete cleanedEnvVars.CLAUDE_CONFIG_DIR;
return {
...session,
accountId: undefined,
accountName: undefined,
customEnvVars: Object.keys(cleanedEnvVars).length > 0 ? cleanedEnvVars : undefined,
};
} else if (correction.configDir && session.accountId) {
// Account exists — ensure CLAUDE_CONFIG_DIR is current
return {
...session,
accountId: correction.accountId ?? undefined,
accountName: correction.accountName ?? undefined,
customEnvVars: {
...session.customEnvVars,
CLAUDE_CONFIG_DIR: correction.configDir,
},
};
}
return session;
}));
}
// Re-register assignments for sessions that have accountId but were
// created before the assign() call was added to session creation
for (const session of restoredSessions) {
if (session.accountId && session.toolType === 'claude-code') {
window.maestro.accounts.assign(session.id, session.accountId).catch(() => {});
}
}
} catch (reconcileError) {
console.error('[App] Account reconciliation failed:', reconcileError);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace silent/console-only catches with Sentry reporting.

These new account-related flows use console.error and empty .catch(() => {}), which silently swallow failures (reconcile/assign/switch/cleanup/default-assign). This both violates the renderer error-handling guideline and can leave session/account state inconsistent. Use captureException from src/utils/sentry with context and either rethrow or return a structured failure; avoid empty catches.

♻️ Example pattern (apply similarly across the listed ranges)
+import { captureException } from './utils/sentry';
...
-} catch (reconcileError) {
-	console.error('[App] Account reconciliation failed:', reconcileError);
-}
+} catch (reconcileError) {
+	captureException(reconcileError, { tags: { area: 'accounts.reconcile' } });
+	throw reconcileError;
+}
...
-window.maestro.accounts.assign(session.id, session.accountId).catch(() => {});
+window.maestro.accounts
+	.assign(session.id, session.accountId)
+	.catch((err) =>
+		captureException(err, { tags: { area: 'accounts.assign' }, extra: { sessionId: session.id } })
+	);

As per coding guidelines: “Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry… Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context.”

Also applies to: 1755-1812, 4855-4885, 5415-5419, 5675-5687

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/App.tsx` around lines 1245 - 1289, Replace the silent
console.error and empty catch usage around account reconciliation and assignment
with Sentry reporting: import and call captureException from src/utils/sentry
inside the catch blocks for window.maestro.accounts.reconcileSessions and the
per-session window.maestro.accounts.assign promise rejections (and the other
listed ranges), include contextual metadata (e.g., session.id,
session.accountId, reconciliation details) when calling captureException, and
either rethrow/return a structured failure result instead of swallowing the
error so callers can handle it; update handling in the
setSessions/reconciliation flow (functions/variables: setSessions,
restoredSessions, reconciliation, window.maestro.accounts.reconcileSessions,
window.maestro.accounts.assign) to propagate or surface errors after reporting.

Comment on lines +56 to +65
// Fetch accounts on mount and when dropdown opens (refresh)
useEffect(() => {
let cancelled = false;
(async () => {
try {
const list = (await window.maestro.accounts.list()) as AccountProfile[];
if (!cancelled) setAccounts(list);
} catch (err) {
console.warn('[AccountSelector] Failed to fetch accounts:', err);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t swallow account list failures—report via Sentry.

console.warn hides failures and bypasses error tracking. Handle only expected error codes; otherwise capture and let unexpected errors bubble.

🐛 Suggested fix
@@
-import type { AccountProfile } from '../../shared/account-types';
+import type { AccountProfile } from '../../shared/account-types';
+import { captureException } from '../utils/sentry';
@@
-		} catch (err) {
-			console.warn('[AccountSelector] Failed to fetch accounts:', err);
-		}
+		} catch (err) {
+			captureException(err, {
+				tags: { area: 'AccountSelector', action: 'accounts.list' },
+			});
+			throw err;
+		}
As per coding guidelines: "Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking. Handle only expected/recoverable errors explicitly with specific error codes. Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AccountSelector.tsx` around lines 56 - 65, The
current try/catch in the useEffect around window.maestro.accounts.list()
silently logs failures with console.warn; instead, handle only
expected/recoverable errors explicitly (e.g., check known error codes from the
accounts.list() response) and for any unexpected errors call Sentry utilities
(import captureException or captureMessage from src/utils/sentry) with context
(e.g., component name "AccountSelector" and operation "fetch accounts"), then
rethrow or let the error bubble so it is tracked; keep the cancelled flag and
setAccounts(list) behavior unchanged for success paths and avoid swallowing
unknown exceptions in the useEffect wrapper.

Comment on lines +974 to +977
{session.accountName && (
<div className="text-[10px] mb-2" style={{ color: theme.colors.textDim }}>
Account: <span style={{ color: theme.colors.accent }}>{session.accountName}</span>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tooltip should fall back to accountId when accountName is missing.

Right now the tooltip omits account info when accountName is undefined, while the context menu shows accountId. Using the same fallback keeps the UI consistent.

🔧 Suggested tweak
-{session.accountName && (
+{(session.accountName || session.accountId) && (
 	<div className="text-[10px] mb-2" style={{ color: theme.colors.textDim }}>
-		Account: <span style={{ color: theme.colors.accent }}>{session.accountName}</span>
+		Account:{' '}
+		<span style={{ color: theme.colors.accent }}>
+			{session.accountName || session.accountId}
+		</span>
 	</div>
 )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{session.accountName && (
<div className="text-[10px] mb-2" style={{ color: theme.colors.textDim }}>
Account: <span style={{ color: theme.colors.accent }}>{session.accountName}</span>
</div>
{(session.accountName || session.accountId) && (
<div className="text-[10px] mb-2" style={{ color: theme.colors.textDim }}>
Account:{' '}
<span style={{ color: theme.colors.accent }}>
{session.accountName || session.accountId}
</span>
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` around lines 974 - 977, The tooltip
currently shows account info only when session.accountName exists; update the
rendering in SessionList (the JSX block that renders the div with className
"text-[10px] mb-2") to fall back to session.accountId when session.accountName
is falsy so the tooltip content becomes Account: (session.accountName ||
session.accountId), and ensure the element is rendered whenever either value is
present while preserving the existing styling using theme.colors.textDim and
theme.colors.accent.

Add migration tracking fields (migratedFromSessionId, migratedToSessionId,
migratedAt, archivedByMigration, migrationGeneration) to the Session
interface for Virtuosos provider switching provenance chains.

Add ProviderSwitchConfig interface and DEFAULT_PROVIDER_SWITCH_CONFIG
to shared/account-types.ts for automated provider failover configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/shared/account-types.ts (1)

132-132: Move import statement to the top of the file.

The import of ToolType is placed after export statements. Standard convention is to place all imports at the top of the file for readability and consistency.

Proposed fix

Move line 132 to the top of the file:

 /**
  * Account multiplexing types for managing multiple Claude Code accounts.
  * Supports usage monitoring, limit tracking, and automatic account switching.
  */
+
+import type { ToolType } from './types';

 /** Unique identifier for an account (generated UUID) */
 export type AccountId = string;

And remove from line 132:

 export const DEFAULT_TOKEN_WINDOW_MS = 5 * 60 * 60 * 1000;

-import type { ToolType } from './types';
-
 /**
  * Configuration for automated provider failover (Virtuosos vertical swapping).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/account-types.ts` at line 132, Move the import of ToolType so all
imports are grouped at the top of the file: locate the existing import statement
"import type { ToolType }" (currently after export statements) and cut it to
place alongside the other imports at the top of src/shared/account-types.ts,
ensuring no duplicate imports remain and preserving the "type" import modifier
and any ESLint/max-import-order rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/shared/account-types.ts`:
- Line 132: Move the import of ToolType so all imports are grouped at the top of
the file: locate the existing import statement "import type { ToolType }"
(currently after export statements) and cut it to place alongside the other
imports at the top of src/shared/account-types.ts, ensuring no duplicate imports
remain and preserving the "type" import modifier and any ESLint/max-import-order
rules.

…r identity carry-over

Extend CreateMergedSessionOptions with identity (nudgeMessage, bookmarked,
sessionSshRemoteConfig, autoRunFolderPath) and provenance (migratedFromSessionId,
migratedAt, migrationGeneration) fields. Apply them in createMergedSession so
sessions created via provider switching are born complete.

Create useProviderSwitch orchestrator hook that reuses the existing context
transfer pipeline (extractTabContext, contextGroomingService, createMergedSession)
but differs from useSendToAgent: preserves original session name, carries full
identity, pre-loads context in tab logs, and sets provenance fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/hooks/agent/useProviderSwitch.ts`:
- Around line 195-205: The try/catch around
window.maestro.agents.get(targetProvider) in useProviderSwitch.ts currently
swallows all errors; update the catch to distinguish expected "agent not
available" logic from unexpected IPC/infrastructure failures by reporting
unexpected errors to Sentry (or the app's error-tracking) and rethrowing (or
returning a failed state) so they don't silently continue; keep the existing
availability check using agentStatus and getAgentDisplayName(targetProvider) but
if agentCheckError looks like an infrastructure/IPC error, call the app's error
reporter (e.g., captureException) with agentCheckError and then throw the error
to fail fast.
- Around line 39-50: The ProviderSwitchRequest type includes archiveSource but
the switchProvider implementation in useProviderSwitch never uses it; either
remove archiveSource from the ProviderSwitchRequest interface or implement
archiving logic inside switchProvider (or the useProviderSwitch hook) that, when
archiveSource is true, calls the existing session-archiving routine (e.g.,
invoke the archiveSession/archiveCurrentTab function or dispatch the archive
action used elsewhere) after switching providers; update any callers or tests
accordingly to reflect the chosen approach and keep ProviderSwitchRequest and
switchProvider signatures consistent.

Comment on lines +39 to +50
export interface ProviderSwitchRequest {
/** Source session to switch from */
sourceSession: Session;
/** Tab ID within source session (active tab) */
sourceTabId: string;
/** Target provider to switch to */
targetProvider: ToolType;
/** Whether to groom context for target provider */
groomContext: boolean;
/** Whether to auto-archive source session after switch */
archiveSource: boolean;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'archiveSource' src/renderer/hooks/agent/useProviderSwitch.ts

Repository: RunMaestro/Maestro

Length of output: 119


🏁 Script executed:

sed -n '90,110p' src/renderer/hooks/agent/useProviderSwitch.ts

Repository: RunMaestro/Maestro

Length of output: 772


🏁 Script executed:

sed -n '140,200p' src/renderer/hooks/agent/useProviderSwitch.ts

Repository: RunMaestro/Maestro

Length of output: 1751


🏁 Script executed:

rg -n 'archive' src/renderer/hooks/agent/useProviderSwitch.ts

Repository: RunMaestro/Maestro

Length of output: 236


Unused parameter: archiveSource is defined but never used.

The archiveSource field is defined in ProviderSwitchRequest but is not destructured or used anywhere in the switchProvider function implementation. Either implement the archive logic within this hook, or remove the field if archiving is handled by the caller.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/agent/useProviderSwitch.ts` around lines 39 - 50, The
ProviderSwitchRequest type includes archiveSource but the switchProvider
implementation in useProviderSwitch never uses it; either remove archiveSource
from the ProviderSwitchRequest interface or implement archiving logic inside
switchProvider (or the useProviderSwitch hook) that, when archiveSource is true,
calls the existing session-archiving routine (e.g., invoke the
archiveSession/archiveCurrentTab function or dispatch the archive action used
elsewhere) after switching providers; update any callers or tests accordingly to
reflect the chosen approach and keep ProviderSwitchRequest and switchProvider
signatures consistent.

Comment on lines +195 to +205
try {
const agentStatus = await window.maestro.agents.get(targetProvider);
if (!agentStatus?.available) {
throw new Error(
`${getAgentDisplayName(targetProvider)} is not available. Please install and configure it first.`
);
}
} catch (agentCheckError) {
// If we can't check, log warning but continue
console.warn('Could not verify agent availability:', agentCheckError);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Swallowed exception may mask infrastructure failures.

The catch block logs a warning but continues execution. If window.maestro.agents.get() fails due to IPC issues or other infrastructure problems, the operation will proceed and fail later with a less informative error.

Consider either letting the error propagate (fail fast) or explicitly handling expected failure cases while reporting unexpected ones to Sentry.

🛡️ Suggested fix to report unexpected errors
 try {
 	const agentStatus = await window.maestro.agents.get(targetProvider);
 	if (!agentStatus?.available) {
 		throw new Error(
 			`${getAgentDisplayName(targetProvider)} is not available. Please install and configure it first.`
 		);
 	}
 } catch (agentCheckError) {
-	// If we can't check, log warning but continue
-	console.warn('Could not verify agent availability:', agentCheckError);
+	// Only continue if it's a timeout or network error; otherwise fail fast
+	if (agentCheckError instanceof Error && agentCheckError.message.includes('timeout')) {
+		console.warn('Agent availability check timed out, continuing:', agentCheckError);
+	} else {
+		throw agentCheckError;
+	}
 }

As per coding guidelines: "Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const agentStatus = await window.maestro.agents.get(targetProvider);
if (!agentStatus?.available) {
throw new Error(
`${getAgentDisplayName(targetProvider)} is not available. Please install and configure it first.`
);
}
} catch (agentCheckError) {
// If we can't check, log warning but continue
console.warn('Could not verify agent availability:', agentCheckError);
}
try {
const agentStatus = await window.maestro.agents.get(targetProvider);
if (!agentStatus?.available) {
throw new Error(
`${getAgentDisplayName(targetProvider)} is not available. Please install and configure it first.`
);
}
} catch (agentCheckError) {
// Only continue if it's a timeout or network error; otherwise fail fast
if (agentCheckError instanceof Error && agentCheckError.message.includes('timeout')) {
console.warn('Agent availability check timed out, continuing:', agentCheckError);
} else {
throw agentCheckError;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/agent/useProviderSwitch.ts` around lines 195 - 205, The
try/catch around window.maestro.agents.get(targetProvider) in
useProviderSwitch.ts currently swallows all errors; update the catch to
distinguish expected "agent not available" logic from unexpected
IPC/infrastructure failures by reporting unexpected errors to Sentry (or the
app's error-tracking) and rethrowing (or returning a failed state) so they don't
silently continue; keep the existing availability check using agentStatus and
getAgentDisplayName(targetProvider) but if agentCheckError looks like an
infrastructure/IPC error, call the app's error reporter (e.g., captureException)
with agentCheckError and then throw the error to fail fast.

Add the confirmation modal for Virtuosos provider switching (VSWITCH-03).
Includes agent detection, radio-button provider selection with availability
badges, groom context and archive source checkboxes, token estimation,
and keyboard navigation. Uses Modal base component with layer stack
registration for Escape handling. PROVIDER_SWITCH priority placed at 1003,
near ACCOUNT_SWITCH at 1005.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/components/SwitchProviderModal.tsx`:
- Around line 239-240: The div with the onKeyDown handler (the element using
handleKeyDown) cannot receive keyboard events because it lacks focusability; add
tabIndex={0} to that div and add the "outline-none" class to preserve visual
styling (remove the eslint disable comment afterwards) so keyboard navigation
works and handleKeyDown will fire as intended.
- Around line 108-110: In SwitchProviderModal's catch block (the one that
currently does console.error('Failed to detect agents for provider switch:',
err)), replace the silent console logging with a call to our Sentry reporting
utility (e.g., reportError or captureException) and pass the caught error along
with contextual metadata (provider IDs, selected provider, user action such as
"switch provider", and any relevant state from detectAgentsForProvider or
detectAgents call) so errors are recorded in Sentry with context for debugging;
keep console.error only if you want local logs but ensure the Sentry call is the
primary error reporting mechanism.

Comment on lines +108 to +110
} catch (err) {
console.error('Failed to detect agents for provider switch:', err);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent exception swallowing violates error handling guidelines.

The catch block logs to console.error but doesn't report to Sentry. Per coding guidelines, use Sentry utilities for error reporting with context rather than silently swallowing exceptions.

🛡️ Proposed fix to report error to Sentry
+import { captureException } from '../utils/sentry';
 			} catch (err) {
-				console.error('Failed to detect agents for provider switch:', err);
+				captureException(err, {
+					tags: { component: 'SwitchProviderModal' },
+					extra: { sourceToolType: sourceSession.toolType },
+				});
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SwitchProviderModal.tsx` around lines 108 - 110, In
SwitchProviderModal's catch block (the one that currently does
console.error('Failed to detect agents for provider switch:', err)), replace the
silent console logging with a call to our Sentry reporting utility (e.g.,
reportError or captureException) and pass the caught error along with contextual
metadata (provider IDs, selected provider, user action such as "switch
provider", and any relevant state from detectAgentsForProvider or detectAgents
call) so errors are recorded in Sentry with context for debugging; keep
console.error only if you want local logs but ensure the Sentry call is the
primary error reporting mechanism.

Comment on lines +239 to +240
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div className="flex flex-col gap-4" onKeyDown={handleKeyDown}>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing tabIndex prevents keyboard navigation from working.

The <div> with onKeyDown handler lacks tabIndex={0}, so it cannot receive focus and keyboard events won't fire. The eslint-disable comment acknowledges this accessibility gap. Per coding guidelines, add tabIndex={0} and outline-none class to ensure focus works correctly.

⌨️ Proposed fix to enable keyboard focus
-			{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
-			<div className="flex flex-col gap-4" onKeyDown={handleKeyDown}>
+			<div
+				className="flex flex-col gap-4 outline-none"
+				onKeyDown={handleKeyDown}
+				tabIndex={0}
+				ref={(el) => el?.focus()}
+			>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div className="flex flex-col gap-4" onKeyDown={handleKeyDown}>
<div
className="flex flex-col gap-4 outline-none"
onKeyDown={handleKeyDown}
tabIndex={0}
ref={(el) => el?.focus()}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SwitchProviderModal.tsx` around lines 239 - 240, The
div with the onKeyDown handler (the element using handleKeyDown) cannot receive
keyboard events because it lacks focusability; add tabIndex={0} to that div and
add the "outline-none" class to preserve visual styling (remove the eslint
disable comment afterwards) so keyboard navigation works and handleKeyDown will
fire as intended.

openasocket and others added 5 commits February 19, 2026 02:58
… Agent modal

- Add "Switch Provider..." menu item to SessionContextMenu (after Edit Agent)
- Thread onSwitchProvider callback through SessionList and useSessionListProps
- Add "Switch..." button to Edit Agent modal's provider section
- Thread onSwitchProviderFromEdit through AppSessionModals and AppMasterModals
- All entry points gated behind optional callbacks (hidden when Virtuosos disabled)
- Actual SwitchProviderModal wiring deferred to VSWITCH-05

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add provider switching UI integration:
- Import SwitchProviderModal and useProviderSwitch hook
- Add switchProviderSession state for tracking which session is being switched
- Create handleSwitchProvider and handleConfirmProviderSwitch callbacks
- Pass handleSwitchProvider to useSessionListProps (gated behind encoreFeatures.virtuosos)
- Pass onSwitchProviderFromEdit to AppModals for EditAgentModal integration
- Render SwitchProviderModal in JSX near AccountSwitchModal and VirtuososModal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sessions with archivedByMigration render at 40% opacity, show a grey
hollow status dot (no animation), and display "Provider switched —
archived" indicator text below the session name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…er config, and migration history

- Extend VirtuososModal from 2 tabs to 3: Accounts, Providers, Usage
- Rename "Configuration" tab to "Accounts" for clarity
- Create ProviderPanel component with three sections:
  1. Provider Status Grid showing detected agents with availability and session counts
  2. Failover Configuration with toggles, thresholds, and ordered fallback list
  3. Migration History timeline of past provider switches
- Update tests for new tab structure and add Providers tab coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…PC API, and App.tsx wiring

Implements VSWITCH-08: sliding-window error tracker that monitors consecutive
agent errors per session and emits failover suggestions when the configured
threshold is reached. The renderer subscribes to these events and opens the
SwitchProviderModal (or shows a toast for auto-switch).

- ProviderErrorTracker class with configurable threshold, window, and fallback list
- Only counts recoverable, provider-level errors (rate_limited, network_error,
  agent_crashed, auth_expired) — not token_exhaustion or session_not_found
- IPC handlers (providers:get-error-stats, get-all-error-stats, clear-session-errors)
- Preload API (window.maestro.providers namespace)
- Error listener integration: feeds agent errors into tracker
- Query-complete listener: resets error count on successful response
- App.tsx: failover suggestion subscription gated by encoreFeatures.virtuosos
- Clears provider error state on successful provider switch
- 21 unit tests covering all tracker behaviors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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

Comments