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>
12 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-service-infrastructure | 01 | execute | 1 |
|
true |
|
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:-
Define
ServiceContextConfiginterface:id: string(e.g., 'local', 'ssh-myserver')type: 'local' | 'ssh'fsProvider: FileSystemProviderprojectsDir?: string(defaults to getProjectsBasePath())todosDir?: string(defaults to getTodosBasePath())
-
Define
ServiceContextclass 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)wheredisableCache = 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, callsetFileSystemProvider(config.fsProvider)after construction.
start()method: callsfileWatcher.start()anddataCache.startAutoCleanup(CACHE_CLEANUP_INTERVAL_MINUTES), stores the cleanup interval handlestopFileWatcher()method: callsfileWatcher.stop()(for pausing on context switch)startFileWatcher()method: callsfileWatcher.start()(for resuming on context switch)dispose()method: callsfileWatcher.dispose(),dataCache.dispose(), clears cleanup interval. Does NOT dispose fsProvider (ownership belongs to SshConnectionManager per research).
- Readonly properties:
Import constants from @shared/constants: MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, CACHE_CLEANUP_INTERVAL_MINUTES.
ServiceContextRegistry.ts - Create the registry coordinator:
- Define
ServiceContextRegistryclass: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 existsgetActive(): ServiceContext— returns context for activeContextId, throws if not foundget(contextId: string): ServiceContext | undefined— returns context by idhas(contextId: string): boolean— check existenceswitch(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-initdestroy(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 contextsgetActiveContextId(): string— getterdispose(): 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';Runpnpm 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.
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:
- Call
this.stop()first (closes fs.watch watchers) - Clear retry timer:
if (this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = null; } - Clear all debounce timers:
for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear(); - Clear catch-up timer:
if (this.catchUpTimer) { clearInterval(this.catchUpTimer); this.catchUpTimer = null; } - Clear polling timer:
if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer = null; } - Clear all tracking maps:
this.lastProcessedLineCount.clear(),this.lastProcessedSize.clear(),this.activeSessionFiles.clear(),this.polledFileSizes.clear(),this.processingInProgress.clear() - Also clear
this.pendingReprocessif it exists (check the class for this field) - 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:
- Call
this.cache.clear() - Set
this.enabled = false - Add a
private disposed = falseflag, set it in dispose - 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. IfstartAutoCleanup()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.
<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>