agent-ecosystem/.planning/phases/02-service-infrastructure/02-02-PLAN.md
matt 85f34eb828 docs(02-service-infrastructure): create phase plan (3 plans, 3 waves)
Plan 01: ServiceContext + ServiceContextRegistry + dispose() methods
Plan 02: Wire registry into main/index.ts + update IPC routing
Plan 03: Context management IPC + preload bridge + connection profiles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:49:19 +00:00

15 KiB

phase plan type wave depends_on files_modified autonomous must_haves
02-service-infrastructure 02 execute 2
02-01
src/main/index.ts
src/main/ipc/handlers.ts
src/main/ipc/sessions.ts
src/main/ipc/projects.ts
src/main/ipc/search.ts
src/main/ipc/subagents.ts
src/main/ipc/ssh.ts
true
truths artifacts key_links
main/index.ts creates a ServiceContextRegistry instead of individual global service variables
Local context is created and registered at startup, behaving identically to current behavior
IPC handlers route through the registry to get service instances, not module-level variables
SSH connect creates a new ServiceContext in the registry instead of destroying/recreating globals
SSH disconnect destroys the SSH context but leaves local context untouched
reinitializeServiceHandlers is no longer needed — handlers always fetch from registry
path provides contains
src/main/index.ts Registry-based service initialization ServiceContextRegistry
path provides contains
src/main/ipc/handlers.ts Registry-aware IPC initialization ServiceContextRegistry
path provides contains
src/main/ipc/sessions.ts Registry-routed session handlers registry
path provides contains
src/main/ipc/projects.ts Registry-routed project handlers registry
path provides contains
src/main/ipc/search.ts Registry-routed search handlers registry
path provides contains
src/main/ipc/subagents.ts Registry-routed subagent handlers registry
from to via pattern
src/main/index.ts src/main/services/infrastructure/ServiceContextRegistry.ts creates and uses registry new ServiceContextRegistry
from to via pattern
src/main/ipc/handlers.ts src/main/services/infrastructure/ServiceContextRegistry.ts receives registry for handler initialization ServiceContextRegistry
from to via pattern
src/main/ipc/sessions.ts src/main/services/infrastructure/ServiceContextRegistry.ts calls registry.getActive() for service resolution registry.getActive()
from to via pattern
src/main/ipc/ssh.ts src/main/services/infrastructure/ServiceContextRegistry.ts creates SSH context and switches via registry registry.(createSshContext|switch|destroy)
Replace the global service variables in main/index.ts with ServiceContextRegistry, and update all IPC handler modules to route through the registry instead of holding module-level service references.

Purpose: This is the core integration that transforms the app from single-mode (global services) to multi-context (registry-managed services). After this plan, SSH connections will create new contexts instead of destroying/recreating globals, and local context remains alive throughout.

Output: Refactored main/index.ts using registry pattern, all IPC handlers routing via registry.getActive().

<execution_context> @.planning/phases/02-service-infrastructure/02-RESEARCH.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/02-service-infrastructure/02-01-PLAN.md @src/main/index.ts @src/main/ipc/handlers.ts @src/main/ipc/sessions.ts @src/main/ipc/projects.ts @src/main/ipc/search.ts @src/main/ipc/subagents.ts @src/main/ipc/ssh.ts @src/main/services/infrastructure/ServiceContext.ts @src/main/services/infrastructure/ServiceContextRegistry.ts Task 1: Refactor main/index.ts to use ServiceContextRegistry src/main/index.ts src/main/ipc/handlers.ts **src/main/index.ts** - Replace global service variables with registry:
  1. Remove these individual service variable declarations:

    • let projectScanner: ProjectScanner;
    • let sessionParser: SessionParser;
    • let subagentResolver: SubagentResolver;
    • let chunkBuilder: ChunkBuilder;
    • let dataCache: DataCache;
    • let fileWatcher: FileWatcher;
    • let cleanupInterval: NodeJS.Timeout | null = null;
  2. Add: let contextRegistry: ServiceContextRegistry;

  3. Rewrite initializeServices():

    • Create sshConnectionManager (unchanged)
    • Create contextRegistry = new ServiceContextRegistry()
    • Create local ServiceContext:
      const localContext = new ServiceContext({
        id: 'local',
        type: 'local',
        fsProvider: new LocalFileSystemProvider(),
      });
      
    • Register: contextRegistry.registerContext(localContext)
    • Start: localContext.start()
    • Initialize notification manager (unchanged — singleton, stays global, not context-scoped)
    • Set notification manager on local context's fileWatcher: localContext.fileWatcher.setNotificationManager(notificationManager)
    • Forward file-change and todo-change events from local context's fileWatcher to renderer (same as current, but using localContext.fileWatcher.on(...))
    • Initialize IPC handlers — change signature to pass contextRegistry instead of individual services. Also pass sshConnectionManager.
    • Forward SSH state changes (unchanged)
  4. Remove the handleModeSwitch callback entirely. SSH connect/disconnect will be handled by the SSH IPC handler using the registry directly (Plan 02 Task 2 below).

  5. Rewrite shutdownServices():

    • Call contextRegistry.dispose() (disposes all contexts including local)
    • Dispose sshConnectionManager
    • Call removeIpcHandlers()
  6. File watcher event forwarding concern: Currently events are wired to the single global fileWatcher. With multi-context, we need to forward events from the ACTIVE context's fileWatcher. Two approaches:

    • Simple (do this): Wire events on local context at startup. When context switches happen (in SSH IPC handler), re-wire events to new active context's fileWatcher.
    • Complex (don't do this): Create a proxy event system.

    For now, wire local context's fileWatcher events at startup. The SSH IPC handler (Task 2) will handle re-wiring on switch.

    Store the event cleanup functions so they can be unwired:

    let fileChangeCleanup: (() => void) | null = null;
    let todoChangeCleanup: (() => void) | null = null;
    

    Create a wireFileWatcherEvents(context: ServiceContext) helper that:

    • Cleans up previous listeners (if any)
    • Adds new listeners to context.fileWatcher for 'file-change' and 'todo-change'
    • Stores cleanup functions Export this function (or make it accessible to IPC handlers via a setter).

    Actually, simpler approach: have the registry emit a 'context-switched' event, and index.ts listens to it to re-wire. But the registry doesn't extend EventEmitter. Even simpler: have switch() in the registry return the previous and current contexts, and let the caller (SSH IPC handler) call a function on index.ts to re-wire.

    Simplest approach: Create and export a rewireFileWatcherEvents(context: ServiceContext, mainWindow: BrowserWindow | null) function from index.ts that the SSH handler can import and call after switching contexts. This function removes old listeners, adds new ones.

src/main/ipc/handlers.ts - Update initialization signature:

  1. Change initializeIpcHandlers signature to accept ServiceContextRegistry instead of individual services:

    export function initializeIpcHandlers(
      registry: ServiceContextRegistry,
      updater: UpdaterService,
      sshManager: SshConnectionManager,
    ): void
    
  2. Inside, pass registry to each domain handler's initialize function:

    • initializeProjectHandlers(registry)
    • initializeSessionHandlers(registry)
    • initializeSearchHandlers(registry)
    • initializeSubagentHandlers(registry)
    • initializeSshHandlers(sshManager, registry) — SSH handler now gets registry instead of mode switch callback
    • initializeUpdaterHandlers(updater) — unchanged (not context-dependent)
  3. Remove reinitializeServiceHandlers() entirely — no longer needed because handlers always call registry.getActive() to get current services.

  4. Keep removeIpcHandlers() unchanged. Run pnpm typecheck — must pass with zero errors. The main/index.ts should no longer have individual service variables (projectScanner, sessionParser, etc.) and should use contextRegistry instead. main/index.ts creates ServiceContextRegistry at startup with local context registered. handlers.ts accepts ServiceContextRegistry. reinitializeServiceHandlers is removed. Type checking passes.

Task 2: Update domain IPC handlers to route via registry src/main/ipc/sessions.ts src/main/ipc/projects.ts src/main/ipc/search.ts src/main/ipc/subagents.ts src/main/ipc/ssh.ts Update each domain IPC handler to receive `ServiceContextRegistry` instead of individual service instances, and call `registry.getActive()` to resolve services at invocation time.

src/main/ipc/projects.ts:

  1. Change initializeProjectHandlers(scanner: ProjectScanner) to initializeProjectHandlers(registry: ServiceContextRegistry)
  2. Store registry as module-level variable: let registry: ServiceContextRegistry;
  3. Remove module-level scanner variable
  4. In each handler, resolve scanner from registry: const { projectScanner } = registry.getActive();
  5. Update all references from scanner. to projectScanner.

src/main/ipc/sessions.ts:

  1. Change initializeSessionHandlers(scanner, parser, resolver, builder, cache) to initializeSessionHandlers(registry: ServiceContextRegistry)
  2. Store registry as module-level variable
  3. Remove module-level scanner, parser, resolver, builder, cache variables
  4. In each handler, destructure from registry: const { projectScanner, sessionParser, subagentResolver, chunkBuilder, dataCache } = registry.getActive();
  5. Update all references to use destructured names

src/main/ipc/search.ts:

  1. Change initializeSearchHandlers(scanner: ProjectScanner) to initializeSearchHandlers(registry: ServiceContextRegistry)
  2. Store registry, resolve from registry.getActive() in handlers
  3. Update references

src/main/ipc/subagents.ts:

  1. Change initializeSubagentHandlers(builder, cache, parser, resolver, scanner) to initializeSubagentHandlers(registry: ServiceContextRegistry)
  2. Store registry, resolve from registry.getActive() in handlers
  3. In the handler, get fsProvider and projectsDir from registry.getActive().projectScanner as was done in Phase 1

src/main/ipc/ssh.ts:

  1. Change initializeSshHandlers(manager, modeSwitchCallback) to initializeSshHandlers(manager: SshConnectionManager, registry: ServiceContextRegistry)
  2. Store registry as module-level variable, remove onModeSwitch callback
  3. Rewrite SSH_CONNECT handler:
    • Call connectionManager.connect(config) (unchanged)
    • Get provider and remoteProjectsPath from connectionManager
    • Generate contextId: ssh-${config.host} (or ssh-${config.host}-${config.port} if port is non-standard)
    • Check if context already exists in registry (reconnection case): if yes, destroy old and create new
    • Create new ServiceContext: new ServiceContext({ id: contextId, type: 'ssh', fsProvider: provider, projectsDir: remoteProjectsPath })
    • Register in registry: registry.registerContext(context)
    • Start context: context.start()
    • Switch registry to new context: registry.switch(contextId)
    • Import and call rewireFileWatcherEvents(context, mainWindow) from index.ts (or accept a callback for this)
    • Return success with status
  4. Rewrite SSH_DISCONNECT handler:
    • Get current contextId from registry if it's SSH type
    • Call connectionManager.disconnect()
    • Call registry.switch('local') to switch back
    • Call registry.destroy(contextId) to clean up SSH context
    • Re-wire file watcher events to local context
    • Return success with status
  5. All other SSH handlers (test, getConfigHosts, resolveHost, saveLastConnection, getLastConnection) remain unchanged — they operate on SshConnectionManager directly, not service contexts.

Important: For the file watcher re-wiring, two approaches:

  • Option A: Export rewireFileWatcherEvents from index.ts (simple but circular dependency risk)
  • Option B: Pass a callback to initializeSshHandlers that the SSH handler calls after switching
  • Option C: Have initializeSshHandlers also accept mainWindow reference and handle rewiring internally

Choose Option B: Add a onContextSwitched: (context: ServiceContext) => void callback parameter to initializeSshHandlers. index.ts passes a callback that re-wires FileWatcher events and sends context-changed notification to renderer.

Updated signature: initializeSshHandlers(manager: SshConnectionManager, registry: ServiceContextRegistry, onContextSwitched: (context: ServiceContext) => void)

In index.ts, the callback:

const onContextSwitched = (context: ServiceContext) => {
  // Re-wire file watcher events
  rewireFileWatcherEvents(context);
  // Notify renderer
  if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send(SSH_STATUS, sshConnectionManager.getStatus());
    mainWindow.webContents.send('context-changed', {
      contextId: context.id,
      type: context.type,
    });
  }
};

The rewireFileWatcherEvents function in index.ts removes old listeners and adds new ones from context.fileWatcher, forwarding to mainWindow.

  1. Run pnpm typecheck — must pass with zero errors
  2. Run pnpm test — all existing tests must pass (IPC handler tests may need mock updates)
  3. Verify no remaining module-level service variables in sessions.ts, projects.ts, search.ts, subagents.ts (should all use registry)
  4. Verify ssh.ts creates ServiceContext on connect and destroys on disconnect All domain IPC handlers (sessions, projects, search, subagents) resolve services via registry.getActive() at invocation time. SSH handler creates/destroys ServiceContext instances and switches registry on connect/disconnect. No module-level service variables remain — all routing goes through the registry. reinitializeServiceHandlers is fully removed. All tests pass.
1. `pnpm typecheck` passes with zero errors 2. `pnpm test` — all existing tests pass 3. main/index.ts has `contextRegistry` instead of individual service globals 4. `reinitializeServiceHandlers` no longer exists in handlers.ts 5. All IPC handlers use `registry.getActive()` pattern 6. SSH connect creates a new ServiceContext and registers it 7. SSH disconnect destroys SSH context and switches to local 8. File watcher events are re-wired on context switch

<success_criteria>

  • Global service variables replaced with ServiceContextRegistry in main/index.ts
  • All IPC domain handlers route through registry, not module-level service refs
  • SSH connect creates isolated context; SSH disconnect destroys it without affecting local
  • File watcher events forward from active context to renderer
  • reinitializeServiceHandlers removed (no longer needed)
  • No regressions in existing tests </success_criteria>
After completion, create `.planning/phases/02-service-infrastructure/02-02-SUMMARY.md`