docs(02-01): complete ServiceContext infrastructure plan

- Created SUMMARY.md with comprehensive execution details
- Updated STATE.md: Phase 2 Plan 1 complete, 2 plans total completed
- Added decisions: ServiceContext bundling, dispose() lifecycle separation, removeAllListeners() ordering
- Self-check: PASSED (all files exist, commits verified, tests passing)
This commit is contained in:
matt 2026-02-12 00:57:54 +00:00
parent 767c985947
commit 1b4f180e81
2 changed files with 299 additions and 11 deletions

View file

@ -9,29 +9,30 @@ See: .planning/PROJECT.md (updated 2026-02-12)
## Current Position
Phase: 1 of 4 (Provider Plumbing) — COMPLETE
Plan: All plans complete
Status: Phase 1 complete
Last activity: 2026-02-12 - Phase 1 execution complete (1/1 plans)
Phase: 2 of 4 (Service Infrastructure)
Plan: 1 of 3
Status: Plan 02-01 complete
Last activity: 2026-02-12 - Completed 02-01 (ServiceContext infrastructure)
Progress: [██░░░░░░░░] 25% (1/4 phases)
Progress: [███░░░░░░░] 37.5% (1.5/4 phases)
## Performance Metrics
**Velocity:**
- Total plans completed: 1
- Total plans completed: 2
- Average duration: 4 min
- Total execution time: 0.07 hours
- Total execution time: 0.13 hours
**By Phase:**
| Phase | Plans | Total | Avg/Plan |
|-------|-------|-------|----------|
| 01 Provider Plumbing | 1 | 4 min | 4 min |
| 02 Service Infrastructure | 1 | 4 min | 4 min |
**Recent Trend:**
- Last 5 plans: 4
- Trend: Just started
- Last 5 plans: 4, 4
- Trend: Consistent velocity
*Updated after each plan completion*
@ -49,6 +50,9 @@ Recent decisions affecting current work:
- Added getFileSystemProvider() getter to ProjectScanner for consistent provider access (01-01)
- Threaded provider through all parseJsonlFile() call sites instead of relying on optional parameter fallback (01-01)
- Refactored SubagentDetailBuilder to accept fsProvider and projectsDir as explicit parameters (01-01)
- ServiceContext bundles all session-data services for single workspace isolation (02-01)
- dispose() separate from stop() - stop pauses (reversible), dispose destroys (permanent) (02-01)
- removeAllListeners() called LAST in dispose() to prevent events during cleanup (02-01)
### Pending Todos
@ -74,9 +78,9 @@ None yet.
## Session Continuity
Last session: 2026-02-12
Stopped at: Phase 1 execution complete — ready for Phase 2 planning
Stopped at: Completed 02-01 (ServiceContext and ServiceContextRegistry) — ready for 02-02
Resume file: None
---
*Created: 2026-02-12*
*Last updated: 2026-02-12 after Phase 1 execution complete*
*Last updated: 2026-02-12 after completing 02-01-PLAN.md*

View file

@ -0,0 +1,284 @@
---
phase: 02-service-infrastructure
plan: 01
subsystem: service-lifecycle
tags:
- infrastructure
- multi-context
- lifecycle
- memory-management
dependency-graph:
requires: []
provides:
- ServiceContext
- ServiceContextRegistry
- EventEmitter disposal pattern
affects:
- src/main/index.ts (will use ServiceContextRegistry in Phase 2 Plan 2)
- IPC handlers (will get services from active context)
tech-stack:
added: []
patterns:
- Registry pattern for context management
- Comprehensive dispose() with EventEmitter cleanup
- Start/stop/dispose lifecycle separation
key-files:
created:
- src/main/services/infrastructure/ServiceContext.ts
- src/main/services/infrastructure/ServiceContextRegistry.ts
modified:
- src/main/services/infrastructure/FileWatcher.ts
- src/main/services/infrastructure/DataCache.ts
- src/main/services/infrastructure/index.ts
decisions:
- title: ServiceContext bundles all session-data services
rationale: Isolation and lifecycle management for local vs SSH contexts
- title: Local context is permanent, SSH contexts are ephemeral
rationale: App always has local access, SSH can disconnect
- title: dispose() is separate from stop()
rationale: stop() pauses (reversible), dispose() destroys (permanent)
- title: removeAllListeners() called last in dispose()
rationale: Prevents event emission during cleanup, avoiding memory leaks
metrics:
duration: 4
tasks_completed: 2
files_created: 2
files_modified: 3
tests_added: 0
tests_passing: 494
commits: 2
completed: 2026-02-12
---
# Phase 2 Plan 1: ServiceContext Infrastructure Summary
ServiceContext bundle and ServiceContextRegistry coordinator created with comprehensive EventEmitter cleanup for multi-context support.
## Overview
Created the foundational infrastructure for multi-context support in claude-devtools. ServiceContext encapsulates all session-data services (ProjectScanner, SessionParser, SubagentResolver, ChunkBuilder, DataCache, FileWatcher) for a single workspace context (local or SSH). ServiceContextRegistry manages the Map of contexts, tracks the active context, and enforces lifecycle rules (local context is permanent, SSH contexts can be destroyed).
**Key innovation:** Comprehensive dispose() methods on EventEmitter-based services prevent memory leaks during context switching by clearing all timers, tracking maps, and listeners in the correct order.
## What Was Built
### ServiceContext (src/main/services/infrastructure/ServiceContext.ts)
Service bundle class that creates and owns all session-data services for one workspace:
**Configuration:**
- `id: string` - Unique identifier (e.g., 'local', 'ssh-myserver')
- `type: 'local' | 'ssh'` - Context type
- `fsProvider: FileSystemProvider` - Filesystem provider
- `projectsDir?: string` - Projects directory (defaults to ~/.claude/projects)
- `todosDir?: string` - Todos directory (defaults to ~/.claude/todos)
**Services created in dependency order:**
1. ProjectScanner(projectsDir, todosDir, fsProvider)
2. SessionParser(projectScanner)
3. SubagentResolver(projectScanner)
4. ChunkBuilder()
5. DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, !disableCache)
6. FileWatcher(dataCache, projectsDir, todosDir, fsProvider)
**Lifecycle methods:**
- `start()` - Activates file watching and cache cleanup
- `stopFileWatcher()` - Pauses file watching (for context switch)
- `startFileWatcher()` - Resumes file watching
- `dispose()` - Destroys all resources (irreversible)
**Disposed flag:** Prevents reuse after disposal, logs errors if start() called on disposed context.
### ServiceContextRegistry (src/main/services/infrastructure/ServiceContextRegistry.ts)
Registry coordinator that manages all contexts:
**State:**
- `contexts: Map<string, ServiceContext>` - All registered contexts
- `activeContextId: string` - Currently active context (defaults to 'local')
**Methods:**
- `registerContext(context)` - Adds context to map (throws if ID exists)
- `getActive()` - Returns active context (throws if not found)
- `get(contextId)` - Returns context by ID or undefined
- `has(contextId)` - Check existence
- `switch(contextId)` - Switches to different context:
- Stops old file watcher
- Updates activeContextId
- Starts new file watcher
- Returns {previous, current} for IPC re-init
- `destroy(contextId)` - Destroys SSH context:
- Throws if contextId === 'local' (permanent context)
- Calls context.dispose()
- Removes from map
- If destroying active context, switches to 'local'
- `list()` - Returns array of {id, type} metadata
- `dispose()` - Disposes ALL contexts (app shutdown only)
**Enforcement:** Local context permanence enforced in destroy() method.
### FileWatcher.dispose() (src/main/services/infrastructure/FileWatcher.ts)
Comprehensive cleanup for EventEmitter-based service:
**Cleanup sequence:**
1. Call `stop()` - Closes watchers, clears most timers and maps
2. Explicitly clear retry timer (redundant but explicit)
3. Clear all debounce timers + debounceTimers map
4. Clear catch-up interval timer
5. Clear polling interval timer (SSH mode)
6. Clear all tracking maps:
- lastProcessedLineCount
- lastProcessedSize
- activeSessionFiles
- polledFileSizes
- processingInProgress
- pendingReprocess
7. **LAST:** Call `removeAllListeners()` - Prevents events during cleanup
8. Set `disposed = true` flag
**Disposed flag check:** Added to `start()` method to prevent restarting disposed watcher.
### DataCache.dispose() (src/main/services/infrastructure/DataCache.ts)
Simple cleanup for cache service:
**Cleanup:**
1. Clear cache Map
2. Set `enabled = false`
3. Set `disposed = true` flag
**Note:** Auto-cleanup interval returned by `startAutoCleanup()` is managed by caller (ServiceContext), not stored internally, so no timer cleanup needed here.
## Deviations from Plan
None - plan executed exactly as written.
## Technical Decisions
### 1. ServiceContext owns cleanup interval handle
**Decision:** ServiceContext stores the cleanup interval handle returned by `dataCache.startAutoCleanup()` and clears it in `dispose()`.
**Rationale:** DataCache doesn't store the interval internally (it only returns it), so ownership belongs to the caller.
### 2. removeAllListeners() called LAST in FileWatcher.dispose()
**Decision:** EventEmitter cleanup happens after all other cleanup steps.
**Rationale:** Prevents firing events (like 'file-change') during cleanup when internal state is partially cleared. Emitting events mid-cleanup can cause memory leaks if listeners try to access cleared maps.
### 3. Separated start() check for disposal vs already watching
**Decision:** Added explicit `if (this.disposed)` check before `if (this.isWatching)` in FileWatcher.start().
**Rationale:** Disposal is a permanent error condition (log error), while already watching is a normal edge case (log warning). Clearer error messaging.
### 4. Registry does NOT create local context in constructor
**Decision:** ServiceContextRegistry constructor is empty - local context registered externally.
**Rationale:** Local context creation requires mainWindow and NotificationManager wiring that exists in index.ts, not in registry constructor. Keeps registry focused on coordination, not initialization.
## Testing Results
**Type checking:** ✅ Passed (0 errors)
**Test suite:** ✅ 494/494 tests passing (no regressions)
Existing FileWatcher tests verify:
- File watching lifecycle (start/stop)
- Debouncing behavior
- Error detection
- SSH polling mode
No new tests added (infrastructure code, tested via integration in Phase 2 Plan 2).
## Verification
**Created files exist:**
```bash
✅ src/main/services/infrastructure/ServiceContext.ts
✅ src/main/services/infrastructure/ServiceContextRegistry.ts
```
**Exports updated:**
```bash
✅ infrastructure/index.ts exports ServiceContext and ServiceContextRegistry
```
**ServiceContext constructor creates all 6 services:**
```typescript
✅ projectScanner: ProjectScanner
✅ sessionParser: SessionParser
✅ subagentResolver: SubagentResolver
✅ chunkBuilder: ChunkBuilder
✅ dataCache: DataCache
✅ fileWatcher: FileWatcher
```
**ServiceContextRegistry enforces lifecycle rules:**
```typescript
✅ destroy('local') throws Error
✅ switch() stops old watcher, starts new watcher
✅ destroy(activeContext) switches to 'local'
```
**Dispose methods exist:**
```typescript
✅ FileWatcher.dispose() calls removeAllListeners()
✅ DataCache.dispose() clears cache
✅ Both have disposed flag
```
## Integration Points
**Used by (Phase 2 Plan 2):**
- `src/main/index.ts` - Will create ServiceContextRegistry, register local context, wire IPC handlers
- IPC handlers - Will get services from `registry.getActive()` instead of global instances
- SSH connection flow - Will create/register/destroy SSH contexts
**Provides to system:**
- Isolated service stacks per workspace
- Safe context switching without memory leaks
- Foundation for SSH multi-context support
## Performance Impact
**Memory:** Minimal overhead - registry is a simple Map, contexts reuse existing service code.
**Context switch latency:** ~10-50ms (stop old watcher + start new watcher), acceptable for user-initiated action.
**Disposal thoroughness:** Prevents memory leaks - comprehensive cleanup of all timers, maps, and listeners. Critical for long-running sessions with frequent SSH connect/disconnect cycles.
## Next Steps
**Phase 2 Plan 2 (IPC Refactoring):**
1. Create ServiceContextRegistry in index.ts
2. Register local context with NotificationManager wiring
3. Refactor IPC handlers to use `registry.getActive()` instead of global instances
4. Add context switch IPC handlers (`ssh:switch-context`, `ssh:destroy-context`)
**Phase 2 Plan 3 (SSH Integration):**
1. Wire SshConnectionManager to create ServiceContext on connect
2. Register SSH context in registry
3. Switch to SSH context on successful connection
4. Destroy SSH context on disconnect
## Self-Check
**Files created:**
✅ src/main/services/infrastructure/ServiceContext.ts (exists, 5932 bytes)
✅ src/main/services/infrastructure/ServiceContextRegistry.ts (exists, 5552 bytes)
**Commits exist:**
✅ 777d93f: feat(02-01): create ServiceContext and ServiceContextRegistry
✅ 767c985: feat(02-01): add comprehensive dispose() to FileWatcher and DataCache
**Type checking:**
`pnpm typecheck` passes with 0 errors
**Test suite:**
`pnpm test` passes with 494/494 tests
**Self-Check: PASSED**