agent-ecosystem/.planning/phases/02-service-infrastructure/02-01-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

12 KiB

phase plan type wave depends_on files_modified autonomous must_haves
02-service-infrastructure 01 execute 1
src/main/services/infrastructure/ServiceContext.ts
src/main/services/infrastructure/ServiceContextRegistry.ts
src/main/services/infrastructure/index.ts
src/main/services/infrastructure/FileWatcher.ts
src/main/services/infrastructure/DataCache.ts
true
truths artifacts key_links
ServiceContext bundles all session-data services for a single workspace context
ServiceContextRegistry manages local + N SSH contexts with create/switch/destroy lifecycle
Local context is always alive and cannot be destroyed
All EventEmitter-based services have comprehensive dispose() methods that prevent memory leaks
FileWatcher dispose() clears all timers, watchers, tracking maps, and listeners
path provides exports
src/main/services/infrastructure/ServiceContext.ts Service bundle class for a single workspace context
ServiceContext
ServiceContextConfig
path provides exports
src/main/services/infrastructure/ServiceContextRegistry.ts Registry coordinator for all contexts
ServiceContextRegistry
path provides contains
src/main/services/infrastructure/FileWatcher.ts dispose() method on FileWatcher dispose()
path provides contains
src/main/services/infrastructure/DataCache.ts dispose() method on DataCache dispose()
from to via pattern
src/main/services/infrastructure/ServiceContextRegistry.ts src/main/services/infrastructure/ServiceContext.ts creates ServiceContext instances new ServiceContext
from to via pattern
src/main/services/infrastructure/ServiceContext.ts src/main/services/infrastructure/FileWatcher.ts creates and owns FileWatcher instance new FileWatcher
from to via pattern
src/main/services/infrastructure/ServiceContext.ts src/main/services/infrastructure/DataCache.ts creates and owns DataCache instance new DataCache
Create the ServiceContext bundle class and ServiceContextRegistry coordinator, plus add comprehensive dispose() methods to all EventEmitter-based services.

Purpose: These are the foundational types for multi-context support. ServiceContext encapsulates all session-data services for a single workspace (local or SSH). ServiceContextRegistry manages the Map of contexts, tracks the active context, and enforces lifecycle rules (local never destroyed, proper cleanup on destroy).

Output: Two new TypeScript files (ServiceContext.ts, ServiceContextRegistry.ts), updated dispose methods on FileWatcher and DataCache, updated barrel exports.

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

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-provider-plumbing/01-01-SUMMARY.md @src/main/services/infrastructure/FileSystemProvider.ts @src/main/services/infrastructure/FileWatcher.ts @src/main/services/infrastructure/DataCache.ts @src/main/services/infrastructure/LocalFileSystemProvider.ts @src/main/services/discovery/ProjectScanner.ts @src/main/services/parsing/SessionParser.ts @src/main/services/discovery/SubagentResolver.ts @src/main/services/analysis/ChunkBuilder.ts @src/main/services/infrastructure/index.ts @src/main/index.ts Task 1: Create ServiceContext and ServiceContextRegistry src/main/services/infrastructure/ServiceContext.ts src/main/services/infrastructure/ServiceContextRegistry.ts src/main/services/infrastructure/index.ts **ServiceContext.ts** - Create a service bundle class:
  1. Define ServiceContextConfig interface:

    • id: string (e.g., 'local', 'ssh-myserver')
    • type: 'local' | 'ssh'
    • fsProvider: FileSystemProvider
    • projectsDir?: string (defaults to getProjectsBasePath())
    • todosDir?: string (defaults to getTodosBasePath())
  2. Define ServiceContext class with:

    • Readonly properties: id, type
    • Service instances (all readonly): projectScanner: ProjectScanner, sessionParser: SessionParser, subagentResolver: SubagentResolver, chunkBuilder: ChunkBuilder, dataCache: DataCache, fileWatcher: FileWatcher
    • fsProvider: FileSystemProvider (readonly, stored for reference)
    • Constructor that creates all services with correct dependency chain:
      • projectScanner = new ProjectScanner(config.projectsDir, config.todosDir, config.fsProvider)
      • sessionParser = new SessionParser(this.projectScanner)
      • subagentResolver = new SubagentResolver(this.projectScanner)
      • chunkBuilder = new ChunkBuilder()
      • dataCache = new DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, !disableCache) where disableCache = process.env.CLAUDE_CONTEXT_DISABLE_CACHE === '1'
      • fileWatcher = new FileWatcher(this.dataCache) — do NOT call setFileSystemProvider here; FileWatcher uses local fs by default and the setFileSystemProvider pattern is used externally. Actually, looking at the code, FileWatcher stores an fsProvider internally. Check its constructor — if it accepts a provider, pass it. If not, call setFileSystemProvider(config.fsProvider) after construction.
    • start() method: calls fileWatcher.start() and dataCache.startAutoCleanup(CACHE_CLEANUP_INTERVAL_MINUTES), stores the cleanup interval handle
    • stopFileWatcher() method: calls fileWatcher.stop() (for pausing on context switch)
    • startFileWatcher() method: calls fileWatcher.start() (for resuming on context switch)
    • dispose() method: calls fileWatcher.dispose(), dataCache.dispose(), clears cleanup interval. Does NOT dispose fsProvider (ownership belongs to SshConnectionManager per research).

Import constants from @shared/constants: MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, CACHE_CLEANUP_INTERVAL_MINUTES.

ServiceContextRegistry.ts - Create the registry coordinator:

  1. Define ServiceContextRegistry class:
    • private contexts = new Map<string, ServiceContext>()
    • private activeContextId: string = 'local'
    • Constructor takes NO arguments. It does NOT create the local context internally (that happens in index.ts where the mainWindow and notification manager wiring exists).
    • registerContext(context: ServiceContext): void — adds to map, throws if id already exists
    • getActive(): ServiceContext — returns context for activeContextId, throws if not found
    • get(contextId: string): ServiceContext | undefined — returns context by id
    • has(contextId: string): boolean — check existence
    • switch(contextId: string): { previous: ServiceContext; current: ServiceContext } — validates contextId exists, stops file watcher on previous context, sets activeContextId, starts file watcher on new context, returns both contexts for caller to do IPC re-init
    • destroy(contextId: string): void — throws if 'local', calls context.dispose(), removes from map, if activeContextId was this context switch to 'local'
    • list(): Array<{ id: string; type: 'local' | 'ssh' }> — returns metadata for all contexts
    • getActiveContextId(): string — getter
    • dispose(): void — disposes ALL contexts (including local), clears map. Used only on app shutdown.

Use createLogger('Infrastructure:ServiceContextRegistry') for logging.

index.ts - Add exports:

  • Add export * from './ServiceContext';
  • Add export * from './ServiceContextRegistry'; Run pnpm typecheck — must pass with zero errors. Verify the new files exist and export the expected classes. ServiceContext.ts exports ServiceContext class and ServiceContextConfig interface. ServiceContextRegistry.ts exports ServiceContextRegistry class. Both are type-safe and compile without errors. Barrel export updated.
Task 2: Add comprehensive dispose() methods to FileWatcher and DataCache src/main/services/infrastructure/FileWatcher.ts src/main/services/infrastructure/DataCache.ts **FileWatcher.ts** - Add a `dispose()` method (separate from existing `stop()`):

The existing stop() method closes watchers but does NOT clear all internal state or remove EventEmitter listeners. Add a dispose() method that performs complete cleanup:

  1. Call this.stop() first (closes fs.watch watchers)
  2. Clear retry timer: if (this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = null; }
  3. Clear all debounce timers: for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear();
  4. Clear catch-up timer: if (this.catchUpTimer) { clearInterval(this.catchUpTimer); this.catchUpTimer = null; }
  5. Clear polling timer: if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer = null; }
  6. Clear all tracking maps: this.lastProcessedLineCount.clear(), this.lastProcessedSize.clear(), this.activeSessionFiles.clear(), this.polledFileSizes.clear(), this.processingInProgress.clear()
  7. Also clear this.pendingReprocess if it exists (check the class for this field)
  8. LAST: this.removeAllListeners() — MUST be last to prevent firing events during cleanup

Add a private disposed = false flag. Set it in dispose(). Check it in start() to prevent restarting a disposed watcher (throw error or log warning and return early).

DataCache.ts - Add a dispose() method:

  1. Call this.cache.clear()
  2. Set this.enabled = false
  3. Add a private disposed = false flag, set it in dispose
  4. If DataCache has a startAutoCleanup() method that returns an interval handle, the caller (ServiceContext) manages that interval. But also add internal cleanup: check if there's an internal auto-cleanup timer stored as a class field. If startAutoCleanup() only returns the interval for the caller to manage, just clear the cache and disable.

Look at the actual DataCache code to see if it stores the cleanup interval internally. If it only returns it, the caller is responsible. If it stores it, clear it in dispose(). Run pnpm typecheck — must pass. Run pnpm test — all existing tests must still pass. Verify FileWatcher.dispose() exists and calls removeAllListeners(). Verify DataCache.dispose() exists and clears cache. FileWatcher has a dispose() method that: stops watchers, clears ALL timers (retry, debounce, catch-up, polling), clears ALL tracking maps, and calls removeAllListeners() LAST. DataCache has a dispose() method that clears the cache and disables it. Both have a disposed flag to prevent reuse after disposal. All existing tests pass.

1. `pnpm typecheck` passes with zero errors 2. `pnpm test` — all existing tests pass (no regressions) 3. New files exist at expected paths: `src/main/services/infrastructure/ServiceContext.ts`, `src/main/services/infrastructure/ServiceContextRegistry.ts` 4. ServiceContext constructor creates all 6 services correctly 5. ServiceContextRegistry.switch() stops old FileWatcher and starts new one 6. ServiceContextRegistry.destroy('local') throws an error 7. FileWatcher.dispose() calls removeAllListeners()

<success_criteria>

  • ServiceContext class creates isolated service bundles with correct dependency chain
  • ServiceContextRegistry manages Map of contexts with active tracking
  • Local context cannot be destroyed (throws on attempt)
  • FileWatcher.dispose() performs comprehensive cleanup (timers, maps, listeners)
  • DataCache.dispose() clears cache and disables
  • All existing tests pass with no regressions
  • Type checking passes </success_criteria>
After completion, create `.planning/phases/02-service-infrastructure/02-01-SUMMARY.md`