The Codeman notification system is a **multi-layer, event-driven pipeline** that flows from backend event emitters, through SSE broadcasts, to a frontend `NotificationManager` class. The backend itself has no concept of "notifications" -- it broadcasts structured SSE events, and the frontend decides which events warrant user notification (browser notifications, audio alerts, tab title flashing, in-app notification drawer, tab alert badges).
The system handles ~25 distinct notification-triggering SSE events across 5 categories: hook events, session lifecycle, respawn state machine, Ralph Loop, and UI actions.
These are resolved at runtime by the shell, so the hook config is static per case.
### 2.5 Hook Config Writing (Lines 107-129)
`writeHooksConfig()` merges hook config into existing `.claude/settings.local.json`, preserving other keys. Called during case creation (server.ts lines 2197, 2441).
**Not handled by respawn controller**: `teammate_idle`, `task_completed`, `permission_prompt`. The first two are team-related hooks that have no backend integration beyond being broadcast via SSE (and the frontend has no handlers either -- see Section 7).
**None of these trigger frontend notifications.** The frontend auto-opens subagent windows on `subagent:discovered` (app.js line 2887), but the `NotificationManager` is not invoked.
### 5.2 TeamWatcher (`src/team-watcher.ts`)
**CRITICAL FINDING: TeamWatcher is completely unintegrated with the server.**
- The class is defined in `src/team-watcher.ts` and extends `EventEmitter`
- It emits events: `teamCreated`, `teamUpdated`, `teamRemoved`, `taskUpdated`, `inboxMessage`
- It is **never imported** in `server.ts` or any other file
- The `hasActiveTeammates()` method (designed for idle detection) is never called
- There is no SSE broadcast for any team event
- The frontend has no SSE listeners for `team:*` events
The frontend does have team-related UI code (teammate badges, team task panel, teammate terminal windows at app.js lines 13057-13501), but this appears to be driven by polling APIs or subagent watcher integration rather than dedicated SSE events.
### 5.3 Team Hook Events (teammate_idle, task_completed)
These are accepted by the API endpoint (validated by `HookEventSchema`) and broadcast as `hook:teammate_idle` and `hook:task_completed` SSE events, but:
- The **frontend has no SSE listeners** for `hook:teammate_idle` or `hook:task_completed`
- They are recorded in the **run summary** via `recordHookEvent()` but otherwise silently dropped
---
## 6. Run Summary (`src/run-summary.ts`)
### 6.1 Overview
The RunSummaryTracker records a timeline of events per session for "what happened while I was away" views. It is a **recording system**, not a notification system -- it does not trigger any notifications itself.
### 6.3 Integration Points with Notification System
The run summary and notification system are **parallel but independent**:
- Both consume the same backend events (hooks, idle, working, errors)
- Run summary records for historical review; notifications alert in real-time
- There is no feedback loop between them (e.g., run summary does not trigger delayed notifications)
### 6.4 State Stuck Detection (Lines 402-421)
The RunSummaryTracker has its own state-stuck detection (10-minute threshold, checked every 60s) that records `state_stuck` events. This is separate from the frontend's idle-stuck notification (which uses `stuckThresholdMs`, default 10 minutes, triggered by `session:idle` events).
**Potential overlap**: Both the run summary and the frontend independently detect "stuck" states. The run summary records it; the frontend notifies. They could diverge if their thresholds or detection logic differ.
There is **no dedicated notification type** in the backend. The backend has no `Notification` interface or notification-specific data structures. All notification logic lives in the frontend `NotificationManager` class (`app.js` lines 859-1230).
---
## 8. Bugs and Issues
### 8.1 TeamWatcher Not Integrated (Critical Gap)
**File**: `src/team-watcher.ts` (entire file)
**Issue**: TeamWatcher is defined but never instantiated or imported in the server. The `hasActiveTeammates()` method was designed for team-aware idle detection (preventing premature respawn when teammates are still working), but it is never called.
**Impact**:
- The respawn controller has no awareness of active teammates
- Team events (member join/leave, task updates, inbox messages) are never broadcast to clients
- The frontend's team UI must rely on other mechanisms (likely API polling or subagent watcher)
### 8.2 `hook:teammate_idle` and `hook:task_completed` Are Dead Events
**Issue**: These hook events are accepted by the API, validated, and broadcast via SSE, but:
- The respawn controller does not handle them (line 3408-3418 -- only checks elicitation, stop, idle_prompt)
- The frontend has no `addListener('hook:teammate_idle', ...)` or `addListener('hook:task_completed', ...)`
- They are recorded in the run summary but otherwise have zero effect
**Impact**: When Claude Code fires TeammateIdle or TaskCompleted hooks, the data is broadcast into the void. No notification, no UI update, no respawn logic.
**Issue**: The server broadcasts `respawn:error` events, but the frontend has no listener for this event. Respawn errors are silently ignored on the client side.
**Impact**: If the respawn controller encounters an error (e.g., PTY write failure), the user gets no notification.
### 8.4 `respawn:cycleCompleted` Has No Frontend Handler
**File**: `src/web/server.ts` (line 4154)
**Issue**: `respawn:cycleCompleted` is broadcast but has no frontend listener. The cycle count is updated via `respawn:cycleStarted`, but completion is not acknowledged.
### 8.5 `respawn:stepCompleted` Has No Frontend Handler
**File**: `src/web/server.ts` (line 4169)
**Issue**: Broadcast but not listened to in the frontend.
### 8.6 Image Detection Lacks Notification
**File**: `src/web/public/app.js` (line 3039)
**Issue**: `image:detected` events auto-open a popup window but do not trigger the `NotificationManager`. If the user is on another tab, they get no notification that a screenshot or generated image was detected.
**Issue**: Subagent events auto-open windows but do not trigger notifications. The notification preferences have `subagent_spawn` and `subagent_complete` event types defined (app.js line 906-907) with defaults of `enabled: false`, but no code actually calls `notificationManager.notify()` for these events.
**Impact**: The notification preferences UI shows toggle switches for subagent events, but they do nothing -- the notifications are never triggered regardless of the setting.
### 8.8 `session:autoCompact` Has No Notification
**File**: `src/web/public/app.js`
**Issue**: `session:autoClear` triggers a notification (app.js line 2623), but `session:autoCompact` does not. Both are significant session events (context reset vs. context compaction). The `autoCompact` SSE event is handled (line 2633 area) but only shows a toast if it's the active session, with no `NotificationManager.notify()` call.
**Note**: After reviewing the code more carefully, `session:autoCompact` is not in the file at the lines I checked. It may be handled elsewhere or may genuinely be missing a notification.
### 8.9 Cache Invalidation Pattern Is Overly Broad
**File**: `src/web/server.ts` (line 4648)
**Issue**: `if (event.startsWith('session:') || event === 'respawn:')` -- the `respawn:` check uses exact equality, but all respawn events are formatted as `respawn:stateChanged`, `respawn:blocked`, etc. The check `event === 'respawn:'` will never match. This means respawn events do NOT invalidate the cached state.
**Impact**: After respawn state changes, the cached `getLightSessionsState()` may serve stale data until a `session:*` event triggers invalidation. Since respawn status is included in session state (via `getSessionStateWithRespawn()`), subsequent API calls to `GET /api/sessions` or SSE reconnects could show outdated respawn info.
---
## 9. Missing Notification Paths
### 9.1 Events That SHOULD Notify But Don't
| Event | Current Behavior | Suggested Notification |