Files
paseo/plan.md
Mohamed Boudra 4ab79b1a50 feat: add multi-daemon support with session routing and connection management
Introduce DaemonRegistry and DaemonConnections contexts for managing multiple server connections. Add session directory for aggregating agents across daemons, with automatic routing based on agent/server context.

Key changes:
- Add daemon profile management (add/edit/remove/set default)
- Implement connection state tracking with reconnection and exponential backoff
- Create useAggregatedAgents hook for unified agent list across daemons
- Add useDaemonSession and useDaemonRequest hooks for daemon-scoped operations
- Update agent routes to include serverId for proper session routing
- Add connection health indicators on home screen and settings
- Persist session snapshots per daemon for faster hydration on reconnect
- Guard screens gracefully when target daemon is offline
- Add React Query for consistent loading/error state management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 08:25:02 +01:00

15 KiB
Raw Blame History

Multi-Daemon Production Rollout Plan

Guiding Principles

  • The app always reflects the union of every connected daemon—no manual switching to “see” data.
  • Actions (create/resume agent, browse files, realtime tools) automatically route to the correct daemon based on the agent/workspace context.
  • Connection health, loading states, and mutations are observable and resilient (React Query or equivalent).

Workstreams & Tasks

1. Session Directory & Data Consistency

  • Rebuild useSessionDirectory so it stays in sync with realtime session state (agents, permissions, stream updates) instead of caching an accessor forever.
    • Added session-level subscriptions so the directory re-renders on daemon updates and ran npm run typecheck to verify.
  • Expose a lightweight subscription/API for background SessionProviders so any change invalidates aggregated consumers without forcing rerenders of the main tree.
    • Added central session-directory listeners in the daemon connections context, had SessionProviders emit invalidations, updated useSessionDirectory to use the new API, and ran npm run typecheck.
  • Audit every consumer that still reaches for useSession() directly (AgentStreamView, AgentInputArea, file explorers, realtime, etc.) and ensure they receive the session instance that corresponds to the agent/daemon theyre operating on.
    • Agent detail UI now pulls daemon-scoped sessions via useDaemonSession, so sending messages, toggling modes, and inline file explorers all operate against the routes serverId (see packages/app/src/components/agent-input-area.tsx:72, packages/app/src/components/agent-stream-view.tsx:31, packages/app/src/components/agent-status-bar.tsx:6, packages/app/src/app/agent/[serverId]/[agentId].tsx:256) and npm run typecheck passes.

2. Aggregated Agent Experience

  • Replace the home screens agents.size gate with useAggregatedAgents, render the merged list (grouped/sorted by daemon), and remove the daemon picker UI from the header.
    • Home now builds grouped daemon sections via useAggregatedAgents, AgentList renders each section with the correct serverId routing, the header exposes dedicated import/create buttons without manual daemon switching, and npm run typecheck passes.
    • When closing the agent action sheet we now clear the stored serverId so useDaemonSession falls back to the active daemon and the Home screen no longer crashes if that background daemon disconnects after a long-press.
  • Ensure all agent rows carry their serverId through navigation (Agent screen, diff viewer, file explorer, orchestrator) so every deep link includes /agent/[serverId]/[agentId].
    • Annotated every agent snapshot with its daemon serverId, updated shared row components (AgentList, AgentSidebar, ActiveProcesses) to read it when navigating/deleting, and re-ran npm run typecheck.
  • Update stream detail routes (git diff, file explorer) to validate the daemon session from params and gracefully show status/loading/error states if the background session is unavailable.
    • Wrapped both routes in session guards that render connection-aware placeholders when the target daemon is offline/unavailable, moved the existing logic into gated child components, and re-ran npm run typecheck to verify.
  • Guard the main agent route when the requested daemon session is unavailable so deep links or quick daemon switches don't crash the screen.
    • Added a connection-aware guard around /agent/[serverId]/[agentId] that renders a friendly placeholder when the session is offline/unavailable and re-ran npm run typecheck.
  • Fix the agent screen dropdown positioning so we don't double-apply the safe-area offset and remove the leaked debug logging.
    • packages/app/src/app/agent/[serverId]/[agentId].tsx:320-339 adds insets.top to measureInWindow coordinates (which already include the status bar) and emits [Menu] console logs on every open, so the action menu renders ~40px too low on notched devices and spams the JS console.
    • Removed the extra insets.top offset, cleaned up the [Menu] debug logs, and re-ran npm run typecheck.
  • Make inline file path navigation pick up the correct daemon id even when two daemons generate the same agentId.
    • Added resolvedServerId to the handleInlinePathPress dependency list in packages/app/src/components/agent-stream-view.tsx:83-116, so navigating after switching daemons now routes to the correct file explorer target; re-ran npm run typecheck.
  • Sync the active daemon context with the agent route so realtime/audio flows hit the same websocket as the rendered agent, even when arriving from a deep link.
    • AgentScreen looks up the requested session via useDaemonSession but never calls setActiveDaemonId, so opening /agent/[serverB]/[agentId] while daemon A is active leaves the global SessionProvider/RealtimeProvider pointed at A (see packages/app/src/app/agent/[serverId]/[agentId].tsx:74-138).
    • AgentInputArea forwards realtime/voice interactions through useRealtime() and the active ws (packages/app/src/components/agent-input-area.tsx:656-744), so with the mismatch above those commands get sent to daemon A instead of the daemon hosting the open agent.
    • Added a route-aware effect in packages/app/src/app/agent/[serverId]/[agentId].tsx that synchronizes setActiveDaemonId with the screen's serverId param so the root Session/Realtime providers follow deep links, and ran npm run typecheck --workspace=@paseo/app.
  • Restore the legacy /agent/[id] route as a compatibility shim so old deep links (without a daemon id) keep working while we transition the UI.
    • Added packages/app/src/app/agent/[id].tsx, which scans the session directory for the requested agent, auto-redirects when theres a single match, and lets the user choose when multiple daemons share that id; registered the screen in _layout.tsx and re-ran npm run typecheck --workspace=@paseo/app.
  • Keep background agent actions from swapping the active daemon just to open the action sheet or delete.
    • AgentList still called setActiveDaemonId on long-press and before deletion (packages/app/src/components/agent-list.tsx:19-63), so managing a background daemons agent on Home would tear down the active websocket/realtime session. Removed those calls and rely on useDaemonSession so the aggregated view no longer hijacks the global session for contextual actions.
  • Allow guarded screens to opt out of the useDaemonSession alert so offline placeholders dont trigger duplicate system popups.
    • Added a suppressUnavailableAlert option to useDaemonSession, had Agent, Git diff, and File Explorer guards opt in so they only render their inline placeholders, and ran npm run typecheck --workspace=@paseo/app.
  • Force the root SessionProvider to remount whenever the active daemon changes so session state never leaks between servers.
    • Added key={activeDaemon.id} in _layout.tsx, ensuring each daemon gets a fresh session tree so cached agents/permissions dont show up under the wrong daemon while switching routes.

3. Agent Creation & Lifecycle Actions

  • Remove the server selector from the home header; inside the Create/Import modal replace the current “session swap” approach with an explicit serverId prop that simply determines which daemon receives the mutation.
    • Create/Import modals now accept a serverId prop, stop mutating the active daemon, and every caller (home screen, footer, agent view) passes the daemon id theyre operating against; ran npm run typecheck.
  • Prevent useDaemonSession from throwing during modal render when a daemon is offline—surface that state inside the UI (disabled create button + inline error) and keep the chip selection responsive.
    • Currently selecting a daemon chip whose session isnt connected (e.g., auto-connect disabled or still initializing) crashes CreateAgentModal because useDaemonSession rethrows immediately; we need to gate the selection and show an inline “connect first” state instead of exploding.
    • Confirmed this is still happening: CreateAgentModal passes the selected serverId straight into useDaemonSession (packages/app/src/components/create-agent-modal.tsx:233), so tapping an offline daemon chip kills the modal before we can render error UI.
    • Updated CreateAgentModal to read sessions from the directory, block websocket actions while the target daemon is offline, surface a daemon availability warning, and disable create/import flows until the daemon connects; ran npm run typecheck.
  • When creating/resuming/cloning agents, route follow-up navigation and queued requests to the daemon returned in the success payload rather than assuming the active daemon changed.
    • CreateAgentModal now calls setActiveDaemonId with the server from the success payload before pushing the agent route, so the global SessionProvider/RealtimeProvider swaps to the correct daemon and realtime controls no longer stay bound to the previous server (see packages/app/src/components/create-agent-modal.tsx:210-220 and packages/app/src/components/create-agent-modal.tsx:845-864); re-ran npm run typecheck.
  • Block Create/Import repo + snapshot fetches when the selected daemon is offline so we surface the availability error instead of spinning forever.
    • Guarded requestRepoInfo/requestImportCandidates with the daemon availability signal so offline sessions now surface daemonAvailabilityError immediately instead of issuing ws.send, then added the missing sheetDeleteTextDisabled style in packages/app/src/components/agent-list.tsx to clear the lingering npm run typecheck --workspace=@paseo/app failure.
  • Restore the Import Agent flow (modal entry, mutation wiring, navigation) without undoing the multi-daemon routing work from previous steps.
    • After reworking Home/Header and the modals, the import trigger disappeared and existing deep links no longer reach a functioning flow. Bring the Import CTA back (Home, footer, agent screen), ensure it accepts a daemon id, and verify the import mutation routes to the selected daemon without regressing the new server-aware navigation.
    • Rewired the import buttons across Home (header + empty state with deep-link auto open), the global footer, and the agent action menu so every trigger passes the correct daemon id into ImportAgentModal, and ran npm run typecheck --workspace=@paseo/app.

4. Connection State & Persistence

  • Stop the Settings daemon list from firing “Daemon unavailable” alerts when background daemons are offline by reading session snapshots via useSessionForServer and only performing restart/test flows when a session is actually mounted.
    • Updated packages/app/src/app/settings.tsx to rely on useSessionForServer for both the active daemon and each DaemonCard, so offline entries no longer call useDaemonSession (which showed alerts) and npm run typecheck still passes.
  • Introduce React Query (or a similar observable store) around AsyncStorage-backed registries (DaemonRegistryProvider, DaemonConnectionsProvider, app settings) so callers get loading/error states without bespoke hooks.
    • Added @tanstack/react-query with a root provider, refactored the daemon registry, connections, and app settings to load/persist via cached queries (surfacing shared loading/error states) and verified everything with npm run typecheck --workspace=@paseo/app.
  • Add background reconnection + exponential backoff per daemon; surface “connecting/offline/last error” indicators in settings and home.
    • WebSocket sessions now retry with exponential backoff and feed precise status/error metadata into the daemon connection store, the home screen shows a connection health banner, settings display colored status badges plus last errors for every daemon, and npm run typecheck --workspace=@paseo/app passes.
  • Persist the last successful session snapshot per daemon so the UI can hydrate agent lists immediately while a websocket reconnects.
    • SessionProviders now hydrate agents/permissions/commands from the last stored session_state snapshot, persist new snapshots to AsyncStorage per daemon, and npm run typecheck --workspace=@paseo/app passes.
  • Standardize request/response handling behind a shared hook (React-Query style states for idle/loading/success/error, request dedupe, retries, timeouts) and document how daemon-facing components consume it.
    • Added useDaemonRequest with deduped execution, timeout/retry controls, and React Query-style metadata plus wrote docs/daemon-request-hook.md describing how daemon clients consume it; ran npm run typecheck --workspace=@paseo/app.
  • Replace ad-hoc websocket request flows (git info, permission responses, diff/file fetches, etc.) with the new hook so every async action exposes consistent status + cancellation semantics.
    • Adopted useDaemonRequest for repo-inspection modals, permission cards, git diff, and file explorer interactions (with inline loading/error states) and re-ran npm run typecheck --workspace=@paseo/app.

5. Performance & UX Polish

  • Ensure agent image attachments preserve MIME metadata and are base64 encoded before hitting the daemon.
    • packages/app/src/contexts/session-context.tsx:1198 now accepts { uri, mimeType } attachments and reads them via expo-file-system, and packages/app/src/components/agent-input-area.tsx:140 forwards the stored metadata so queued sends no longer drop screenshots (npm run typecheck passes).
  • Profile the Create Agent modal—debounce expensive effects (e.g., provider model fetches) per server and prefetch metadata when daemons are idle to eliminate the visible lag when switching targets.
  • Audit websocket usage so background SessionProviders never duplicate connections for the active daemon (one live connection per daemon id).
  • Enforce “impossible states are impossible” across UI/data models (strict typing, discriminated unions, exhaustive switches) so complex flows remain clean without relying on Expo E2E tests.

6. Observability & Tooling

  • Add structured logging for daemon connection lifecycle (connect, error, auto-connect skip) so we can diagnose “multi daemon” issues from device logs.
  • Emit analytics when users create/resume agents on background daemons, attempt actions while those daemons are offline, or switch default daemons—helps prioritize reconnection UX.
  • Document the architecture in docs/multi-daemon.md (registry, sessions, routing rules) so future contributors understand how to extend it.
  • Land the accumulated multi-daemon changes in source control with a clean commit (linted, type-checked, plan updated).

Review

  • 2025-11-26 Reviewer sanity check for the recent multi-daemon rollout work.
    • Confirmed the new useDaemonSession hook, guarded Agent/Git Diff/File Explorer screens, and server-aware Create/Import flows align with the documented fixes; no regressions or missing follow-ups spotted, so no additional tasks were opened.
  • 2025-11-26 Reviewer follow-up on session isolation across daemons.
    • Found that the active SessionProvider kept its React state when switching daemons, so stale agents/permissions could leak between server contexts; fixed by keying the provider in _layout.tsx so the tree remounts on each daemon change.