Full product rename across 109 files (~834 occurrences): - Env vars: CLAUDEMAN_* → CODEMAN_* - Data dirs: ~/.claudeman/ → ~/.codeman/, ~/claudeman-cases/ → ~/codeman-cases/ - tmux prefix: claudeman- → codeman- - localStorage: claudeman-* → codeman-* - Package/CLI: claudeman → codeman - GitHub repo: Ark0N/Claudeman → Ark0N/Codeman - systemd service: claudeman-web → codeman-web - Class: ClaudemanApp → CodemanApp Migration infrastructure for seamless transition: - state-store.ts: auto-migrates data directories on startup - tmux-manager.ts: dual-prefix detection (legacy claudeman- sessions) - app.js: localStorage key migration (preserves old keys) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
23 KiB
Codeman Notification System - Backend Research Report
Date: 2026-02-17
Executive Summary
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.
1. Server-Side Notification Logic (src/web/server.ts)
1.1 The broadcast() Method (Line 4646)
All real-time client communication flows through a single private method:
private broadcast(event: string, data: unknown): void {
// Invalidate caches on state-changing broadcasts
if (event.startsWith('session:') || event === 'respawn:') {
this.cachedLightState = null;
this.cachedSessionsList = null;
}
let message: string;
try {
message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
} catch (err) {
console.error(`[Server] Failed to serialize SSE event "${event}":`, err);
return;
}
for (const client of this.sseClients) {
this.sendSSEPreformatted(client, message);
}
}
Key characteristics:
- Serializes JSON once, then writes to all connected SSE clients
- Has backpressure handling (
sendSSEPreformattedtracksbackpressuredClients) - Silently drops events on serialization failure (circular refs)
- Cache invalidation is broad -- any
session:*event clears caches
1.2 SSE Client Management (Line 547-564)
Clients connect at GET /api/events:
- Immediately sent
initevent with lightweight state (no terminal buffers) - Tracked in
Set<FastifyReply>(this.sseClients) - Dead client cleanup runs every 30s (
SSE_HEALTH_CHECK_INTERVAL) - Max 100 SSE clients (
MAX_SSE_CLIENTSfrommap-limits.ts)
1.3 Complete Catalog of Notification-Relevant Broadcasts
The server emits ~70 distinct SSE event types. Those that trigger frontend notifications are:
| SSE Event | Server Location | Frontend Notification? | Category |
|---|---|---|---|
hook:idle_prompt |
Line 3431 | Yes - warning | Hook |
hook:permission_prompt |
Line 3431 | Yes - critical | Hook |
hook:elicitation_dialog |
Line 3431 | Yes - critical | Hook |
hook:stop |
Line 3431 | Yes - info | Hook |
hook:teammate_idle |
Line 3431 | NO (no frontend handler) | Hook |
hook:task_completed |
Line 3431 | NO (no frontend handler) | Hook |
session:error |
Line 3921 | Yes - critical | Session |
session:exit |
Line 3939 | Yes - critical (non-zero code) | Session |
session:idle |
Line 3976 | Yes - warning (after stuck threshold) | Session |
session:autoClear |
Line 4009 | Yes - info | Session |
session:ralphCompletionDetected |
Line 4044 | Yes - warning | Ralph |
session:circuitBreakerUpdate |
Line 4066 | Yes - critical (OPEN state) | Ralph |
session:exitGateMet |
Line 4080 | Yes - warning | Ralph |
respawn:blocked |
Line 4158 | Yes - critical | Respawn |
respawn:autoAcceptSent |
Line 4177 | Yes - info | Respawn |
Events that update UI but do NOT trigger notifications:
session:working(line 3966) -- clears stuck timer and tab alertssession:completion(line 3928) -- updates cost displaysession:updated(many locations) -- tab/panel state updatesrespawn:stateChanged(line 4143) -- banner update onlyrespawn:cycleStarted(line 4150) -- cycle counter updatesubagent:discovered(line 458) -- auto-opens window, no notificationsubagent:completed(line 464) -- no notificationimage:detected(line 504) -- auto-opens popup, no notificationtranscript:*events (lines 3575-3591) -- no frontend handlers for notifications
1.4 Terminal Data Batching
Terminal and output data use separate batching pipelines that bypass broadcast():
batchTerminalData()(line 4668) -- adaptive 16-50ms batching for PTY outputbatchOutputData()(line 4741) -- 50ms batching for parsed text output- Both flush through
broadcast('session:terminal', ...)andbroadcast('session:output', ...)
2. Hook Events System (src/hooks-config.ts)
2.1 Hook Configuration Generator (Lines 24-67)
The generateHooksConfig() function creates .claude/settings.local.json entries that make Claude Code POST to Codeman when hooks fire:
const curlCmd = (event: HookEventType) =>
`HOOK_DATA=$(cat 2>/dev/null || echo '{}'); ` +
`curl -s -X POST "$CODEMAN_API_URL/api/hook-event" ` +
`-H 'Content-Type: application/json' ` +
`-d "{\\"event\\":\\"${event}\\",\\"sessionId\\":\\"$CODEMAN_SESSION_ID\\",\\"data\\":$HOOK_DATA}" ` +
`2>/dev/null || true`;
Six hook types are configured:
| Hook Category | Matcher | Event Type |
|---|---|---|
| Notification | idle_prompt |
idle_prompt |
| Notification | permission_prompt |
permission_prompt |
| Notification | elicitation_dialog |
elicitation_dialog |
| Stop | (all stops) | stop |
| TeammateIdle | (all) | teammate_idle |
| TaskCompleted | (all) | task_completed |
2.2 Hook Event Flow
Claude Code Hook Fires
--> Shell command executes (curl)
--> POST /api/hook-event with {event, sessionId, data}
--> Zod validation (HookEventSchema, schemas.ts:79)
--> Session lookup (must exist)
--> Respawn controller signaling (elicitation/stop/idle_prompt only)
--> Transcript watcher setup (if data.transcript_path present)
--> Data sanitization (sanitizeHookData, server.ts:178)
--> SSE broadcast as `hook:{eventType}`
--> Run summary tracking (recordHookEvent)
2.3 Data Sanitization (Lines 178-211)
The sanitizeHookData() function limits what gets broadcast:
- Allowed keys:
hook_event_name,tool_name,tool_input,session_id,cwd,permission_mode,stop_hook_active,transcript_path tool_inputobjects are summarized (command truncated to 500 chars, only summary fields forwarded)- Total data size capped at
MAX_HOOK_DATA_SIZE(line 135)
2.4 Environment Variables (Lines 70-101)
Two env vars are set per case directory via updateCaseEnvVars():
CODEMAN_API_URL-- server URL (e.g.,http://localhost:3000)CODEMAN_SESSION_ID-- session identifier
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).
3. Hook-to-Respawn Controller Integration (src/web/server.ts, Lines 3406-3418)
Three of the six hook types signal the respawn controller:
| Hook Event | Controller Method | Effect |
|---|---|---|
elicitation_dialog |
signalElicitation() |
Blocks auto-accept (prevents Enter press on question prompts) |
stop |
signalStopHook() |
Definitive idle signal; starts short confirmation timer, skips AI check |
idle_prompt |
signalIdlePrompt() |
Definitive 60s+ idle signal; cancels all detection timers, directly confirms idle |
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).
4. Respawn Controller Events (src/respawn-controller.ts)
The RespawnController extends EventEmitter and emits many events that the server wires to SSE broadcasts (server.ts lines 4143-4271).
4.1 Notification-Triggering Events
| Controller Event | SSE Broadcast | Frontend Notification? |
|---|---|---|
respawnBlocked |
respawn:blocked |
Yes - critical (reason: circuit_breaker, exit_signal, status_blocked, session_error, session_stopped, no_pty) |
autoAcceptSent |
respawn:autoAcceptSent |
Yes - info ("Plan Accepted") |
4.2 UI-Only Events (No Notification)
| Controller Event | SSE Broadcast | Frontend Action |
|---|---|---|
stateChanged |
respawn:stateChanged |
Banner state label update |
respawnCycleStarted |
respawn:cycleStarted |
Cycle counter update |
respawnCycleCompleted |
respawn:cycleCompleted |
(no explicit handler) |
detectionUpdate |
respawn:detectionUpdate |
Detection display update |
stepSent |
respawn:stepSent |
(no-op handler) |
stepCompleted |
respawn:stepCompleted |
(no handler) |
aiCheckStarted/Completed/Failed |
respawn:aiCheck* |
(no-op handlers) |
aiCheckCooldown |
respawn:aiCheckCooldown |
(no-op handler) |
planCheckStarted/Completed/Failed |
respawn:planCheck* |
(no handlers) |
timerStarted/Cancelled/Completed |
respawn:timer* |
Countdown timer UI |
actionLog |
respawn:actionLog |
Action log display |
log |
respawn:log |
Debug log |
error |
respawn:error |
(no handler) |
4.3 Respawn-to-Run-Summary Integration
The server wires respawn state changes into the run summary tracker (server.ts line 4143):
this.broadcast('respawn:stateChanged', { sessionId, state, prevState });
// Also records in run summary:
const summaryTracker = this.runSummaryTrackers.get(sessionId);
if (summaryTracker) summaryTracker.recordStateChange(state);
5. Team / Agent Notification Flow
5.1 SubagentWatcher (src/subagent-watcher.ts)
The SubagentWatcher emits events that the server wires to SSE broadcasts (server.ts lines 458-477):
discovered: (info) => this.broadcast('subagent:discovered', info),
updated: (info) => this.broadcast('subagent:updated', info),
toolCall: (data) => this.broadcast('subagent:tool_call', data),
toolResult: (data) => this.broadcast('subagent:tool_result', data),
progress: (data) => this.broadcast('subagent:progress', data),
message: (data) => this.broadcast('subagent:message', data),
completed: (info) => this.broadcast('subagent:completed', info),
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.tsand extendsEventEmitter - It emits events:
teamCreated,teamUpdated,teamRemoved,taskUpdated,inboxMessage - It is never imported in
server.tsor 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 respawn controller ignores them (only handles
elicitation_dialog,stop,idle_prompt) - The frontend has no SSE listeners for
hook:teammate_idleorhook: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.2 Event Types Tracked
From types.ts (lines 1359-1375):
type RunSummaryEventType =
| 'session_started' | 'session_stopped'
| 'respawn_cycle_started' | 'respawn_cycle_completed' | 'respawn_state_change'
| 'error' | 'warning'
| 'token_milestone' | 'auto_compact' | 'auto_clear'
| 'idle_detected' | 'working_detected'
| 'ralph_completion' | 'ai_check_result'
| 'hook_event' | 'state_stuck';
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.
7. Types (src/types.ts)
7.1 Hook Event Types (Line 749)
type HookEventType = 'idle_prompt' | 'permission_prompt' | 'elicitation_dialog'
| 'stop' | 'teammate_idle' | 'task_completed';
7.2 Hook Event Request (Lines 754-761)
interface HookEventRequest {
event: HookEventType;
sessionId: string;
data?: Record<string, unknown>;
}
7.3 Run Summary Types (Lines 1355-1470)
RunSummaryEventType-- 16 event typesRunSummaryEventSeverity--'info' | 'warning' | 'error' | 'success'RunSummaryEvent--{id, timestamp, type, severity, title, details?, metadata?}RunSummaryStats-- aggregated statistics (cycles, tokens, time active/idle, etc.)RunSummary-- complete summary{sessionId, sessionName, startedAt, lastUpdatedAt, events, stats}
7.4 Missing Notification Types
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
File: src/web/server.ts (line 3431), src/web/public/app.js
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', ...)oraddListener('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.
8.3 respawn:error Has No Frontend Handler
File: src/web/server.ts (line 4236), src/web/public/app.js
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.
8.7 Subagent Discovery/Completion Lacks Notification (By Design?)
File: src/web/public/app.js (lines 2887, 2912+)
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.
if (event.startsWith('session:') || event === 'respawn:') {
Should likely be:
if (event.startsWith('session:') || event.startsWith('respawn:')) {
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 |
|---|---|---|
hook:teammate_idle |
Broadcast, no handler | Warning: "Teammate idle, may need new task" |
hook:task_completed |
Broadcast, no handler | Info: "Team task completed" |
respawn:error |
Broadcast, no handler | Critical: "Respawn error: {message}" |
image:detected |
Auto-opens popup | Info (when tab unfocused): "Screenshot captured" |
subagent:discovered |
Auto-opens window | Info (if enabled): "New subagent spawned: {description}" |
subagent:completed |
Updates panel | Info (if enabled): "Subagent completed: {description}" |
respawn:cycleCompleted |
Broadcast, no handler | Info: "Respawn cycle #{n} completed" |
9.2 Team Events That Need SSE Broadcasting
Since TeamWatcher is not integrated, these events never reach clients:
- Team created/updated/removed
- Task status changes
- New inbox messages
- Teammate count changes
10. Architecture Diagram
Claude Code Hooks
|
curl POST /api/hook-event
|
+-------------------+
| server.ts |
| (Fastify) |
+-------------------+
| | |
Respawn | Broadcast | RunSummary
Signal | via SSE | Record
| | |
+---------+ +---+---+ +--------+
|RespawnCtrl| |SSE Bus| |Summary |
| emits | | | |Tracker |
| events | +---+---+ +--------+
+---------+ |
| |
server.ts Connected
wires to Browsers
SSE via |
broadcast() |
+----+-----+
| app.js |
| Frontend |
+----------+
|
+----------+-----------+
| |
NotificationManager Tab Alert System
(4 layers) (pendingHooks)
1. In-app drawer - action (critical)
2. Tab title flash - idle (warning)
3. Browser Notification API
4. Audio alerts
11. Summary of Key Files and Line References
| File | Lines | Purpose |
|---|---|---|
src/web/server.ts |
4646-4663 | broadcast() method |
src/web/server.ts |
547-564 | SSE client setup |
src/web/server.ts |
3396-3440 | Hook event endpoint |
src/web/server.ts |
178-211 | sanitizeHookData() |
src/web/server.ts |
3900-4103 | Session event wiring |
src/web/server.ts |
4143-4271 | Respawn event wiring |
src/web/server.ts |
458-477 | Subagent event wiring |
src/hooks-config.ts |
24-67 | Hook config generator |
src/hooks-config.ts |
107-129 | Config file writer |
src/types.ts |
749 | HookEventType |
src/types.ts |
754-761 | HookEventRequest |
src/types.ts |
1359-1375 | RunSummaryEventType |
src/team-watcher.ts |
27-338 | TeamWatcher (unintegrated) |
src/subagent-watcher.ts |
217-1346 | SubagentWatcher events |
src/respawn-controller.ts |
467-476 | Event documentation |
src/respawn-controller.ts |
2469-2551 | Hook signal methods |
src/respawn-controller.ts |
2750-2842 | Respawn blocking logic |
src/run-summary.ts |
56-447 | RunSummaryTracker class |
src/web/schemas.ts |
79-83 | HookEventSchema |
src/web/public/app.js |
860-1038 | NotificationManager class |
src/web/public/app.js |
1445-1477 | Pending hooks state machine |
src/web/public/app.js |
2813-2883 | Hook event SSE handlers |
src/web/public/app.js |
2443-2613 | Respawn event SSE handlers |
src/web/public/app.js |
2335-2417 | Session lifecycle SSE handlers |