ai-agent-session-center 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +484 -429
  2. package/docs/3D/ADAPTATION_GUIDE.md +592 -0
  3. package/docs/3D/index.html +754 -0
  4. package/docs/AGENT_TEAM_TASKS.md +716 -0
  5. package/docs/CYBERDROME_V2_SPEC.md +531 -0
  6. package/docs/ERROR_185_ANALYSIS.md +263 -0
  7. package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
  8. package/docs/SESSION_DETAIL_FEATURES.md +98 -0
  9. package/docs/_3d_multimedia_features.md +1080 -0
  10. package/docs/_frontend_features.md +1057 -0
  11. package/docs/_server_features.md +1077 -0
  12. package/docs/session-duplication-fixes.md +271 -0
  13. package/docs/session-terminal-linkage.md +412 -0
  14. package/package.json +63 -5
  15. package/public/apple-touch-icon.svg +21 -0
  16. package/public/css/dashboard.css +0 -161
  17. package/public/css/detail-panel.css +25 -0
  18. package/public/css/layout.css +18 -1
  19. package/public/css/modals.css +0 -26
  20. package/public/css/settings.css +0 -150
  21. package/public/css/terminal.css +34 -0
  22. package/public/favicon.svg +18 -0
  23. package/public/index.html +6 -26
  24. package/public/js/alarmManager.js +0 -21
  25. package/public/js/app.js +21 -7
  26. package/public/js/detailPanel.js +63 -64
  27. package/public/js/historyPanel.js +61 -55
  28. package/public/js/quickActions.js +132 -48
  29. package/public/js/sessionCard.js +5 -20
  30. package/public/js/sessionControls.js +8 -0
  31. package/public/js/settingsManager.js +0 -142
  32. package/server/apiRouter.js +60 -15
  33. package/server/apiRouter.ts +774 -0
  34. package/server/approvalDetector.ts +94 -0
  35. package/server/authManager.ts +144 -0
  36. package/server/autoIdleManager.ts +110 -0
  37. package/server/config.ts +121 -0
  38. package/server/constants.ts +150 -0
  39. package/server/db.ts +475 -0
  40. package/server/hookInstaller.d.ts +3 -0
  41. package/server/hookProcessor.ts +108 -0
  42. package/server/hookRouter.ts +18 -0
  43. package/server/hookStats.ts +116 -0
  44. package/server/index.js +15 -1
  45. package/server/index.ts +230 -0
  46. package/server/logger.ts +75 -0
  47. package/server/mqReader.ts +349 -0
  48. package/server/portManager.ts +55 -0
  49. package/server/processMonitor.ts +239 -0
  50. package/server/serverConfig.ts +29 -0
  51. package/server/sessionMatcher.js +17 -6
  52. package/server/sessionMatcher.ts +403 -0
  53. package/server/sessionStore.js +109 -3
  54. package/server/sessionStore.ts +1145 -0
  55. package/server/sshManager.js +167 -24
  56. package/server/sshManager.ts +671 -0
  57. package/server/teamManager.ts +289 -0
  58. package/server/wsManager.ts +200 -0
@@ -0,0 +1,1077 @@
1
+ # AI Agent Session Center — Server Features Reference
2
+
3
+ ---
4
+
5
+ ## 1. Platform Overview
6
+
7
+ The AI Agent Session Center is a localhost dashboard (default port **3333**) that monitors active AI coding agent sessions in real time. Hook scripts installed into the AI CLI capture lifecycle events, relay them via a file-based message queue, and the server pushes updates to all connected browsers over WebSocket.
8
+
9
+ ### Tech Stack
10
+
11
+ | Layer | Technology | Version / Notes |
12
+ |---|---|---|
13
+ | Runtime | Node.js | 18+ (ESM modules) |
14
+ | HTTP framework | Express | 5 |
15
+ | WebSocket | ws | 8 |
16
+ | PTY / terminal | node-pty | native bindings |
17
+ | Database | better-sqlite3 | WAL mode |
18
+ | Hook delivery | Bash → JSONL queue file | POSIX atomic append |
19
+ | Frontend (legacy) | Vanilla JS + CSS | Import maps, no build step |
20
+ | Frontend (new) | React 19 + TypeScript + Vite | Served from dist/client |
21
+ | Port | 3333 | Configurable |
22
+
23
+ ### Top-Level Architecture
24
+
25
+ ```
26
+ AI CLI (Claude / Gemini / Codex)
27
+ │ Hook script fires on each event
28
+
29
+ dashboard-hook.sh
30
+ ├── Reads stdin JSON
31
+ ├── Enriches with: PID, TTY, TERM_PROGRAM, tab IDs, team env vars
32
+ ├── Single jq pass (~2-5ms)
33
+ └── Appends to /tmp/claude-session-center/queue.jsonl (~0.1ms)
34
+ │ (HTTP POST fallback if MQ dir absent)
35
+
36
+ mqReader.js
37
+ ├── fs.watch() + 10ms debounce (instant notification)
38
+ ├── 500ms fallback poll
39
+ ├── 5s health check (detects silent fs.watch failures)
40
+ └── Reads from last byte offset (no re-reading)
41
+
42
+
43
+ hookProcessor.js
44
+ ├── Validates payload (session_id, event type, PID)
45
+ ├── Calls sessionStore.handleEvent()
46
+ ├── Records stats (latency, processing time)
47
+ └── Broadcasts to WebSocket clients
48
+
49
+
50
+ sessionStore.js (coordinator)
51
+ ├── sessionMatcher → link hook to session (5-priority system)
52
+ ├── approvalDetector → tool approval timeout timers
53
+ ├── teamManager → subagent team tracking
54
+ ├── processMonitor → PID liveness checks
55
+ └── autoIdleManager → idle transition timers
56
+
57
+
58
+ wsManager.js
59
+ └── Broadcasts session_update to all connected browsers
60
+
61
+
62
+ Browser (React / Vanilla JS)
63
+ └── IndexedDB + UI rendering
64
+ ```
65
+
66
+ ### Latency Budget
67
+
68
+ | Stage | Typical | Notes |
69
+ |---|---|---|
70
+ | jq enrichment | 2-5 ms | Single jq invocation |
71
+ | File append | ~0.1 ms | POSIX atomic for writes < 4096 bytes |
72
+ | fs.watch + debounce | 0-10 ms | Instant on macOS/Linux |
73
+ | Server processing | ~0.5 ms | handleEvent + broadcast |
74
+ | **Total end-to-end** | **3-17 ms** | Hook fired → browser updated |
75
+
76
+ ---
77
+
78
+ ## 2. Hook Delivery Pipeline
79
+
80
+ ### 2.1 Bash Hook Script (`hooks/dashboard-hook.sh`)
81
+
82
+ The hook script is copied to `~/.claude/hooks/dashboard-hook.sh` and registered in `~/.claude/settings.json`. It runs synchronously to read stdin, then forks a background subshell immediately (`} &>/dev/null &`) so the Claude process is never blocked.
83
+
84
+ **Execution flow:**
85
+
86
+ 1. Capture `SENT_AT=$(date +%s)` and `INPUT=$(cat)` synchronously
87
+ 2. Fork background subshell (`{ ... } &>/dev/null &; disown`)
88
+ 3. In background: TTY detection (cached per PID in `/tmp/claude-tty-cache/$PPID`)
89
+ 4. Single `jq -c` pass to enrich and extract fields
90
+ 5. Tab title update (only on state-changing events: `SessionStart`, `UserPromptSubmit`, `PermissionRequest`, `Stop`, `Notification`, `SessionEnd`)
91
+ 6. Deliver: append to `/tmp/claude-session-center/queue.jsonl` if MQ dir exists; otherwise HTTP POST to `http://localhost:3333/api/hooks`
92
+
93
+ **Fields enriched by jq:**
94
+
95
+ | Field | Source | Description |
96
+ |---|---|---|
97
+ | `claude_pid` | `$PPID` | Claude process PID |
98
+ | `hook_sent_at` | `date +%s * 1000` | Timestamp in ms for latency tracking |
99
+ | `tty_path` | `ps -o tty= -p $PPID` | Full TTY path (e.g. `/dev/ttys003`) |
100
+ | `term_program` | `$TERM_PROGRAM` | Terminal app name |
101
+ | `term_program_version` | `$TERM_PROGRAM_VERSION` | Terminal version |
102
+ | `vscode_pid` | `$VSCODE_PID` | VS Code extension host PID |
103
+ | `term` | `$TERM` | TERM env variable |
104
+ | `tab_id` | `$ITERM_SESSION_ID`, `$KITTY_WINDOW_ID`, `$WARP_SESSION_ID`, `$WEZTERM_PANE`, `$TERM_SESSION_ID` | Tab/session identifier |
105
+ | `window_id` | `$WINDOWID` | X11 window ID |
106
+ | `tmux` | `$TMUX`, `$TMUX_PANE` | `{session, pane}` or null |
107
+ | `is_ghostty` | `$GHOSTTY_RESOURCES_DIR` | Boolean flag |
108
+ | `kitty_pid` | `$KITTY_PID` | Kitty process PID |
109
+ | `agent_terminal_id` | `$AGENT_MANAGER_TERMINAL_ID` | PTY terminal ID injected by sshManager |
110
+ | `claude_project_dir` | `$CLAUDE_PROJECT_DIR` | Claude's project directory |
111
+ | `parent_session_id` | `$CLAUDE_CODE_PARENT_SESSION_ID` | Parent session for subagent linking |
112
+ | `team_name` | `$CLAUDE_CODE_TEAM_NAME` | Team name for multi-agent |
113
+ | `agent_name` | `$CLAUDE_CODE_AGENT_NAME` | Agent name (e.g. `backend-engineer`) |
114
+ | `agent_type` | `$CLAUDE_CODE_AGENT_TYPE` | Agent type (e.g. `task`) |
115
+ | `agent_id` | `$CLAUDE_CODE_AGENT_ID` | Agent UUID |
116
+ | `agent_color` | `$CLAUDE_CODE_AGENT_COLOR` | Agent accent color |
117
+
118
+ **TTY caching:** Cached per PPID in `/tmp/claude-tty-cache/$PPID` to avoid running `ps` on every hook event.
119
+
120
+ ### 2.2 MQ Reader (`server/mqReader.js`)
121
+
122
+ ```
123
+ Queue file: /tmp/claude-session-center/queue.jsonl
124
+ (Windows: %TEMP%\claude-session-center\queue.jsonl)
125
+ ```
126
+
127
+ | Parameter | Value |
128
+ |---|---|
129
+ | Poll interval (fallback) | 500 ms |
130
+ | fs.watch debounce | 10 ms |
131
+ | Health check interval | 5000 ms |
132
+ | Truncation threshold | 1 MB |
133
+
134
+ **Read algorithm:**
135
+ 1. `fs.watch()` fires → `scheduleRead()` (debounced 10ms)
136
+ 2. Open file, get `fstat` size
137
+ 3. If `fileSize < lastByteOffset`: external truncation detected, reset offset to 0
138
+ 4. `readSync()` from `lastByteOffset` to `fileSize`
139
+ 5. Split on `\n`, retain partial trailing line in `partialLine` buffer
140
+ 6. Parse each complete JSON line → `processHookEvent()`
141
+ 7. Advance `lastByteOffset` by bytes consumed (minus partial)
142
+ 8. If `lastByteOffset > 1MB` and no partial: truncate file (write remaining partial back, reset offset)
143
+
144
+ **Snapshot resume:** On startup the reader accepts `resumeOffset` from a saved snapshot to continue from where it left off without reprocessing events.
145
+
146
+ **Stats tracked:** `linesProcessed`, `linesErrored`, `truncations`, `lastProcessedAt`, `startedAt`, `currentOffset`, `hasPartialLine`.
147
+
148
+ ### 2.3 Hook Validation (`server/hookProcessor.js`)
149
+
150
+ Every hook (from HTTP or MQ) passes through `validateHookPayload()`:
151
+
152
+ | Field | Requirement |
153
+ |---|---|
154
+ | `session_id` | Required, string, max 256 chars |
155
+ | `hook_event_name` | Required, must be in `KNOWN_EVENTS` set |
156
+ | `claude_pid` | Optional, must be positive integer if present |
157
+ | `timestamp` | Optional, must be valid number if present |
158
+
159
+ Unknown event types are rejected with `"unknown event type: ..."`. Invalid payloads are logged and not processed.
160
+
161
+ ### 2.4 HTTP Fallback (`server/hookRouter.js`)
162
+
163
+ When the MQ directory does not exist (server not yet started), the hook script falls back to:
164
+ ```
165
+ POST http://localhost:3333/api/hooks
166
+ Content-Type: application/json
167
+ --connect-timeout 1 -m 3
168
+ ```
169
+
170
+ Rate limit: **100 requests/second per IP** (enforced by `hookRateLimitMiddleware`).
171
+
172
+ ---
173
+
174
+ ## 3. Hook Events and Density Levels
175
+
176
+ ### 3.1 Claude Code Events (14 total)
177
+
178
+ | Event | Constant | When It Fires |
179
+ |---|---|---|
180
+ | `SessionStart` | `EVENT_TYPES.SESSION_START` | Claude process starts |
181
+ | `UserPromptSubmit` | `EVENT_TYPES.USER_PROMPT_SUBMIT` | User submits a prompt |
182
+ | `PreToolUse` | `EVENT_TYPES.PRE_TOOL_USE` | Before a tool call executes |
183
+ | `PostToolUse` | `EVENT_TYPES.POST_TOOL_USE` | After a tool call succeeds |
184
+ | `PostToolUseFailure` | `EVENT_TYPES.POST_TOOL_USE_FAILURE` | After a tool call fails |
185
+ | `PermissionRequest` | `EVENT_TYPES.PERMISSION_REQUEST` | Claude needs user approval |
186
+ | `Stop` | `EVENT_TYPES.STOP` | Claude finishes its turn |
187
+ | `Notification` | `EVENT_TYPES.NOTIFICATION` | System notification |
188
+ | `SubagentStart` | `EVENT_TYPES.SUBAGENT_START` | Subagent spawned |
189
+ | `SubagentStop` | `EVENT_TYPES.SUBAGENT_STOP` | Subagent finished |
190
+ | `TeammateIdle` | `EVENT_TYPES.TEAMMATE_IDLE` | A teammate is idle (high density only) |
191
+ | `TaskCompleted` | `EVENT_TYPES.TASK_COMPLETED` | A task was completed |
192
+ | `PreCompact` | `EVENT_TYPES.PRE_COMPACT` | Context compaction about to start (high only) |
193
+ | `SessionEnd` | `EVENT_TYPES.SESSION_END` | Claude process exits |
194
+
195
+ ### 3.2 Gemini CLI Events (7 total)
196
+
197
+ | Event | When It Fires |
198
+ |---|---|
199
+ | `BeforeAgent` | Before agent turn |
200
+ | `BeforeTool` | Before tool call |
201
+ | `AfterTool` | After tool call |
202
+ | `AfterAgent` | After agent turn |
203
+ | (plus `SessionStart`, `SessionEnd`, `Notification` from Claude mapping) |
204
+
205
+ ### 3.3 Codex Events (1 event)
206
+
207
+ | Event | When It Fires |
208
+ |---|---|
209
+ | `agent-turn-complete` | After Codex completes a turn |
210
+
211
+ ### 3.4 Density Levels
212
+
213
+ | Level | Claude Events | Gemini Events | Notes |
214
+ |---|---|---|---|
215
+ | `high` | All 14 | 7 (`SessionStart`, `BeforeAgent`, `BeforeTool`, `AfterTool`, `AfterAgent`, `SessionEnd`, `Notification`) | Full monitoring |
216
+ | `medium` | 12 (excludes `TeammateIdle`, `PreCompact`) | 5 (`SessionStart`, `BeforeAgent`, `AfterAgent`, `SessionEnd`, `Notification`) | Default — good balance |
217
+ | `low` | 5 (`SessionStart`, `UserPromptSubmit`, `PermissionRequest`, `Stop`, `SessionEnd`) | 3 (`SessionStart`, `AfterAgent`, `SessionEnd`) | Minimal overhead |
218
+
219
+ ### 3.5 Hook Registration
220
+
221
+ Hooks are registered in `~/.claude/settings.json` under the `hooks` key. Each event gets a group entry:
222
+ ```json
223
+ {
224
+ "_source": "ai-agent-session-center",
225
+ "hooks": [{ "type": "command", "command": "~/.claude/hooks/dashboard-hook.sh", "async": true }]
226
+ }
227
+ ```
228
+
229
+ Registration uses **atomic writes** (write to `.tmp.XXXX`, then `rename()`) to prevent corrupting `settings.json`. The installer checks for existing hooks before adding to avoid duplicates. Hooks are re-synced on every server startup if the script content has changed.
230
+
231
+ ---
232
+
233
+ ## 4. Session State Machine
234
+
235
+ ### 4.1 States
236
+
237
+ | Status | Constant | Animation | Description |
238
+ |---|---|---|---|
239
+ | `idle` | `SESSION_STATUS.IDLE` | `Idle` | No activity |
240
+ | `prompting` | `SESSION_STATUS.PROMPTING` | `Walking` + `Wave` emote | User prompt submitted |
241
+ | `working` | `SESSION_STATUS.WORKING` | `Running` | Tool in progress |
242
+ | `approval` | `SESSION_STATUS.APPROVAL` | `Waiting` | Needs user approval for a tool |
243
+ | `input` | `SESSION_STATUS.INPUT` | `Waiting` | Waiting for user answer (AskUserQuestion etc.) |
244
+ | `waiting` | `SESSION_STATUS.WAITING` | `Waiting` + `ThumbsUp` emote | Turn finished, ready for next prompt |
245
+ | `ended` | `SESSION_STATUS.ENDED` | `Death` | Session ended |
246
+ | `connecting` | `SESSION_STATUS.CONNECTING` | `Walking` + `Wave` emote | SSH terminal connecting |
247
+
248
+ **Heavy work variant:** If `totalToolCalls > 10` at `Stop` time, the animation switches to `Dance` instead of `Waiting+ThumbsUp`.
249
+
250
+ ### 4.2 State Transitions
251
+
252
+ ```
253
+ SessionStart → idle
254
+ UserPromptSubmit → prompting (Walking + Wave)
255
+ PreToolUse → working (Running)
256
+ PostToolUse → working (stays Running)
257
+ [timer expires] → approval (Waiting) — tool approval heuristic
258
+ PermissionRequest → approval (Waiting) — direct signal
259
+ [timer expires for userInput tools] → input (Waiting)
260
+ Stop → waiting (Waiting + ThumbsUp / Dance)
261
+ [2 min idle] → idle (auto-idle)
262
+ SessionEnd → ended (Death)
263
+ [10s after ended] → deleted from memory (non-SSH)
264
+ ```
265
+
266
+ ### 4.3 Auto-Idle Timeouts
267
+
268
+ | Status | Timeout | Transitions To |
269
+ |---|---|---|
270
+ | `prompting` | 30,000 ms (30 s) | `waiting` |
271
+ | `waiting` | 120,000 ms (2 min) | `idle` |
272
+ | `working` | 180,000 ms (3 min) | `idle` |
273
+ | `approval` | 600,000 ms (10 min) | `idle` (safety net) |
274
+ | `input` | 600,000 ms (10 min) | `idle` (safety net) |
275
+
276
+ Auto-idle is checked every **10 seconds** by `autoIdleManager.js`.
277
+
278
+ ### 4.4 Session Object Fields
279
+
280
+ Every session in memory (`Map<string, Session>`) contains:
281
+
282
+ | Field | Type | Description |
283
+ |---|---|---|
284
+ | `sessionId` | string | Claude-assigned UUID or terminal ID |
285
+ | `projectPath` | string | Working directory (full path) |
286
+ | `projectName` | string | Last segment of projectPath |
287
+ | `title` | string | Auto-generated or user-set title |
288
+ | `status` | string | Current status (see table above) |
289
+ | `animationState` | string | `Idle`, `Walking`, `Running`, `Waiting`, `Death`, `Dance` |
290
+ | `emote` | string\|null | `Wave`, `ThumbsUp`, `Jump`, `Yes` or null |
291
+ | `startedAt` | number | Unix ms timestamp |
292
+ | `lastActivityAt` | number | Unix ms timestamp of last event |
293
+ | `endedAt` | number\|null | Unix ms timestamp or null |
294
+ | `currentPrompt` | string | Current/last prompt text |
295
+ | `promptHistory` | array | Last 50 prompts: `{text, timestamp}` |
296
+ | `toolUsage` | object | `{toolName: count}` map |
297
+ | `totalToolCalls` | number | Total tool calls this turn (reset at `Stop`) |
298
+ | `toolLog` | array | Last 200 tool entries: `{tool, input, timestamp, failed?, error?}` |
299
+ | `responseLog` | array | Last 50 response excerpts: `{text, timestamp}` (first 2000 chars each) |
300
+ | `events` | array | Last 50 lifecycle events: `{type, detail, timestamp}` |
301
+ | `model` | string | AI model name (e.g. `claude-opus-4-6`) |
302
+ | `subagentCount` | number | Currently active subagents |
303
+ | `archived` | number | 0 or 1 |
304
+ | `source` | string | `ssh`, `vscode`, `iterm`, `warp`, `terminal`, etc. |
305
+ | `pendingTool` | string\|null | Tool awaiting approval |
306
+ | `pendingToolDetail` | string\|null | Summary of pending tool input |
307
+ | `waitingDetail` | string\|null | Human-readable approval message |
308
+ | `cachedPid` | number\|null | Claude process PID |
309
+ | `queueCount` | number | Pending prompt queue count |
310
+ | `terminalId` | string\|null | Active PTY terminal ID |
311
+ | `lastTerminalId` | string\|null | Previous terminal ID (for resume) |
312
+ | `sshHost` | string | SSH host |
313
+ | `sshCommand` | string | Command run in terminal |
314
+ | `sshConfig` | object | `{host, port, username, authMethod, privateKeyPath, workingDir, command}` |
315
+ | `transcriptPath` | string | Claude transcript file path |
316
+ | `permissionMode` | string\|null | Claude permission mode |
317
+ | `teamId` | string\|null | Team this session belongs to |
318
+ | `teamRole` | string\|null | `leader` or `member` |
319
+ | `agentName` | string\|null | Agent name (multi-agent) |
320
+ | `agentType` | string\|null | Agent type |
321
+ | `agentColor` | string\|null | Agent accent color |
322
+ | `tmuxPaneId` | string\|null | Tmux pane ID (e.g. `%5`) |
323
+ | `isHistorical` | boolean | SSH session archived after end |
324
+ | `previousSessions` | array | Up to 5 previous session snapshots (for resume history) |
325
+ | `replacesId` | string | One-time field set during session re-key |
326
+ | `label` | string | User-assigned label |
327
+ | `summary` | string | AI-generated summary |
328
+ | `accentColor` | string | Custom accent color |
329
+ | `characterModel` | string | 3D character model override |
330
+
331
+ ### 4.5 Event Ring Buffer
332
+
333
+ The server maintains a ring buffer of the last **500 events** for WebSocket reconnect replay:
334
+
335
+ ```js
336
+ const EVENT_BUFFER_MAX = 500;
337
+ // Each entry: { seq: number, type: string, data: any, timestamp: number }
338
+ ```
339
+
340
+ On reconnect, the client sends `{ type: "replay", sinceSeq: N }` and the server replays all events with `seq > N`.
341
+
342
+ ### 4.6 Snapshot Persistence
343
+
344
+ Sessions are saved to `/tmp/claude-session-center/sessions-snapshot.json` every **10 seconds** using atomic write (tmp file + rename). The snapshot includes:
345
+
346
+ - All session objects
347
+ - `projectSessionCounters` Map
348
+ - `pidToSession` Map
349
+ - `pendingResume` Map
350
+ - `eventSeq` (ring buffer sequence number)
351
+ - `mqOffset` (byte offset in queue file)
352
+
353
+ On startup, the snapshot is loaded and PID liveness is checked. Dead PIDs result in sessions marked `ended`. SSH sessions with orphaned processes (alive but unreachable after server restart) are sent `SIGTERM`. Non-SSH ended sessions are kept for 30 minutes to allow auto-linking on `claude --resume`.
354
+
355
+ ### 4.7 Broadcast Debounce
356
+
357
+ Session updates are debounced within a **50ms window** to batch rapid state changes. Within a batch, only the latest `session_update` per `sessionId` is sent (deduplication).
358
+
359
+ ---
360
+
361
+ ## 5. Session Matching (5-Priority System)
362
+
363
+ When a hook event arrives with an unknown `session_id`, the matcher (`server/sessionMatcher.js`) tries the following priorities:
364
+
365
+ | Priority | Strategy | Match Condition | Risk |
366
+ |---|---|---|---|
367
+ | 0 | `pendingResume` + terminal ID | `agent_terminal_id` matches a pending resume entry | Low — explicit user action |
368
+ | 0 (fallback) | `pendingResume` + workDir | Exactly one pending resume has matching `projectPath` | Medium — ambiguous if multiple |
369
+ | 0.5 | Snapshot-restored ended session | One ended session with `ServerRestart` event matches `cwd` (within 30 min) | Low — post-restart linking |
370
+ | 1 | `agent_terminal_id` env var | SSH terminal injected `AGENT_MANAGER_TERMINAL_ID` into PTY env | Low — direct match |
371
+ | 2 | `tryLinkByWorkDir` | `pendingLinks` Map has entry for the hook's `cwd` | Medium — two sessions in same dir |
372
+ | 3 | Path scan (connecting sessions) | Exactly one `connecting` session has matching `projectPath` | Medium — ambiguous if multiple |
373
+ | 4 | PID parent check | Claude's PID is a child of a known PTY process (`ps -o ppid=`) | High — unreliable across shells |
374
+
375
+ If no match is found, a **display-only card** is created with the detected terminal source.
376
+
377
+ ### Session Source Detection
378
+
379
+ The `detectHookSource()` function maps environment variables to source labels:
380
+
381
+ | Source | Detection |
382
+ |---|---|
383
+ | `vscode` | `$VSCODE_PID` present, or `TERM_PROGRAM` contains `vscode`/`code` |
384
+ | `jetbrains` | `TERM_PROGRAM` contains `jetbrains`, `intellij`, `idea`, `webstorm`, etc. |
385
+ | `iterm` | `TERM_PROGRAM` contains `iterm` |
386
+ | `warp` | `TERM_PROGRAM` contains `warp` |
387
+ | `kitty` | `TERM_PROGRAM` contains `kitty` |
388
+ | `ghostty` | `TERM_PROGRAM` contains `ghostty` or `$GHOSTTY_RESOURCES_DIR` set |
389
+ | `alacritty` | `TERM_PROGRAM` contains `alacritty` |
390
+ | `wezterm` | `TERM_PROGRAM` contains `wezterm` or `$WEZTERM_PANE` set |
391
+ | `hyper` | `TERM_PROGRAM` contains `hyper` |
392
+ | `terminal` | `TERM_PROGRAM` is `apple_terminal` |
393
+ | `tmux` | `$TMUX` is set |
394
+ | `unknown` | No matching env var |
395
+
396
+ ### Session Re-keying
397
+
398
+ When a resumed session is matched, `reKeyResumedSession()` transfers data from the old session key to the new `session_id`:
399
+ - Deletes old Map entry
400
+ - Resets `status`, `animationState`, `emote`, `startedAt`, `totalToolCalls`, `toolUsage`, `promptHistory`, `toolLog`, `responseLog`, `events`
401
+ - Preserves `previousSessions` array (history chain)
402
+ - Sets `replacesId` for DB migration
403
+ - Inserts under new `session_id`
404
+
405
+ ---
406
+
407
+ ## 6. Approval Detection
408
+
409
+ ### 6.1 Timeout Heuristic
410
+
411
+ When `PreToolUse` fires, `startApprovalTimer()` sets a category-based timer:
412
+
413
+ | Category | Tools | Timeout | Status Set |
414
+ |---|---|---|---|
415
+ | `fast` | `Read`, `Write`, `Edit`, `Grep`, `Glob`, `NotebookEdit` | 3,000 ms | `approval` |
416
+ | `userInput` | `AskUserQuestion`, `EnterPlanMode`, `ExitPlanMode` | 3,000 ms | `input` |
417
+ | `medium` | `WebFetch`, `WebSearch` | 15,000 ms | `approval` |
418
+ | `slow` | `Bash`, `Task` | 8,000 ms | `approval` |
419
+
420
+ Tools not in any category get no timer. The timer is cleared immediately when `PostToolUse` or `PostToolUseFailure` arrives.
421
+
422
+ **Waiting detail labels:**
423
+
424
+ | Status | Label format |
425
+ |---|---|
426
+ | `approval` | `"Approve {toolName}: {inputSummary}"` or `"Approve {toolName}"` |
427
+ | `input` (`AskUserQuestion`) | `"Waiting for your answer"` |
428
+ | `input` (`EnterPlanMode`) | `"Review plan mode request"` |
429
+ | `input` (`ExitPlanMode`) | `"Review plan"` |
430
+
431
+ ### 6.2 `hasChildProcesses` Check
432
+
433
+ For `slow` category tools (Bash, Task), before setting `approval` status, the server checks if the cached PID still has child processes via:
434
+ ```bash
435
+ pgrep -P {pid}
436
+ ```
437
+ If child processes exist, the command is still running (not waiting for approval) and the status transition is skipped.
438
+
439
+ ### 6.3 `PermissionRequest` Direct Signal
440
+
441
+ When the `PermissionRequest` hook event fires (at medium+ density), the heuristic timer is immediately cleared and the session transitions directly to `approval` status. This is more reliable than the timeout approach.
442
+
443
+ ### 6.4 Timer Management
444
+
445
+ All pending timers are stored in a `Map<sessionId, timeoutHandle>`. A new timer for the same session replaces any existing one. All timers are cleared on:
446
+ - `PostToolUse`
447
+ - `PostToolUseFailure`
448
+ - `PermissionRequest`
449
+ - `Stop`
450
+ - `SessionEnd`
451
+ - Process liveness check (dead process)
452
+
453
+ ---
454
+
455
+ ## 7. Team and Subagent Tracking
456
+
457
+ ### 7.1 Auto-Detection (Path-Based)
458
+
459
+ When `SubagentStart` fires on a parent session, `addPendingSubagent()` records `{parentSessionId, parentCwd, agentType, timestamp}`. When a new `SessionStart` arrives within **10 seconds** from a child session whose `cwd` matches (exact or parent/child path relationship), the sessions are linked into a team.
460
+
461
+ Stale entries older than **30 seconds** are pruned from the pending list.
462
+
463
+ ### 7.2 Direct Linking (Priority 0)
464
+
465
+ When the `CLAUDE_CODE_PARENT_SESSION_ID` env var is set, `linkByParentSessionId()` directly links the child to its parent without path guessing. This is the preferred mechanism.
466
+
467
+ ### 7.3 Team Object
468
+
469
+ ```js
470
+ {
471
+ teamId: "team-{parentSessionId}",
472
+ parentSessionId: string,
473
+ childSessionIds: Set<string>,
474
+ teamName: string, // "{projectName} Team" or from env var
475
+ createdAt: number
476
+ }
477
+ ```
478
+
479
+ **Serialized form** (for WebSocket) converts `childSessionIds` Set to array.
480
+
481
+ ### 7.4 Team Config Reader
482
+
483
+ Team configurations can be stored in `~/.claude/teams/{teamName}/config.json`:
484
+ ```json
485
+ {
486
+ "members": {
487
+ "backend-engineer": {
488
+ "tmuxPaneId": "%3",
489
+ "backendType": "node",
490
+ "color": "#00ff88"
491
+ }
492
+ }
493
+ }
494
+ ```
495
+
496
+ The team name is sanitized (only `a-zA-Z0-9_-. `) before constructing the file path (path traversal prevention). If the config is found, the member's `tmuxPaneId`, `backendType`, and `agentColor` are applied to the child session.
497
+
498
+ ### 7.5 Team Cleanup
499
+
500
+ When a team member session ends, `handleTeamMemberEnd()` removes it from the team's `childSessionIds`. If the **parent** ends and all children are also ended, the team is deleted after a **15-second** delay.
501
+
502
+ ---
503
+
504
+ ## 8. SSH/PTY Terminal Management
505
+
506
+ ### 8.1 Terminal Modes
507
+
508
+ | Mode | How | When |
509
+ |---|---|---|
510
+ | Local direct | `node-pty` spawns `$SHELL` | `host` is `localhost`/`127.0.0.1`/`::1` |
511
+ | Remote SSH | `node-pty` spawns `ssh -t -i keyfile user@host` | Remote host |
512
+ | Tmux attach | Shell runs `tmux attach -t '{session}'` | `tmuxSession` parameter provided |
513
+ | Tmux new | Shell runs `tmux new-session -s 'claude-{id}' '{command}'` | `useTmux: true` |
514
+
515
+ ### 8.2 PTY Spawn Parameters
516
+
517
+ ```js
518
+ pty.spawn(shell, args, {
519
+ name: 'xterm-256color',
520
+ cols: 120,
521
+ rows: 40,
522
+ cwd, // workDir for local; homedir() for remote
523
+ env: {
524
+ ...process.env,
525
+ AGENT_MANAGER_TERMINAL_ID: terminalId,
526
+ // ANTHROPIC_API_KEY / GEMINI_API_KEY / OPENAI_API_KEY if apiKey provided
527
+ }
528
+ })
529
+ ```
530
+
531
+ Terminal IDs use the format: `term-{Date.now()}-{random6chars}`
532
+ Tmux terminal IDs: `term-tmux-{Date.now()}-{random6chars}`
533
+
534
+ ### 8.3 Shell-Ready Detection
535
+
536
+ Before sending the launch command, the server waits for the shell to display a prompt. The detection algorithm:
537
+
538
+ 1. Buffer PTY output (capped at 4096 bytes)
539
+ 2. Strip ANSI escape sequences (`CSI` + `OSC` patterns)
540
+ 3. After 100ms of silence (settle timer), check if the last non-empty line ends with `[#$%>]\s*$` and is shorter than 200 chars
541
+ 4. Resolve `true` (prompt detected) or `false` (timeout)
542
+
543
+ | Timeout | Local | Remote SSH |
544
+ |---|---|---|
545
+ | Shell-ready wait | 5,000 ms | 15,000 ms |
546
+
547
+ On timeout, the command is sent anyway with a warning log.
548
+
549
+ ### 8.4 Output Ring Buffer
550
+
551
+ Each terminal maintains an output ring buffer capped at **128 KB** (128 × 1024 bytes). When a new WebSocket client subscribes, the full buffer is replayed so the client sees previous terminal output.
552
+
553
+ ### 8.5 Pending Links
554
+
555
+ When `createTerminal()` is called, a `pendingLinks` entry is registered: `workDir → {terminalId, host, createdAt}`. This is used by the session matcher (Priority 2) to link the first `SessionStart` hook from that directory to the correct terminal session.
556
+
557
+ Pending links expire after **60 seconds** (cleaned up every 30s).
558
+
559
+ ### 8.6 Input Validation
560
+
561
+ All inputs to `createTerminal()` are validated against injection patterns. The shell metacharacter regex is: `/[;|&$\`\\!><()\n\r{}[\]]/`
562
+
563
+ | Parameter | Validation |
564
+ |---|---|
565
+ | `workingDir` | Max 1024 chars, no shell metacharacters (after stripping leading `~`) |
566
+ | `command` | Max 512 chars, no shell metacharacters |
567
+ | `tmuxSession` | Max 128 chars, only `[a-zA-Z0-9_.\-]` |
568
+ | `host` | Max 255 chars, no shell metacharacters |
569
+ | `username` | Max 128 chars, only `[a-zA-Z0-9_.\-]` |
570
+ | `port` | Integer 1–65535 |
571
+
572
+ API keys are passed via the PTY `env` object, never interpolated into shell command strings.
573
+
574
+ ### 8.7 Terminal Limits
575
+
576
+ Maximum **10 terminals** open simultaneously (enforced at `POST /api/terminals` and `POST /api/teams/:id/members/:sessionId/terminal`).
577
+
578
+ ---
579
+
580
+ ## 9. WebSocket Protocol
581
+
582
+ ### 9.1 Connection Lifecycle
583
+
584
+ 1. Client connects to `ws://localhost:3333`
585
+ 2. If password enabled: token validated via cookie, Authorization header, or `?token=` query param; rejected with code `4001` if invalid
586
+ 3. Server sends `snapshot` message with all current sessions, teams, and event sequence number
587
+ 4. Client sends `replay` request if it has a previous sequence number (reconnect case)
588
+ 5. Heartbeat: server pings every **30 seconds**, terminates unresponsive clients (no pong within **10 seconds**)
589
+
590
+ ### 9.2 Server → Client Messages
591
+
592
+ | Type | Payload | When Sent |
593
+ |---|---|---|
594
+ | `snapshot` | `{sessions, teams, seq}` | On initial connection |
595
+ | `session_update` | `{session, team?}` | Any session state change |
596
+ | `session_removed` | `{sessionId}` | Session deleted via API |
597
+ | `team_update` | `{team}` | Team membership change |
598
+ | `hook_stats` | `{stats}` | After each hook event (throttled to 1/sec) |
599
+ | `terminal_output` | `{terminalId, data: base64}` | PTY output |
600
+ | `terminal_ready` | `{terminalId}` | PTY spawned and ready |
601
+ | `terminal_closed` | `{terminalId, reason}` | PTY process exited |
602
+ | `clearBrowserDb` | — | Full reset requested via API |
603
+ | `replay` | event objects | Missed events on reconnect |
604
+
605
+ ### 9.3 Client → Server Messages
606
+
607
+ | Type | Payload | Action |
608
+ |---|---|---|
609
+ | `terminal_input` | `{terminalId, data}` | Write to PTY stdin |
610
+ | `terminal_resize` | `{terminalId, cols, rows}` | Resize PTY |
611
+ | `terminal_disconnect` | `{terminalId}` | Close PTY |
612
+ | `terminal_subscribe` | `{terminalId}` | Register WebSocket for terminal output relay + replay buffer |
613
+ | `update_queue_count` | `{sessionId, count}` | Update session queue count |
614
+ | `replay` | `{sinceSeq}` | Request missed events since sequence number |
615
+
616
+ ### 9.4 Backpressure
617
+
618
+ For non-critical messages (only `hook_stats`), the server checks `client.bufferedAmount`. If it exceeds **1 MB**, the message is dropped for that client.
619
+
620
+ ### 9.5 Hook Stats Throttle
621
+
622
+ `hook_stats` broadcasts are throttled to a maximum of **once per second** per client. If a stats update arrives while the throttle window is active, it is stored as `pendingHookStats` and sent when the window expires.
623
+
624
+ ---
625
+
626
+ ## 10. REST API
627
+
628
+ All endpoints except auth and hook ingestion require authentication when `passwordHash` is configured. The auth middleware checks: cookie `auth_token`, then `Authorization: Bearer {token}`, then `?token=` query param.
629
+
630
+ ### 10.1 Auth Endpoints (No Auth Required)
631
+
632
+ | Method | Path | Body | Description |
633
+ |---|---|---|---|
634
+ | `GET` | `/api/auth/status` | — | Returns `{passwordRequired, authenticated}` |
635
+ | `POST` | `/api/auth/login` | `{password}` | Returns `{success, token}`, sets `auth_token` cookie |
636
+ | `POST` | `/api/auth/logout` | — | Removes token, clears cookie |
637
+
638
+ ### 10.2 Hook Ingestion (No Auth — Rate Limited)
639
+
640
+ | Method | Path | Body | Description |
641
+ |---|---|---|---|
642
+ | `POST` | `/api/hooks` | Hook JSON | Processes a hook event; rate limit 100/sec per IP |
643
+
644
+ ### 10.3 Session Endpoints
645
+
646
+ | Method | Path | Body | Description |
647
+ |---|---|---|---|
648
+ | `GET` | `/api/sessions` | — | Returns all in-memory sessions |
649
+ | `GET` | `/api/sessions/:id/source` | — | Returns `{source}` (vscode/terminal/etc.) |
650
+ | `PUT` | `/api/sessions/:id/title` | `{title}` | Update session title (max 500 chars) |
651
+ | `PUT` | `/api/sessions/:id/label` | `{label}` | Update session label |
652
+ | `PUT` | `/api/sessions/:id/accent-color` | `{color}` | Update accent color (max 50 chars) |
653
+ | `POST` | `/api/sessions/:id/kill` | `{confirm: true}` | Send SIGTERM (SIGKILL after 3s), archive session |
654
+ | `DELETE` | `/api/sessions/:id` | — | Permanently delete from memory, broadcast removal |
655
+ | `POST` | `/api/sessions/:id/resume` | — | Resume SSH session (`claude --resume {id} \|\| claude --continue`) |
656
+ | `POST` | `/api/sessions/:id/summarize` | `{context, promptTemplate?, custom_prompt?}` | Summarize via Claude CLI (`haiku` model), max 2 concurrent |
657
+
658
+ ### 10.4 Terminal Endpoints
659
+
660
+ | Method | Path | Body | Description |
661
+ |---|---|---|---|
662
+ | `POST` | `/api/terminals` | Connection config | Create PTY terminal (max 10 total) |
663
+ | `GET` | `/api/terminals` | — | List all active terminals |
664
+ | `DELETE` | `/api/terminals/:id` | — | Close and cleanup terminal |
665
+
666
+ **`POST /api/terminals` body fields:**
667
+ `host`, `port` (default 22), `username` (required), `password`, `privateKeyPath`, `authMethod` (default `key`), `workingDir` (default `~`), `command` (default `claude`), `apiKey`, `tmuxSession`, `useTmux`, `sessionTitle`, `label`
668
+
669
+ ### 10.5 SSH Key and Tmux Endpoints
670
+
671
+ | Method | Path | Body | Description |
672
+ |---|---|---|---|
673
+ | `GET` | `/api/ssh-keys` | — | List private keys from `~/.ssh/` |
674
+ | `POST` | `/api/tmux-sessions` | Connection config | List tmux sessions on host |
675
+
676
+ ### 10.6 Team Endpoints
677
+
678
+ | Method | Path | Body | Description |
679
+ |---|---|---|---|
680
+ | `GET` | `/api/teams/:teamId/config` | — | Read team config from `~/.claude/teams/{name}/config.json` |
681
+ | `POST` | `/api/teams/:teamId/members/:sessionId/terminal` | — | Attach to member's tmux pane (requires `tmuxPaneId` on session) |
682
+
683
+ ### 10.7 Hook Management Endpoints
684
+
685
+ | Method | Path | Body | Description |
686
+ |---|---|---|---|
687
+ | `GET` | `/api/hooks/status` | — | Current density and installed events |
688
+ | `POST` | `/api/hooks/install` | `{density}` | Install hooks at specified density |
689
+ | `POST` | `/api/hooks/uninstall` | — | Remove all dashboard hooks |
690
+
691
+ ### 10.8 Database / History Endpoints
692
+
693
+ | Method | Path | Query Params | Description |
694
+ |---|---|---|---|
695
+ | `GET` | `/api/db/sessions` | `query`, `project`, `status`, `dateFrom`, `dateTo`, `archived`, `sortBy`, `sortDir`, `page`, `pageSize` | Search/list sessions from SQLite |
696
+ | `GET` | `/api/db/sessions/:id` | — | Full session detail with prompts, responses, tool_calls, events, notes |
697
+ | `DELETE` | `/api/db/sessions/:id` | — | Cascade delete session and all child records |
698
+ | `GET` | `/api/db/projects` | — | Distinct projects |
699
+ | `GET` | `/api/db/search` | `query`, `type` (`all`/`prompts`/`responses`), `page`, `pageSize` | Full-text search across prompts and responses |
700
+ | `GET` | `/api/sessions/history` | `projectPath?` | Legacy endpoint; returns all or project sessions |
701
+
702
+ ### 10.9 Notes Endpoints
703
+
704
+ | Method | Path | Body | Description |
705
+ |---|---|---|---|
706
+ | `GET` | `/api/db/sessions/:id/notes` | — | Get notes for session |
707
+ | `POST` | `/api/db/sessions/:id/notes` | `{text}` | Add note (max 10,000 chars) |
708
+ | `DELETE` | `/api/db/notes/:id` | — | Delete note by ID |
709
+
710
+ ### 10.10 Analytics Endpoints
711
+
712
+ | Method | Path | Description |
713
+ |---|---|---|
714
+ | `GET` | `/api/db/analytics/summary` | Total sessions, active sessions, total prompts, total tool calls, most used tool, busiest project |
715
+ | `GET` | `/api/db/analytics/tools` | Tool breakdown with counts and percentages |
716
+ | `GET` | `/api/db/analytics/projects` | Active projects with session count and last activity |
717
+ | `GET` | `/api/db/analytics/heatmap` | Activity heatmap by `{day_of_week, hour, count}` |
718
+
719
+ ### 10.11 Stats and Admin Endpoints
720
+
721
+ | Method | Path | Description |
722
+ |---|---|---|
723
+ | `GET` | `/api/hook-stats` | Hook performance statistics |
724
+ | `POST` | `/api/hook-stats/reset` | Reset hook stats |
725
+ | `GET` | `/api/mq-stats` | MQ reader stats (offset, linesProcessed, etc.) |
726
+ | `POST` | `/api/reset` | Broadcast `clearBrowserDb` to all clients |
727
+
728
+ ### 10.12 Rate Limiting
729
+
730
+ In-memory sliding-window rate limiter (no external dependencies):
731
+
732
+ | Endpoint | Limit |
733
+ |---|---|
734
+ | `/api/hooks` | 100 requests/second per IP |
735
+ | `/api/sessions/:id/summarize` | 2 concurrent requests |
736
+ | `/api/terminals` (POST) | 10 terminals total |
737
+
738
+ Stale rate limit buckets are cleaned up every **30 seconds** (entries older than 5s).
739
+
740
+ ---
741
+
742
+ ## 11. Authentication
743
+
744
+ Authentication is optional. It is enabled when `passwordHash` is set in `data/server-config.json`.
745
+
746
+ ### 11.1 Password Hashing
747
+
748
+ Uses Node.js `crypto.scryptSync`:
749
+
750
+ ```
751
+ salt = randomBytes(16).toString('hex') // 32 hex chars
752
+ hash = scryptSync(password, salt, 64).toString('hex') // 128 hex chars
753
+ stored = "${salt}:${hash}"
754
+ ```
755
+
756
+ Verification uses `crypto.timingSafeEqual` to prevent timing attacks. Passwords must be at least 4 characters (enforced by setup wizard).
757
+
758
+ ### 11.2 Token Management
759
+
760
+ - Tokens are 32 random bytes encoded as hex (64 hex chars)
761
+ - TTL: **24 hours** (`TOKEN_TTL_MS = 24 * 60 * 60 * 1000`)
762
+ - Stored in-memory: `Map<token, {createdAt: number}>`
763
+ - Expired tokens are removed lazily on `validateToken()` check
764
+ - Periodic cleanup runs every **1 hour** to sweep all expired tokens
765
+
766
+ ### 11.3 Token Extraction Priority
767
+
768
+ 1. Cookie: `auth_token={token}`
769
+ 2. HTTP header: `Authorization: Bearer {token}`
770
+ 3. Query string: `?token={token}` (used for WebSocket connections)
771
+
772
+ ### 11.4 Protected vs Unprotected Routes
773
+
774
+ | Routes | Auth Required |
775
+ |---|---|
776
+ | `/api/auth/*` | No |
777
+ | `/api/hooks` | No (hooks must work without login) |
778
+ | Static files | No (login page is part of SPA) |
779
+ | All other `/api/*` | Yes (if password enabled) |
780
+ | WebSocket | Yes (if password enabled); rejected with WS code `4001` |
781
+
782
+ Cookie is set with: `HttpOnly; SameSite=Strict; Path=/; Max-Age=86400`
783
+
784
+ ---
785
+
786
+ ## 12. SQLite Persistence
787
+
788
+ ### 12.1 Database Location
789
+
790
+ ```
791
+ data/sessions.db
792
+ ```
793
+
794
+ Opened with **WAL mode** (`PRAGMA journal_mode = WAL`) for concurrent reads and writes.
795
+
796
+ ### 12.2 Schema
797
+
798
+ **Table: `sessions`**
799
+
800
+ | Column | Type | Notes |
801
+ |---|---|---|
802
+ | `id` | TEXT PRIMARY KEY | Claude session UUID |
803
+ | `project_path` | TEXT | Full working directory |
804
+ | `project_name` | TEXT | Project display name |
805
+ | `title` | TEXT | Session title |
806
+ | `model` | TEXT | AI model name |
807
+ | `status` | TEXT | Final status |
808
+ | `source` | TEXT DEFAULT 'hook' | `hook`, `ssh`, `vscode`, etc. |
809
+ | `label` | TEXT | User-assigned label |
810
+ | `summary` | TEXT | AI-generated summary |
811
+ | `team_id` | TEXT | Team reference |
812
+ | `team_role` | TEXT | `leader` or `member` |
813
+ | `character_model` | TEXT | 3D character model override |
814
+ | `accent_color` | TEXT | Custom accent color |
815
+ | `started_at` | INTEGER | Unix ms |
816
+ | `ended_at` | INTEGER | Unix ms or null |
817
+ | `last_activity_at` | INTEGER | Unix ms |
818
+ | `total_prompts` | INTEGER DEFAULT 0 | |
819
+ | `total_tool_calls` | INTEGER DEFAULT 0 | |
820
+ | `archived` | INTEGER DEFAULT 0 | 0 or 1 |
821
+
822
+ **Indexes:** `project_path`, `status`, `started_at`, `last_activity_at`
823
+
824
+ **Table: `prompts`**
825
+
826
+ | Column | Type | Notes |
827
+ |---|---|---|
828
+ | `id` | INTEGER PK AUTOINCREMENT | |
829
+ | `session_id` | TEXT NOT NULL | FK → sessions.id |
830
+ | `text` | TEXT | Prompt text |
831
+ | `timestamp` | INTEGER | Unix ms |
832
+
833
+ **Unique index:** `(session_id, timestamp)` — deduplication on upsert.
834
+
835
+ **Table: `responses`**
836
+
837
+ | Column | Type | Notes |
838
+ |---|---|---|
839
+ | `id` | INTEGER PK AUTOINCREMENT | |
840
+ | `session_id` | TEXT NOT NULL | FK → sessions.id |
841
+ | `text_excerpt` | TEXT | First 2000 chars of response |
842
+ | `timestamp` | INTEGER | Unix ms |
843
+
844
+ **Unique index:** `(session_id, timestamp)`
845
+
846
+ **Table: `tool_calls`**
847
+
848
+ | Column | Type | Notes |
849
+ |---|---|---|
850
+ | `id` | INTEGER PK AUTOINCREMENT | |
851
+ | `session_id` | TEXT NOT NULL | FK → sessions.id |
852
+ | `tool_name` | TEXT | Tool name |
853
+ | `tool_input_summary` | TEXT | Summarized input |
854
+ | `timestamp` | INTEGER | Unix ms |
855
+
856
+ **Unique index:** `(session_id, timestamp, tool_name)`
857
+ **Additional index:** `tool_name` (for analytics queries)
858
+
859
+ **Table: `events`**
860
+
861
+ | Column | Type | Notes |
862
+ |---|---|---|
863
+ | `id` | INTEGER PK AUTOINCREMENT | |
864
+ | `session_id` | TEXT NOT NULL | FK → sessions.id |
865
+ | `event_type` | TEXT | Hook event name |
866
+ | `detail` | TEXT | Human-readable description |
867
+ | `timestamp` | INTEGER | Unix ms |
868
+
869
+ **Table: `notes`**
870
+
871
+ | Column | Type | Notes |
872
+ |---|---|---|
873
+ | `id` | INTEGER PK AUTOINCREMENT | |
874
+ | `session_id` | TEXT NOT NULL | FK → sessions.id |
875
+ | `text` | TEXT | Note content |
876
+ | `created_at` | INTEGER | Unix ms |
877
+ | `updated_at` | INTEGER | Unix ms |
878
+
879
+ ### 12.3 Upsert Strategy
880
+
881
+ All child records (prompts, responses, tool_calls) use `INSERT OR IGNORE` with unique indexes for deduplication. The `sessions` table uses `INSERT ... ON CONFLICT(id) DO UPDATE SET ...`, updating all mutable fields. A single `db.transaction()` wraps the full upsert for atomicity.
882
+
883
+ ### 12.4 Persist-on-Events
884
+
885
+ Only key state transitions trigger a DB write:
886
+
887
+ | Event | Triggers DB Upsert |
888
+ |---|---|
889
+ | `SessionStart` | Yes |
890
+ | `UserPromptSubmit` | Yes |
891
+ | `Stop` | Yes |
892
+ | `SessionEnd` | Yes |
893
+
894
+ Other events (PreToolUse, PostToolUse, etc.) are not persisted to DB individually.
895
+
896
+ ### 12.5 Cascade Delete
897
+
898
+ `deleteSessionCascade()` is a transaction that deletes from: `prompts`, `responses`, `tool_calls`, `events`, `notes`, then `sessions` — in that order.
899
+
900
+ ### 12.6 Session ID Migration
901
+
902
+ When a session is re-keyed (resume), `migrateSessionId(oldId, newId)` updates `session_id` in all child tables in a single transaction.
903
+
904
+ ### 12.7 Search
905
+
906
+ `searchSessions()` supports: text search (via `prompts` subquery with `LIKE`), project filter, status filter, date range, archived filter, sorting by `started_at`/`last_activity_at`/`project_name`/`status`, and pagination.
907
+
908
+ `fullTextSearch()` searches both `prompts.text` and `responses.text_excerpt` via `LIKE` patterns, merging and sorting results by timestamp.
909
+
910
+ ### 12.8 Analytics Queries
911
+
912
+ | Query | What It Returns |
913
+ |---|---|
914
+ | `getSummaryStats()` | Total/active sessions, total prompts, total tool calls, most-used tool, busiest project |
915
+ | `getToolBreakdown()` | Per-tool count and percentage of total |
916
+ | `getActiveProjects()` | Projects with session count and last activity |
917
+ | `getHeatmap()` | Activity grid by `{day_of_week (0=Mon), hour, count}` |
918
+
919
+ ---
920
+
921
+ ## 31. Server Infrastructure
922
+
923
+ ### 31.1 Port Resolution
924
+
925
+ Port is resolved in order: `--port` CLI flag → `PORT` environment variable → `config.port` → default **3333**.
926
+
927
+ Port conflict handling: on `EADDRINUSE`, the server calls `killPortProcess(port)` which uses `lsof -ti:{port}` (macOS/Linux) or `netstat -ano | findstr :port` (Windows) to find and SIGTERM the occupying process, then retries binding after **1 second**.
928
+
929
+ ### 31.2 Graceful Shutdown
930
+
931
+ Signals handled: `SIGTERM`, `SIGINT`
932
+
933
+ Shutdown sequence:
934
+ 1. Stop periodic snapshot save
935
+ 2. Stop WebSocket heartbeat
936
+ 3. Stop MQ reader (performs final read to flush)
937
+ 4. Stop auth token cleanup
938
+ 5. Save final snapshot with current MQ offset
939
+ 6. Close SQLite database
940
+ 7. Close HTTP server
941
+ 8. Exit 0 (forced exit 1 after **5 seconds** if not clean)
942
+
943
+ ### 31.3 Global Error Handlers
944
+
945
+ | Handler | Behavior |
946
+ |---|---|
947
+ | `uncaughtException` | Log error; exit 1 if `out of memory` or `ENOMEM` |
948
+ | `unhandledRejection` | Log error and stack; continue |
949
+
950
+ ### 31.4 Process Monitor
951
+
952
+ `processMonitor.js` runs every **15,000 ms** (configurable via `serverConfig.processCheckInterval`) and checks `process.kill(pid, 0)` for each non-ended session with a `cachedPid`. Dead processes trigger:
953
+ - Session marked `ended` + `Death` animation
954
+ - PID released from `pidToSession` Map
955
+ - Approval timer cleared
956
+ - Team cleanup
957
+ - Broadcast to browsers
958
+ - SSH sessions: marked `isHistorical`, terminal link cleared
959
+ - Non-SSH sessions: scheduled for deletion after 10 seconds
960
+
961
+ Sessions with an active PTY terminal are skipped (terminal is the source of truth).
962
+
963
+ **`findClaudeProcess()` fallback chain:**
964
+ 1. Cached PID (validated with signal 0)
965
+ 2. `pgrep -f claude` → match by `cwd` via `lsof -a -d cwd` (macOS) or `/proc/{pid}/cwd` (Linux)
966
+ 3. TTY fallback: first unclaimed PID with a TTY attached
967
+ 4. Last resort: first unclaimed PID
968
+
969
+ ### 31.5 Rate Limiting
970
+
971
+ In-memory sliding-window rate limiter using `Map<key, {count, windowStart}>`. Window is **1 second**. Stale buckets (older than 5s) are cleaned every **30 seconds**.
972
+
973
+ ### 31.6 Logger
974
+
975
+ Levels:
976
+ - `info` — always shown
977
+ - `warn` — always shown (yellow `WARN` prefix)
978
+ - `error` — always shown (red `ERROR` prefix)
979
+ - `debug` — only in debug mode (magenta `DEBUG` prefix)
980
+ - `debugJson` — only in debug mode (full JSON.stringify with indent 2)
981
+
982
+ Format: `[ISO timestamp] [tag] message`
983
+
984
+ Debug mode is activated by `--debug` CLI flag or `"debug": true` in `data/server-config.json`.
985
+
986
+ ### 31.7 Server Config (`data/server-config.json`)
987
+
988
+ | Field | Type | Default | Description |
989
+ |---|---|---|---|
990
+ | `port` | number | `3333` | HTTP/WS listen port |
991
+ | `hookDensity` | string | `"medium"` | `high`, `medium`, or `low` |
992
+ | `debug` | boolean | `false` | Verbose logging |
993
+ | `processCheckInterval` | number | `15000` | PID liveness check interval (ms) |
994
+ | `sessionHistoryHours` | number | `24` | History retention (used by setup wizard) |
995
+ | `enabledClis` | string[] | `["claude"]` | Which CLIs to install hooks for |
996
+ | `passwordHash` | string\|null | `null` | `salt:hash` for dashboard login |
997
+
998
+ ### 31.8 Network Interface Detection
999
+
1000
+ On startup, `getLocalIP()` probes network interfaces in preferred order: `en0`, `en1`, `eth0`, `wlan0`, then falls back to any non-internal IPv4. The LAN IP is displayed in the startup log.
1001
+
1002
+ ### 31.9 Static File Serving
1003
+
1004
+ If `dist/client/` directory exists (Vite build output), it is served from there. Otherwise, `public/` is served. The React SPA fallback (`GET /*`) serves `dist/client/index.html` for client-side routing.
1005
+
1006
+ ---
1007
+
1008
+ ## 32. CLI and Setup
1009
+
1010
+ ### 32.1 npx Entry Point (`bin/cli.js`)
1011
+
1012
+ When invoked as `npx ai-agent-session-center` or after global install:
1013
+
1014
+ 1. Check if `data/server-config.json` exists
1015
+ 2. If missing (`isFirstRun`) or `--setup` flag passed: run `hooks/setup-wizard.js`
1016
+ 3. On wizard exit 0: start `server/index.js` with remaining args
1017
+ 4. On wizard exit non-0: exit with same code
1018
+ 5. If config exists and no `--setup`: start server directly
1019
+
1020
+ ### 32.2 Setup Wizard (`hooks/setup-wizard.js`)
1021
+
1022
+ Interactive 6-step wizard:
1023
+
1024
+ | Step | Question | Options |
1025
+ |---|---|---|
1026
+ | 1 | Server port | Free-form number (default: `3333`) |
1027
+ | 2 | AI CLIs to hook | Claude only / Claude+Gemini / Claude+Codex / All three |
1028
+ | 3 | Hook density | `high` (14 events) / `medium` (12 events, default) / `low` (5 events) |
1029
+ | 4 | Debug mode | Off (default) / On |
1030
+ | 5 | Session history retention | 12h / 24h (default) / 48h / 7 days |
1031
+ | 6 | Dashboard password | No password (default) / Set password (min 4 chars, confirmed) |
1032
+
1033
+ Password input uses raw mode TTY (`process.stdin.setRawMode(true)`) with character-by-character echo masking (`*`). Re-running the wizard shows existing config as defaults and allows keeping/changing/removing the password.
1034
+
1035
+ After wizard completes:
1036
+ 1. Saves `data/server-config.json`
1037
+ 2. Runs `hooks/install-hooks.js --density {density} --clis {clis}` to install hooks
1038
+
1039
+ ### 32.3 Auto-Install on Startup
1040
+
1041
+ `ensureHooksInstalled(config)` runs on every server startup. It:
1042
+ 1. Reads `data/server-config.json` for `hookDensity` and `enabledClis`
1043
+ 2. For each enabled CLI: copies the hook script to the CLI's hooks directory (if content changed)
1044
+ 3. Reads the CLI's settings file
1045
+ 4. Adds missing event registrations (checks for `dashboard-hook` in existing commands)
1046
+ 5. Writes settings atomically if any changes were made
1047
+
1048
+ **Hook registration markers:** Each added entry includes `"_source": "ai-agent-session-center"` so the reset tool can identify and remove them.
1049
+
1050
+ ### 32.4 Available npm Scripts
1051
+
1052
+ | Script | Command | Description |
1053
+ |---|---|---|
1054
+ | `npm start` | `node server/index.js` | Start server, auto-open browser |
1055
+ | `npm run start:no-open` | `node server/index.js --no-open` | Start without opening browser |
1056
+ | `npm run debug` | `node server/index.js --debug` | Start with verbose logging |
1057
+ | `npm run setup` | `node hooks/setup-wizard.js` | Interactive setup wizard |
1058
+ | `npm run install-hooks` | `node hooks/install-hooks.js` | Install hooks into CLI settings |
1059
+ | `npm run uninstall-hooks` | `node hooks/install-hooks.js --uninstall` | Remove all dashboard hooks |
1060
+ | `npm run reset` | `node hooks/reset.js` | Remove hooks, clean config, create backup |
1061
+ | `npm test` | Test suite | Run tests |
1062
+ | `npm run test:watch` | Test suite watch | Run tests in watch mode |
1063
+
1064
+ ### 32.5 Hook Performance Stats
1065
+
1066
+ `hookStats.js` maintains rolling statistics per event type (last **200 samples**) and globally (last **1 minute** for rate):
1067
+
1068
+ | Metric | Description |
1069
+ |---|---|
1070
+ | `count` | Total hooks received of this type |
1071
+ | `rate` | Hooks received in last 60 seconds |
1072
+ | `latency.avg/min/max/p95` | Delivery latency (hook_sent_at → server received) in ms |
1073
+ | `processing.avg/min/max/p95` | Server `handleEvent()` duration in ms |
1074
+ | `totalHooks` | Global total |
1075
+ | `hooksPerMin` | Global rate (hooks in last 60s) |
1076
+
1077
+ Stats are broadcast as `hook_stats` WebSocket messages after each hook event (throttled to 1/sec per client).