feat: Virtuosos — multi-account multiplexing and capacity-aware routing#391
feat: Virtuosos — multi-account multiplexing and capacity-aware routing#391openasocket wants to merge 43 commits intoRunMaestro:mainfrom
Conversation
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>
…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>
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
… 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>
There was a problem hiding this comment.
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 | 🟠 MajorException 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 | 🟡 MinorAdd a skeleton for the new “accounts” view to avoid a blank loading state.
viewModenow allows'accounts', butDashboardSkeletondoesn’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 | 🟠 MajorAccount environment injection is lost after SSH wrapping.
The
CLAUDE_CONFIG_DIRinjected at lines 196-204 is stored inspawnEnvVars, but the SSH wrapping block at lines 209-233 passes the originalconfigResolution.effectiveCustomEnvVars ?? effectiveEnvVars(line 217) towrapSpawnWithSsh, then overwritesspawnEnvVarswithsshWrapped.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 | 🟠 MajorSSH spawns drop the injected account env.
At Line 541 and Line 947 the SSH wrapper receives pre‑injection env vars, soCLAUDE_CONFIG_DIRdoesn’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 | 🟠 MajorPropagate injected account env into SSH command build.
Line 443 passeseffectiveCustomEnvVars, which omits the injectedCLAUDE_CONFIG_DIR. Remote sessions will run under the wrong account. UsecustomEnvVarsToPassinstead.🔧 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 | 🟠 MajorAdd 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 nounassignAPI call, onlyassignandexecuteSwitch. Additionally, theAccountSelectorcomponent currently offers no UI affordance to unassign an account.To properly support unassignment:
- Add an explicit unassign handler in the account registry IPC (or extend
assignto accept null/undefined).- Update the Edit Agent handler to call the unassign method when
accountIdis falsy.- Consider adding a "Clear" or "No Account" option to the
AccountSelectordropdown 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.
| interface ThrottleEvent { | ||
| timestamp: number; | ||
| accountId: string; | ||
| accountName?: string; | ||
| reason: string; | ||
| totalTokens: number; | ||
| recoveryAction?: string; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "ThrottleEvent" -g'*.ts' -g'*.tsx' -C3Repository: RunMaestro/Maestro
Length of output: 17364
🏁 Script executed:
rg -n "getThrottleEvents" -g'*.ts' -g'*.tsx' -C3Repository: RunMaestro/Maestro
Length of output: 8037
🏁 Script executed:
rg -n "tokensAtThrottle" -g'*.ts' -g'*.tsx' -C3Repository: RunMaestro/Maestro
Length of output: 8450
🏁 Script executed:
rg -n "totalTokens\|recoveryAction" -g'*.ts' -g'*.tsx' -C2Repository: RunMaestro/Maestro
Length of output: 44
🏁 Script executed:
sed -n '575,639p' src/renderer/components/UsageDashboard/AccountUsageDashboard.tsxRepository: RunMaestro/Maestro
Length of output: 2813
🏁 Script executed:
rg -n "totalTokens|recoveryAction" src/renderer/components/UsageDashboard/ -C2Repository: RunMaestro/Maestro
Length of output: 9969
🏁 Script executed:
# Check if throttleEvents are iterated/rendered
rg -n "throttleEvents\." src/renderer/components/UsageDashboard/ -A2Repository: 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>
Encore Feature Gating for VirtuososVirtuosos 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:
Gate points (8 files, 12 gate locations)
Design decisions
✅ TypeScript compiles clean, 482/482 test files pass (20,186 tests) |
There was a problem hiding this comment.
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.
| // 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); | ||
| } |
There was a problem hiding this comment.
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.
| // 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); | ||
| } |
There was a problem hiding this comment.
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;
+ }🤖 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.
| {session.accountName && ( | ||
| <div className="text-[10px] mb-2" style={{ color: theme.colors.textDim }}> | ||
| Account: <span style={{ color: theme.colors.accent }}>{session.accountName}</span> | ||
| </div> |
There was a problem hiding this comment.
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.
| {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>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/shared/account-types.ts (1)
132-132: Move import statement to the top of the file.The import of
ToolTypeis 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>
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n 'archiveSource' src/renderer/hooks/agent/useProviderSwitch.tsRepository: RunMaestro/Maestro
Length of output: 119
🏁 Script executed:
sed -n '90,110p' src/renderer/hooks/agent/useProviderSwitch.tsRepository: RunMaestro/Maestro
Length of output: 772
🏁 Script executed:
sed -n '140,200p' src/renderer/hooks/agent/useProviderSwitch.tsRepository: RunMaestro/Maestro
Length of output: 1751
🏁 Script executed:
rg -n 'archive' src/renderer/hooks/agent/useProviderSwitch.tsRepository: 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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>
There was a problem hiding this comment.
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.
| } catch (err) { | ||
| console.error('Failed to detect agents for provider switch:', err); | ||
| } |
There was a problem hiding this comment.
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.
| {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} | ||
| <div className="flex flex-col gap-4" onKeyDown={handleKeyDown}> |
There was a problem hiding this comment.
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.
| {/* 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.
… 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>
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
What's Included (32 commits, 148 files, ~14k lines)
Core Infrastructure
window.maestro.accounts.*)Multiplexing Engine
selectNextAccount()using historical usage dataUI Surfaces
CLI & Batch
maestro accountsCLI command for headless account managementTesting
Test Plan
npm run test— all new and existing tests passnpm run lint— no type errorsmaestro accountssubcommands🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes