|
| 1 | +--- |
| 2 | +phase: design |
| 3 | +title: System Design & Architecture |
| 4 | +description: Define the technical architecture, components, and data models |
| 5 | +--- |
| 6 | + |
| 7 | +# System Design & Architecture |
| 8 | + |
| 9 | +## Architecture Overview |
| 10 | + |
| 11 | +The change is localised to `ClaudeCodeAdapter`. The detection flow always attempts a PID-file lookup for every process first; only processes whose PID file cannot be found fall through to the existing legacy matching step. |
| 12 | + |
| 13 | +```mermaid |
| 14 | +flowchart TD |
| 15 | + A[detectAgents] --> B[listAgentProcesses - ps aux] |
| 16 | + B --> C[enrichProcesses - lsof + ps] |
| 17 | + C --> D[For each PID: try read ~/.claude/sessions/PID.json] |
| 18 | + D --> E{PID file found?} |
| 19 | + E -->|No| G[Add to legacy-fallback set] |
| 20 | + E -->|Yes| F{startedAt within 60s\nof proc.startTime?} |
| 21 | + F -->|No - stale| G |
| 22 | + F -->|Yes| H[Resolve JSONL path from sessionId + cwd] |
| 23 | + H --> I{JSONL exists?} |
| 24 | + I -->|No| G |
| 25 | + I -->|Yes| J[Direct match: process → session] |
| 26 | + G --> K[discoverSessions for fallback processes] |
| 27 | + K --> L[matchProcessesToSessions - existing algo] |
| 28 | + J --> M[Merge direct matches + legacy matches] |
| 29 | + L --> M |
| 30 | + M --> N[Read sessions and build AgentInfo] |
| 31 | +``` |
| 32 | + |
| 33 | +## Data Models |
| 34 | + |
| 35 | +### PID file schema (`~/.claude/sessions/<pid>.json`) |
| 36 | +```typescript |
| 37 | +interface PidFileEntry { |
| 38 | + pid: number; |
| 39 | + sessionId: string; // filename without .jsonl |
| 40 | + cwd: string; // working directory when Claude started |
| 41 | + startedAt: number; // epoch milliseconds |
| 42 | + kind: string; // e.g. "interactive" — not used |
| 43 | + entrypoint: string; // e.g. "cli" — not used |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +### New internal type: `DirectMatch` |
| 48 | +```typescript |
| 49 | +interface DirectMatch { |
| 50 | + process: ProcessInfo; |
| 51 | + sessionFile: SessionFile; // reuse existing SessionFile shape |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +## Component Breakdown |
| 56 | + |
| 57 | +### Modified: `ClaudeCodeAdapter` |
| 58 | + |
| 59 | +**New private method**: `tryPidFileMatching(processes: ProcessInfo[]): { direct: DirectMatch[]; fallback: ProcessInfo[] }` |
| 60 | +- For each process, attempts to read `~/.claude/sessions/<pid>.json`. |
| 61 | + - If the file is absent or unreadable: process goes to `fallback`. |
| 62 | + - If the file is present: |
| 63 | + - Cross-checks `entry.startedAt` (epoch ms) against `proc.startTime.getTime()`; if delta > 60 s, file is stale → process goes to `fallback`. |
| 64 | + - Resolves the JSONL path: `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl` using the `cwd` from the PID file. |
| 65 | + - Verifies the JSONL exists; if missing: process goes to `fallback`. |
| 66 | + - If JSONL exists: process goes to `direct`. |
| 67 | +- There is **no upfront directory-existence check** — each PID is always tried individually. Missing files are handled per-process via try/catch. |
| 68 | + |
| 69 | +**Modified**: `detectAgents()` |
| 70 | +- Calls `tryPidFileMatching()` after enrichment. |
| 71 | +- Passes only `fallback` processes to the existing `discoverSessions()` + `matchProcessesToSessions()` pipeline. |
| 72 | +- Merges `direct` matches with legacy match results before building `AgentInfo` objects. |
| 73 | + |
| 74 | +### Unchanged |
| 75 | +- `utils/process.ts` — process listing and enrichment unchanged. |
| 76 | +- `utils/session.ts` — session file discovery unchanged. |
| 77 | +- `utils/matching.ts` — matching algorithm unchanged. |
| 78 | +- All other adapters — untouched. |
| 79 | + |
| 80 | +## Design Decisions |
| 81 | + |
| 82 | +| Decision | Choice | Rationale | |
| 83 | +|----------|--------|-----------| |
| 84 | +| Where to do PID file lookup | Inside `ClaudeCodeAdapter` as a private method | Keeps the change isolated; other adapters don't need it | |
| 85 | +| CWD source for JSONL path encoding | PID file's `cwd` field | PID file is authoritative; lsof cwd may differ (symlinks, etc.) | |
| 86 | +| `startedAt` type | Epoch milliseconds (`number`) | Verified from real files — not an ISO string | |
| 87 | +| Stale file guard | Cross-check `entry.startedAt` vs `proc.startTime` (60 s tolerance) | Catches PID reuse without false positives from normal startup delays | |
| 88 | +| `enrichProcesses()` scope | Run on all processes before the split | `proc.startTime` is needed for the stale-file guard; batched call is cheap | |
| 89 | +| Error handling for malformed PID files | Catch + fall back to legacy | Avoids crashing; older or corrupt files handled gracefully | |
| 90 | +| Batching PID file reads | No batching (sequential per PID) | Files are tiny JSON; overhead is negligible | |
| 91 | +| Reuse `SessionFile` shape for direct matches | Yes | Avoids new types; existing `readSession` and `buildAgentInfo` code works unchanged | |
| 92 | + |
| 93 | +## Non-Functional Requirements |
| 94 | + |
| 95 | +- **No performance regression**: PID file reads add at most one `fs.readFileSync` + `fs.existsSync` per process, which is negligible. |
| 96 | +- **Backward compatibility**: All existing behaviour is preserved when no PID files exist (older Claude Code installs). Each missing file falls through to the legacy algorithm per-process. |
| 97 | +- **No new external dependencies**. |
0 commit comments