aicodeman 0.2.8

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 (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +403 -0
  3. package/dist/ai-checker-base.d.ts +175 -0
  4. package/dist/ai-checker-base.d.ts.map +1 -0
  5. package/dist/ai-checker-base.js +424 -0
  6. package/dist/ai-checker-base.js.map +1 -0
  7. package/dist/ai-idle-checker.d.ts +53 -0
  8. package/dist/ai-idle-checker.d.ts.map +1 -0
  9. package/dist/ai-idle-checker.js +141 -0
  10. package/dist/ai-idle-checker.js.map +1 -0
  11. package/dist/ai-plan-checker.d.ts +52 -0
  12. package/dist/ai-plan-checker.d.ts.map +1 -0
  13. package/dist/ai-plan-checker.js +103 -0
  14. package/dist/ai-plan-checker.js.map +1 -0
  15. package/dist/bash-tool-parser.d.ts +191 -0
  16. package/dist/bash-tool-parser.d.ts.map +1 -0
  17. package/dist/bash-tool-parser.js +598 -0
  18. package/dist/bash-tool-parser.js.map +1 -0
  19. package/dist/cli.d.ts +12 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +460 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/config/buffer-limits.d.ts +59 -0
  24. package/dist/config/buffer-limits.d.ts.map +1 -0
  25. package/dist/config/buffer-limits.js +74 -0
  26. package/dist/config/buffer-limits.js.map +1 -0
  27. package/dist/config/map-limits.d.ts +40 -0
  28. package/dist/config/map-limits.d.ts.map +1 -0
  29. package/dist/config/map-limits.js +52 -0
  30. package/dist/config/map-limits.js.map +1 -0
  31. package/dist/file-stream-manager.d.ts +148 -0
  32. package/dist/file-stream-manager.d.ts.map +1 -0
  33. package/dist/file-stream-manager.js +351 -0
  34. package/dist/file-stream-manager.js.map +1 -0
  35. package/dist/hooks-config.d.ts +31 -0
  36. package/dist/hooks-config.d.ts.map +1 -0
  37. package/dist/hooks-config.js +115 -0
  38. package/dist/hooks-config.js.map +1 -0
  39. package/dist/image-watcher.d.ts +86 -0
  40. package/dist/image-watcher.d.ts.map +1 -0
  41. package/dist/image-watcher.js +275 -0
  42. package/dist/image-watcher.js.map +1 -0
  43. package/dist/index.d.ts +11 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +54 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/mux-factory.d.ts +13 -0
  48. package/dist/mux-factory.d.ts.map +1 -0
  49. package/dist/mux-factory.js +19 -0
  50. package/dist/mux-factory.js.map +1 -0
  51. package/dist/mux-interface.d.ts +145 -0
  52. package/dist/mux-interface.d.ts.map +1 -0
  53. package/dist/mux-interface.js +9 -0
  54. package/dist/mux-interface.js.map +1 -0
  55. package/dist/plan-orchestrator.d.ts +123 -0
  56. package/dist/plan-orchestrator.d.ts.map +1 -0
  57. package/dist/plan-orchestrator.js +500 -0
  58. package/dist/plan-orchestrator.js.map +1 -0
  59. package/dist/prompts/index.d.ts +9 -0
  60. package/dist/prompts/index.d.ts.map +1 -0
  61. package/dist/prompts/index.js +9 -0
  62. package/dist/prompts/index.js.map +1 -0
  63. package/dist/prompts/planner.d.ts +14 -0
  64. package/dist/prompts/planner.d.ts.map +1 -0
  65. package/dist/prompts/planner.js +83 -0
  66. package/dist/prompts/planner.js.map +1 -0
  67. package/dist/prompts/research-agent.d.ts +10 -0
  68. package/dist/prompts/research-agent.d.ts.map +1 -0
  69. package/dist/prompts/research-agent.js +143 -0
  70. package/dist/prompts/research-agent.js.map +1 -0
  71. package/dist/push-store.d.ts +41 -0
  72. package/dist/push-store.d.ts.map +1 -0
  73. package/dist/push-store.js +168 -0
  74. package/dist/push-store.js.map +1 -0
  75. package/dist/ralph-config.d.ts +67 -0
  76. package/dist/ralph-config.d.ts.map +1 -0
  77. package/dist/ralph-config.js +134 -0
  78. package/dist/ralph-config.js.map +1 -0
  79. package/dist/ralph-loop.d.ts +124 -0
  80. package/dist/ralph-loop.d.ts.map +1 -0
  81. package/dist/ralph-loop.js +418 -0
  82. package/dist/ralph-loop.js.map +1 -0
  83. package/dist/ralph-tracker.d.ts +1081 -0
  84. package/dist/ralph-tracker.d.ts.map +1 -0
  85. package/dist/ralph-tracker.js +3343 -0
  86. package/dist/ralph-tracker.js.map +1 -0
  87. package/dist/respawn-controller.d.ts +1182 -0
  88. package/dist/respawn-controller.d.ts.map +1 -0
  89. package/dist/respawn-controller.js +2754 -0
  90. package/dist/respawn-controller.js.map +1 -0
  91. package/dist/run-summary.d.ts +123 -0
  92. package/dist/run-summary.d.ts.map +1 -0
  93. package/dist/run-summary.js +325 -0
  94. package/dist/run-summary.js.map +1 -0
  95. package/dist/session-lifecycle-log.d.ts +36 -0
  96. package/dist/session-lifecycle-log.d.ts.map +1 -0
  97. package/dist/session-lifecycle-log.js +101 -0
  98. package/dist/session-lifecycle-log.js.map +1 -0
  99. package/dist/session-manager.d.ts +97 -0
  100. package/dist/session-manager.d.ts.map +1 -0
  101. package/dist/session-manager.js +224 -0
  102. package/dist/session-manager.js.map +1 -0
  103. package/dist/session.d.ts +686 -0
  104. package/dist/session.d.ts.map +1 -0
  105. package/dist/session.js +2025 -0
  106. package/dist/session.js.map +1 -0
  107. package/dist/state-store.d.ts +189 -0
  108. package/dist/state-store.d.ts.map +1 -0
  109. package/dist/state-store.js +730 -0
  110. package/dist/state-store.js.map +1 -0
  111. package/dist/subagent-watcher.d.ts +345 -0
  112. package/dist/subagent-watcher.d.ts.map +1 -0
  113. package/dist/subagent-watcher.js +1469 -0
  114. package/dist/subagent-watcher.js.map +1 -0
  115. package/dist/task-queue.d.ts +108 -0
  116. package/dist/task-queue.d.ts.map +1 -0
  117. package/dist/task-queue.js +235 -0
  118. package/dist/task-queue.js.map +1 -0
  119. package/dist/task-tracker.d.ts +306 -0
  120. package/dist/task-tracker.d.ts.map +1 -0
  121. package/dist/task-tracker.js +488 -0
  122. package/dist/task-tracker.js.map +1 -0
  123. package/dist/task.d.ts +73 -0
  124. package/dist/task.d.ts.map +1 -0
  125. package/dist/task.js +177 -0
  126. package/dist/task.js.map +1 -0
  127. package/dist/team-watcher.d.ts +53 -0
  128. package/dist/team-watcher.d.ts.map +1 -0
  129. package/dist/team-watcher.js +313 -0
  130. package/dist/team-watcher.js.map +1 -0
  131. package/dist/templates/case-template.md +461 -0
  132. package/dist/templates/claude-md.d.ts +26 -0
  133. package/dist/templates/claude-md.d.ts.map +1 -0
  134. package/dist/templates/claude-md.js +74 -0
  135. package/dist/templates/claude-md.js.map +1 -0
  136. package/dist/tmux-manager.d.ts +181 -0
  137. package/dist/tmux-manager.d.ts.map +1 -0
  138. package/dist/tmux-manager.js +1405 -0
  139. package/dist/tmux-manager.js.map +1 -0
  140. package/dist/transcript-watcher.d.ts +110 -0
  141. package/dist/transcript-watcher.d.ts.map +1 -0
  142. package/dist/transcript-watcher.js +338 -0
  143. package/dist/transcript-watcher.js.map +1 -0
  144. package/dist/tunnel-manager.d.ts +54 -0
  145. package/dist/tunnel-manager.d.ts.map +1 -0
  146. package/dist/tunnel-manager.js +251 -0
  147. package/dist/tunnel-manager.js.map +1 -0
  148. package/dist/types.d.ts +1139 -0
  149. package/dist/types.d.ts.map +1 -0
  150. package/dist/types.js +215 -0
  151. package/dist/types.js.map +1 -0
  152. package/dist/utils/buffer-accumulator.d.ts +111 -0
  153. package/dist/utils/buffer-accumulator.d.ts.map +1 -0
  154. package/dist/utils/buffer-accumulator.js +172 -0
  155. package/dist/utils/buffer-accumulator.js.map +1 -0
  156. package/dist/utils/claude-cli-resolver.d.ts +26 -0
  157. package/dist/utils/claude-cli-resolver.d.ts.map +1 -0
  158. package/dist/utils/claude-cli-resolver.js +78 -0
  159. package/dist/utils/claude-cli-resolver.js.map +1 -0
  160. package/dist/utils/cleanup-manager.d.ts +165 -0
  161. package/dist/utils/cleanup-manager.d.ts.map +1 -0
  162. package/dist/utils/cleanup-manager.js +274 -0
  163. package/dist/utils/cleanup-manager.js.map +1 -0
  164. package/dist/utils/index.d.ts +19 -0
  165. package/dist/utils/index.d.ts.map +1 -0
  166. package/dist/utils/index.js +19 -0
  167. package/dist/utils/index.js.map +1 -0
  168. package/dist/utils/lru-map.d.ts +140 -0
  169. package/dist/utils/lru-map.d.ts.map +1 -0
  170. package/dist/utils/lru-map.js +234 -0
  171. package/dist/utils/lru-map.js.map +1 -0
  172. package/dist/utils/nice-wrapper.d.ts +13 -0
  173. package/dist/utils/nice-wrapper.d.ts.map +1 -0
  174. package/dist/utils/nice-wrapper.js +17 -0
  175. package/dist/utils/nice-wrapper.js.map +1 -0
  176. package/dist/utils/opencode-cli-resolver.d.ts +21 -0
  177. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -0
  178. package/dist/utils/opencode-cli-resolver.js +67 -0
  179. package/dist/utils/opencode-cli-resolver.js.map +1 -0
  180. package/dist/utils/regex-patterns.d.ts +64 -0
  181. package/dist/utils/regex-patterns.d.ts.map +1 -0
  182. package/dist/utils/regex-patterns.js +74 -0
  183. package/dist/utils/regex-patterns.js.map +1 -0
  184. package/dist/utils/stale-expiration-map.d.ts +159 -0
  185. package/dist/utils/stale-expiration-map.d.ts.map +1 -0
  186. package/dist/utils/stale-expiration-map.js +277 -0
  187. package/dist/utils/stale-expiration-map.js.map +1 -0
  188. package/dist/utils/string-similarity.d.ts +108 -0
  189. package/dist/utils/string-similarity.d.ts.map +1 -0
  190. package/dist/utils/string-similarity.js +189 -0
  191. package/dist/utils/string-similarity.js.map +1 -0
  192. package/dist/utils/token-validation.d.ts +39 -0
  193. package/dist/utils/token-validation.d.ts.map +1 -0
  194. package/dist/utils/token-validation.js +59 -0
  195. package/dist/utils/token-validation.js.map +1 -0
  196. package/dist/utils/type-safety.d.ts +33 -0
  197. package/dist/utils/type-safety.d.ts.map +1 -0
  198. package/dist/utils/type-safety.js +35 -0
  199. package/dist/utils/type-safety.js.map +1 -0
  200. package/dist/web/public/app.js +491 -0
  201. package/dist/web/public/app.js.br +0 -0
  202. package/dist/web/public/app.js.gz +0 -0
  203. package/dist/web/public/index.html +1675 -0
  204. package/dist/web/public/index.html.br +0 -0
  205. package/dist/web/public/index.html.gz +0 -0
  206. package/dist/web/public/manifest.json +8 -0
  207. package/dist/web/public/mobile.css +1 -0
  208. package/dist/web/public/mobile.css.br +0 -0
  209. package/dist/web/public/mobile.css.gz +0 -0
  210. package/dist/web/public/ralph-wizard.js +1037 -0
  211. package/dist/web/public/ralph-wizard.js.br +0 -0
  212. package/dist/web/public/ralph-wizard.js.gz +0 -0
  213. package/dist/web/public/styles.css +1 -0
  214. package/dist/web/public/styles.css.br +0 -0
  215. package/dist/web/public/styles.css.gz +0 -0
  216. package/dist/web/public/sw.js +67 -0
  217. package/dist/web/public/sw.js.br +0 -0
  218. package/dist/web/public/sw.js.gz +0 -0
  219. package/dist/web/public/upload.html +155 -0
  220. package/dist/web/public/upload.html.br +0 -0
  221. package/dist/web/public/upload.html.gz +0 -0
  222. package/dist/web/public/vendor/xterm-addon-fit.min.js +1 -0
  223. package/dist/web/public/vendor/xterm-addon-fit.min.js.br +0 -0
  224. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  225. package/dist/web/public/vendor/xterm-addon-unicode11.min.js +1 -0
  226. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.br +0 -0
  227. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  228. package/dist/web/public/vendor/xterm-addon-webgl.min.js +2 -0
  229. package/dist/web/public/vendor/xterm-addon-webgl.min.js.br +0 -0
  230. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  231. package/dist/web/public/vendor/xterm.css +209 -0
  232. package/dist/web/public/vendor/xterm.css.br +0 -0
  233. package/dist/web/public/vendor/xterm.css.gz +0 -0
  234. package/dist/web/public/vendor/xterm.min.js +9 -0
  235. package/dist/web/public/vendor/xterm.min.js.br +0 -0
  236. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  237. package/dist/web/schemas.d.ts +479 -0
  238. package/dist/web/schemas.d.ts.map +1 -0
  239. package/dist/web/schemas.js +448 -0
  240. package/dist/web/schemas.js.map +1 -0
  241. package/dist/web/server.d.ts +207 -0
  242. package/dist/web/server.d.ts.map +1 -0
  243. package/dist/web/server.js +5784 -0
  244. package/dist/web/server.js.map +1 -0
  245. package/package.json +110 -0
  246. package/scripts/postinstall.js +390 -0
@@ -0,0 +1,1469 @@
1
+ /**
2
+ * @fileoverview Subagent Watcher - Real-time monitoring of Claude Code background agents
3
+ *
4
+ * Watches ~/.claude/projects/{project}/{session}/subagents/agent-{id}.jsonl files
5
+ * and emits structured events for tool calls, progress, and messages.
6
+ */
7
+ import { EventEmitter } from 'node:events';
8
+ import { watch, existsSync } from 'node:fs';
9
+ import { createReadStream } from 'node:fs';
10
+ import { createInterface } from 'node:readline';
11
+ import { homedir } from 'node:os';
12
+ import { join, basename } from 'node:path';
13
+ import { execFile } from 'node:child_process';
14
+ import { readFile, readdir, stat as statAsync } from 'node:fs/promises';
15
+ import { PENDING_TOOL_CALL_TTL_MS, MAX_PENDING_TOOL_CALLS } from './config/map-limits.js';
16
+ // ========== Constants ==========
17
+ const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude/projects');
18
+ const IDLE_TIMEOUT_MS = 30000; // Consider agent idle after 30s of no activity
19
+ const POLL_INTERVAL_MS = 1000; // Base poll interval (lightweight checks)
20
+ const FULL_SCAN_EVERY_N_POLLS = 5; // Full directory traversal every 5th poll (5s)
21
+ const LIVENESS_CHECK_MS = 10000; // Check if subagent processes are still alive every 10s
22
+ const FILE_ALIVE_THRESHOLD_MS = 30000; // File mtime within 30s = agent alive (primary check)
23
+ const STALE_COMPLETED_MAX_AGE_MS = 60 * 60 * 1000; // Remove completed agents older than 1 hour
24
+ const STALE_IDLE_MAX_AGE_MS = 4 * 60 * 60 * 1000; // Remove idle agents older than 4 hours
25
+ const STARTUP_MAX_FILE_AGE_MS = 4 * 60 * 60 * 1000; // Only load files modified in last 4 hours on startup
26
+ const MAX_TRACKED_AGENTS = 500; // Maximum agents to track (LRU eviction when exceeded)
27
+ // Internal Claude Code agent patterns to filter out (not real user-initiated subagents)
28
+ const INTERNAL_AGENT_PATTERNS = [
29
+ /^\[?SUGGESTION MODE/i, // Claude Code's internal suggestion mode
30
+ /^Suggest what user might/i, // Suggestion mode prompt variant
31
+ /aprompt/i, // Internal prompt agent (anywhere in string)
32
+ /^a\s?prompt/i, // Variants of internal prompt agent
33
+ /^prompt$/i, // Just "prompt"
34
+ ];
35
+ // Minimum description length - very short descriptions are likely internal or malformed
36
+ const MIN_DESCRIPTION_LENGTH = 5;
37
+ // Display/preview length constants
38
+ const TEXT_PREVIEW_LENGTH = 200; // Length for text previews in tool results
39
+ const USER_TEXT_PREVIEW_LENGTH = 80; // Length for user message previews
40
+ const SMART_TITLE_MAX_LENGTH = 45; // Max length for smart title extraction
41
+ const MESSAGE_TEXT_LIMIT = 500; // Max length for message text content
42
+ const COMMAND_DISPLAY_LENGTH = 60; // Max length for command display
43
+ const INPUT_TRUNCATE_LENGTH = 100; // Max length for input value truncation
44
+ const FILE_CONTENT_DEBOUNCE_MS = 100; // Debounce delay for file content updates
45
+ // ========== SubagentWatcher Class ==========
46
+ export class SubagentWatcher extends EventEmitter {
47
+ filePositions = new Map();
48
+ fileWatchers = new Map();
49
+ dirWatchers = new Map();
50
+ agentInfo = new Map();
51
+ idleTimers = new Map();
52
+ pollInterval = null;
53
+ livenessInterval = null;
54
+ _isRunning = false;
55
+ knownSubagentDirs = new Set();
56
+ // Map of agentId -> Map of toolUseId -> { toolName, timestamp } (for linking tool_result to tool_call)
57
+ // Includes timestamp for TTL-based cleanup of orphaned entries
58
+ pendingToolCalls = new Map();
59
+ // Guard to prevent concurrent liveness checks (prevents duplicate completed events)
60
+ _isCheckingLiveness = false;
61
+ // Counter for throttling full directory scans (only scan every FULL_SCAN_EVERY_N_POLLS)
62
+ _pollCount = 0;
63
+ // Short-lived cache for parsed parent transcript descriptions (TTL: 5s)
64
+ // Key: "{projectHash}/{sessionId}", Value: { descriptions: Map<agentId, description>, timestamp }
65
+ parentDescriptionCache = new Map();
66
+ // Store error handlers for FSWatchers to enable proper cleanup (prevent memory leaks)
67
+ dirWatcherErrorHandlers = new Map();
68
+ fileWatcherErrorHandlers = new Map();
69
+ constructor() {
70
+ super();
71
+ this.setMaxListeners(50);
72
+ }
73
+ /**
74
+ * Check if a description matches internal Claude Code agent patterns.
75
+ * These are not real user-initiated subagents and should be filtered out.
76
+ * Also filters out very short descriptions that are likely internal or malformed.
77
+ */
78
+ isInternalAgent(description) {
79
+ if (!description)
80
+ return false;
81
+ // Filter out very short descriptions (likely internal or malformed)
82
+ if (description.length < MIN_DESCRIPTION_LENGTH)
83
+ return true;
84
+ return INTERNAL_AGENT_PATTERNS.some((pattern) => pattern.test(description));
85
+ }
86
+ /**
87
+ * Extract short model identifier from full model name
88
+ */
89
+ extractModelShort(model) {
90
+ const lower = model.toLowerCase();
91
+ if (lower.includes('haiku'))
92
+ return 'haiku';
93
+ if (lower.includes('sonnet'))
94
+ return 'sonnet';
95
+ if (lower.includes('opus'))
96
+ return 'opus';
97
+ return undefined;
98
+ }
99
+ // ========== Public API ==========
100
+ /**
101
+ * Start watching for subagent activity
102
+ */
103
+ start() {
104
+ if (this._isRunning)
105
+ return;
106
+ this._isRunning = true;
107
+ // Initial scan (always runs immediately)
108
+ this._pollCount = 0;
109
+ this.scanForSubagents().catch((err) => this.emit('subagent:error', err));
110
+ // Periodic scan for new subagent directories
111
+ // Full directory traversal only every FULL_SCAN_EVERY_N_POLLS polls (~5s)
112
+ // FSWatchers handle known directories between full scans
113
+ this.pollInterval = setInterval(() => {
114
+ this._pollCount++;
115
+ if (this._pollCount % FULL_SCAN_EVERY_N_POLLS === 0) {
116
+ this.scanForSubagents().catch((err) => this.emit('subagent:error', err));
117
+ }
118
+ }, POLL_INTERVAL_MS);
119
+ // Periodic liveness check for active subagents
120
+ this.startLivenessChecker();
121
+ }
122
+ /**
123
+ * Start periodic liveness checker
124
+ * Detects when subagent processes have exited but status is still active/idle.
125
+ *
126
+ * Uses a 3-tier check to minimize cost:
127
+ * 1. File mtime (stat ~0.3ms/agent) — if transcript modified recently, agent is alive
128
+ * 2. Cached PID (/proc/{pid}/stat ~0.1ms) — if stored PID still exists, alive
129
+ * 3. Full pgrep scan (expensive, ~500ms) — only for agents that fail tiers 1+2
130
+ */
131
+ startLivenessChecker() {
132
+ if (this.livenessInterval)
133
+ return;
134
+ this.livenessInterval = setInterval(async () => {
135
+ // Guard: prevent concurrent liveness checks (avoids duplicate completed events)
136
+ if (this._isCheckingLiveness)
137
+ return;
138
+ this._isCheckingLiveness = true;
139
+ try {
140
+ // Collect agents that need the expensive pgrep scan
141
+ const needsFullScan = [];
142
+ for (const [_agentId, info] of this.agentInfo) {
143
+ if (info.status !== 'active' && info.status !== 'idle')
144
+ continue;
145
+ // Tier 1: File mtime check (~0.3ms per agent)
146
+ if (await this.checkSubagentFileAlive(info))
147
+ continue;
148
+ // Tier 2: Cached PID check (~0.1ms per agent)
149
+ if (info.pid && (await this.checkPidAlive(info.pid)))
150
+ continue;
151
+ // Tiers 1+2 failed — need expensive scan for this agent
152
+ needsFullScan.push(info);
153
+ }
154
+ // Tier 3: Full pgrep scan — only if any agents failed cheap checks
155
+ if (needsFullScan.length > 0) {
156
+ const pidMap = await this.getClaudePids();
157
+ for (const info of needsFullScan) {
158
+ // Re-check status in case another check completed this agent
159
+ if (info.status !== 'active' && info.status !== 'idle')
160
+ continue;
161
+ const alive = this.checkSubagentAliveFromPidMap(info, pidMap);
162
+ if (!alive) {
163
+ info.pid = undefined;
164
+ info.status = 'completed';
165
+ this.pendingToolCalls.delete(info.agentId);
166
+ this.emit('subagent:completed', info);
167
+ }
168
+ }
169
+ }
170
+ // Periodically clean up stale completed agents (older than 24 hours)
171
+ this.cleanupStaleAgents();
172
+ }
173
+ finally {
174
+ this._isCheckingLiveness = false;
175
+ }
176
+ }, LIVENESS_CHECK_MS);
177
+ }
178
+ /**
179
+ * Check if a PID is still alive via /proc/{pid}/stat (single file read, ~0.1ms).
180
+ */
181
+ async checkPidAlive(pid) {
182
+ try {
183
+ await statAsync(`/proc/${pid}`);
184
+ return true;
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ }
190
+ /**
191
+ * Run pgrep once and read /proc info for all Claude PIDs in parallel.
192
+ * Returns a Map of pid -> { environ, cmdline } for subagent processes only.
193
+ * Excludes main Codeman-managed Claude processes (CODEMAN_MUX=1).
194
+ * Also updates cached PIDs on tracked agents when a match is found.
195
+ */
196
+ async getClaudePids() {
197
+ const result = new Map();
198
+ try {
199
+ const pgrepOutput = await new Promise((resolve, reject) => {
200
+ execFile('pgrep', ['-f', 'claude'], { encoding: 'utf8' }, (err, stdout) => {
201
+ if (err)
202
+ return reject(err);
203
+ resolve(stdout);
204
+ });
205
+ });
206
+ const pids = pgrepOutput
207
+ .trim()
208
+ .split('\n')
209
+ .filter(Boolean)
210
+ .map((s) => parseInt(s, 10))
211
+ .filter((n) => !Number.isNaN(n));
212
+ // Read /proc for all PIDs in parallel
213
+ await Promise.all(pids.map(async (pid) => {
214
+ let environ = '';
215
+ let cmdline = '';
216
+ try {
217
+ environ = await readFile(`/proc/${pid}/environ`, 'utf8');
218
+ }
219
+ catch {
220
+ /* skip */
221
+ }
222
+ try {
223
+ cmdline = await readFile(`/proc/${pid}/cmdline`, 'utf8');
224
+ }
225
+ catch {
226
+ /* skip */
227
+ }
228
+ // Skip main Codeman-managed Claude processes — only track subagents
229
+ if (environ.includes('CODEMAN_MUX=1'))
230
+ return;
231
+ if (environ || cmdline) {
232
+ result.set(pid, { environ, cmdline });
233
+ }
234
+ }));
235
+ // Update cached PIDs on tracked agents
236
+ for (const [pid, procInfo] of result) {
237
+ for (const [_agentId, info] of this.agentInfo) {
238
+ if (info.status !== 'active' && info.status !== 'idle')
239
+ continue;
240
+ if (procInfo.environ.includes(info.sessionId) || procInfo.cmdline.includes(info.sessionId)) {
241
+ info.pid = pid;
242
+ break; // Each PID belongs to at most one agent
243
+ }
244
+ }
245
+ }
246
+ }
247
+ catch {
248
+ // pgrep returns non-zero if no matches
249
+ }
250
+ return result;
251
+ }
252
+ /**
253
+ * Check if a subagent is alive using the pre-fetched pid map (no process spawning).
254
+ */
255
+ checkSubagentAliveFromPidMap(info, pidMap) {
256
+ for (const [_pid, procInfo] of pidMap) {
257
+ if (procInfo.environ.includes(info.sessionId) || procInfo.cmdline.includes(info.sessionId)) {
258
+ return true;
259
+ }
260
+ }
261
+ return false;
262
+ }
263
+ /**
264
+ * Check if a subagent's transcript file was recently modified.
265
+ * Primary liveness signal — transcript files are written to continuously while agent is active.
266
+ */
267
+ async checkSubagentFileAlive(info) {
268
+ try {
269
+ const fileStat = await statAsync(info.filePath);
270
+ const mtime = fileStat.mtime.getTime();
271
+ const now = Date.now();
272
+ if (now - mtime < FILE_ALIVE_THRESHOLD_MS) {
273
+ return true;
274
+ }
275
+ }
276
+ catch {
277
+ // File doesn't exist or can't be read
278
+ }
279
+ return false;
280
+ }
281
+ /**
282
+ * Check if the watcher is currently running
283
+ */
284
+ isRunning() {
285
+ return this._isRunning;
286
+ }
287
+ /**
288
+ * Stop watching and clean up all state
289
+ */
290
+ stop() {
291
+ this._isRunning = false;
292
+ if (this.pollInterval) {
293
+ clearInterval(this.pollInterval);
294
+ this.pollInterval = null;
295
+ }
296
+ if (this.livenessInterval) {
297
+ clearInterval(this.livenessInterval);
298
+ this.livenessInterval = null;
299
+ }
300
+ // Remove error handlers before closing watchers to prevent memory leak
301
+ for (const [filePath, handler] of this.fileWatcherErrorHandlers) {
302
+ const watcher = this.fileWatchers.get(filePath);
303
+ if (watcher)
304
+ watcher.off('error', handler);
305
+ }
306
+ this.fileWatcherErrorHandlers.clear();
307
+ for (const watcher of this.fileWatchers.values()) {
308
+ watcher.close();
309
+ }
310
+ this.fileWatchers.clear();
311
+ // Remove error handlers before closing watchers to prevent memory leak
312
+ for (const [dir, handler] of this.dirWatcherErrorHandlers) {
313
+ const watcher = this.dirWatchers.get(dir);
314
+ if (watcher)
315
+ watcher.off('error', handler);
316
+ }
317
+ this.dirWatcherErrorHandlers.clear();
318
+ for (const watcher of this.dirWatchers.values()) {
319
+ watcher.close();
320
+ }
321
+ this.dirWatchers.clear();
322
+ for (const timer of this.idleTimers.values()) {
323
+ clearTimeout(timer);
324
+ }
325
+ this.idleTimers.clear();
326
+ // Clear all state for clean restart
327
+ this.filePositions.clear();
328
+ this.agentInfo.clear();
329
+ this.knownSubagentDirs.clear();
330
+ this.pendingToolCalls.clear();
331
+ this.parentDescriptionCache.clear();
332
+ this._pollCount = 0;
333
+ }
334
+ /**
335
+ * Clean up stale agents to prevent unbounded memory growth.
336
+ * - Completed agents: removed after STALE_COMPLETED_MAX_AGE_MS (1 hour)
337
+ * - Idle agents: removed after STALE_IDLE_MAX_AGE_MS (4 hours)
338
+ * Also enforces MAX_TRACKED_AGENTS limit with LRU eviction.
339
+ * Also cleans up orphaned pending tool calls older than PENDING_TOOL_CALL_TTL_MS.
340
+ */
341
+ cleanupStaleAgents() {
342
+ const now = Date.now();
343
+ const agentsToDelete = new Set();
344
+ for (const [agentId, info] of this.agentInfo) {
345
+ const age = now - info.lastActivityAt;
346
+ // Clean up based on status and age
347
+ if (info.status === 'completed' && age > STALE_COMPLETED_MAX_AGE_MS) {
348
+ agentsToDelete.add(agentId);
349
+ }
350
+ else if (info.status === 'idle' && age > STALE_IDLE_MAX_AGE_MS) {
351
+ agentsToDelete.add(agentId);
352
+ }
353
+ }
354
+ // Enforce max tracked agents limit (LRU eviction)
355
+ const currentCount = this.agentInfo.size - agentsToDelete.size;
356
+ if (currentCount > MAX_TRACKED_AGENTS) {
357
+ // Sort by lastActivityAt (oldest first) and evict oldest completed/idle agents
358
+ const sortedAgents = Array.from(this.agentInfo.entries())
359
+ .filter(([id]) => !agentsToDelete.has(id))
360
+ .filter(([, info]) => info.status !== 'active') // Keep active agents
361
+ .sort((a, b) => a[1].lastActivityAt - b[1].lastActivityAt);
362
+ const toEvict = currentCount - MAX_TRACKED_AGENTS;
363
+ for (let i = 0; i < toEvict && i < sortedAgents.length; i++) {
364
+ agentsToDelete.add(sortedAgents[i][0]);
365
+ }
366
+ }
367
+ // Perform cleanup
368
+ for (const agentId of agentsToDelete) {
369
+ this.removeAgent(agentId);
370
+ }
371
+ // Clean up orphaned pending tool calls (older than TTL)
372
+ // These can accumulate if tool_result is never received (e.g., agent crashed)
373
+ for (const [agentId, agentCalls] of this.pendingToolCalls) {
374
+ const idsToDelete = [];
375
+ for (const [toolUseId, callInfo] of agentCalls) {
376
+ if (now - callInfo.timestamp > PENDING_TOOL_CALL_TTL_MS) {
377
+ idsToDelete.push(toolUseId);
378
+ }
379
+ }
380
+ for (const id of idsToDelete) {
381
+ agentCalls.delete(id);
382
+ }
383
+ // If agent has no more pending calls, remove the agent entry from the map
384
+ if (agentCalls.size === 0) {
385
+ this.pendingToolCalls.delete(agentId);
386
+ }
387
+ }
388
+ }
389
+ /**
390
+ * Remove an agent and all its associated resources.
391
+ */
392
+ removeAgent(agentId) {
393
+ const info = this.agentInfo.get(agentId);
394
+ if (info) {
395
+ this.agentInfo.delete(agentId);
396
+ this.pendingToolCalls.delete(agentId);
397
+ this.filePositions.delete(info.filePath);
398
+ const watcher = this.fileWatchers.get(info.filePath);
399
+ if (watcher) {
400
+ watcher.close();
401
+ this.fileWatchers.delete(info.filePath);
402
+ this.fileWatcherErrorHandlers.delete(info.filePath);
403
+ }
404
+ const timer = this.idleTimers.get(agentId);
405
+ if (timer) {
406
+ clearTimeout(timer);
407
+ this.idleTimers.delete(agentId);
408
+ }
409
+ }
410
+ }
411
+ /**
412
+ * Manually trigger cleanup of stale agents.
413
+ * Returns number of agents removed.
414
+ */
415
+ cleanupNow() {
416
+ const beforeCount = this.agentInfo.size;
417
+ this.cleanupStaleAgents();
418
+ return beforeCount - this.agentInfo.size;
419
+ }
420
+ /**
421
+ * Clear all tracked agents (for manual reset).
422
+ * Returns number of agents cleared.
423
+ */
424
+ clearAll() {
425
+ const count = this.agentInfo.size;
426
+ const agentIds = Array.from(this.agentInfo.keys());
427
+ for (const agentId of agentIds) {
428
+ this.removeAgent(agentId);
429
+ }
430
+ return count;
431
+ }
432
+ /**
433
+ * Get all known subagents
434
+ */
435
+ getSubagents() {
436
+ return Array.from(this.agentInfo.values());
437
+ }
438
+ /**
439
+ * Get subagents for a specific Codeman session
440
+ * Maps Codeman working directory to Claude's project hash
441
+ */
442
+ getSubagentsForSession(workingDir) {
443
+ const projectHash = this.getProjectHash(workingDir);
444
+ return Array.from(this.agentInfo.values()).filter((info) => info.projectHash === projectHash);
445
+ }
446
+ /**
447
+ * Get a specific subagent's info
448
+ */
449
+ getSubagent(agentId) {
450
+ return this.agentInfo.get(agentId);
451
+ }
452
+ /**
453
+ * Update a subagent's description.
454
+ * Used to set the short description from TaskTracker when available.
455
+ */
456
+ updateDescription(agentId, description) {
457
+ const info = this.agentInfo.get(agentId);
458
+ if (info) {
459
+ info.description = description;
460
+ this.emit('subagent:updated', info);
461
+ return true;
462
+ }
463
+ return false;
464
+ }
465
+ /**
466
+ * Find sessions whose working directory matches a project hash.
467
+ * Returns the project hash for a given working directory.
468
+ */
469
+ getProjectHashForDir(workingDir) {
470
+ return this.getProjectHash(workingDir);
471
+ }
472
+ /**
473
+ * Get internal statistics for memory monitoring.
474
+ * Returns counts of internal Maps and resources.
475
+ */
476
+ getStats() {
477
+ // Count pending tool calls across all agents
478
+ let pendingToolCallsCount = 0;
479
+ for (const agentCalls of this.pendingToolCalls.values()) {
480
+ pendingToolCallsCount += agentCalls.size;
481
+ }
482
+ return {
483
+ agentCount: this.agentInfo.size,
484
+ fileWatcherCount: this.fileWatchers.size,
485
+ dirWatcherCount: this.dirWatchers.size,
486
+ idleTimerCount: this.idleTimers.size,
487
+ pendingToolCallsCount,
488
+ knownDirsCount: this.knownSubagentDirs.size,
489
+ filePositionsCount: this.filePositions.size,
490
+ };
491
+ }
492
+ /**
493
+ * Get recent subagents (modified within specified minutes)
494
+ */
495
+ getRecentSubagents(minutes = 60) {
496
+ const cutoff = Date.now() - minutes * 60 * 1000;
497
+ return Array.from(this.agentInfo.values())
498
+ .filter((info) => info.lastActivityAt > cutoff)
499
+ .sort((a, b) => b.lastActivityAt - a.lastActivityAt);
500
+ }
501
+ /**
502
+ * Kill a subagent by its agent ID
503
+ * Uses cached PID first, falls back to findSubagentProcess if needed.
504
+ */
505
+ async killSubagent(agentId) {
506
+ const info = this.agentInfo.get(agentId);
507
+ if (!info)
508
+ return false;
509
+ // Already completed, nothing to kill
510
+ if (info.status === 'completed')
511
+ return false;
512
+ try {
513
+ // Always use findSubagentProcess for kill — it verifies environ/cmdline,
514
+ // preventing PID reuse attacks (cached PID may have been recycled by OS)
515
+ const pid = await this.findSubagentProcess(info.sessionId);
516
+ if (pid) {
517
+ process.kill(pid, 'SIGTERM');
518
+ info.pid = undefined;
519
+ info.status = 'completed';
520
+ this.pendingToolCalls.delete(info.agentId);
521
+ this.emit('subagent:completed', info);
522
+ return true;
523
+ }
524
+ }
525
+ catch {
526
+ // Process may have already exited
527
+ }
528
+ // Mark as completed even if we couldn't find the process
529
+ info.pid = undefined;
530
+ info.status = 'completed';
531
+ this.pendingToolCalls.delete(info.agentId);
532
+ this.emit('subagent:completed', info);
533
+ return true;
534
+ }
535
+ /**
536
+ * Kill all subagents for a specific Codeman session.
537
+ * IMPORTANT: Must scope to sessionId to avoid cross-session kills.
538
+ * All sessions in the same workingDir share a projectHash, so filtering
539
+ * by workingDir alone would kill subagents belonging to OTHER sessions.
540
+ */
541
+ async killSubagentsForSession(workingDir, sessionId) {
542
+ const subagents = this.getSubagentsForSession(workingDir);
543
+ for (const agent of subagents) {
544
+ if (agent.status === 'active' || agent.status === 'idle') {
545
+ // Only kill subagents belonging to this specific session
546
+ if (sessionId && agent.sessionId !== sessionId)
547
+ continue;
548
+ await this.killSubagent(agent.agentId);
549
+ }
550
+ }
551
+ }
552
+ /**
553
+ * Find the process ID of a Claude subagent by its session ID.
554
+ * Searches /proc for claude processes with matching session ID in environment.
555
+ * Skips main Codeman-managed Claude processes (identified by CODEMAN_MUX=1).
556
+ * Caches the discovered PID on the matching agent info for future fast checks.
557
+ */
558
+ async findSubagentProcess(sessionId) {
559
+ try {
560
+ // Find all claude processes (async to avoid blocking event loop)
561
+ const pgrepOutput = await new Promise((resolve, reject) => {
562
+ execFile('pgrep', ['-f', 'claude'], { encoding: 'utf8' }, (err, stdout) => {
563
+ if (err)
564
+ return reject(err);
565
+ resolve(stdout);
566
+ });
567
+ });
568
+ const pids = pgrepOutput.trim().split('\n').filter(Boolean);
569
+ for (const pidStr of pids) {
570
+ const pid = parseInt(pidStr, 10);
571
+ if (Number.isNaN(pid))
572
+ continue;
573
+ let environ = '';
574
+ try {
575
+ environ = await readFile(`/proc/${pid}/environ`, 'utf8');
576
+ }
577
+ catch {
578
+ // Can't read this process's environ - skip
579
+ }
580
+ // Defense-in-depth: never kill a main Codeman-managed Claude process.
581
+ // Main processes have CODEMAN_MUX=1 in their environment; subagents don't.
582
+ if (environ.includes('CODEMAN_MUX=1'))
583
+ continue;
584
+ if (environ.includes(sessionId)) {
585
+ // Cache PID on the matching agent
586
+ this.cacheAgentPid(sessionId, pid);
587
+ return pid;
588
+ }
589
+ try {
590
+ const cmdline = await readFile(`/proc/${pid}/cmdline`, 'utf8');
591
+ if (cmdline.includes(sessionId)) {
592
+ this.cacheAgentPid(sessionId, pid);
593
+ return pid;
594
+ }
595
+ }
596
+ catch {
597
+ // Can't read this process's cmdline - skip
598
+ }
599
+ }
600
+ }
601
+ catch {
602
+ // pgrep returns non-zero if no matches
603
+ }
604
+ return null;
605
+ }
606
+ /**
607
+ * Store a discovered PID on the agent info with matching sessionId.
608
+ */
609
+ cacheAgentPid(sessionId, pid) {
610
+ for (const [_agentId, info] of this.agentInfo) {
611
+ if (info.sessionId === sessionId && (info.status === 'active' || info.status === 'idle')) {
612
+ info.pid = pid;
613
+ return;
614
+ }
615
+ }
616
+ }
617
+ /**
618
+ * Get transcript for a subagent (optionally limited to last N entries)
619
+ */
620
+ async getTranscript(agentId, limit) {
621
+ const info = this.agentInfo.get(agentId);
622
+ if (!info)
623
+ return [];
624
+ const entries = [];
625
+ try {
626
+ const content = await readFile(info.filePath, 'utf8');
627
+ const lines = content.split('\n').filter((l) => l.trim());
628
+ for (const line of lines) {
629
+ try {
630
+ const entry = JSON.parse(line);
631
+ entries.push(entry);
632
+ }
633
+ catch {
634
+ // Skip malformed lines
635
+ }
636
+ }
637
+ }
638
+ catch {
639
+ // File read error
640
+ }
641
+ if (limit && limit > 0) {
642
+ return entries.slice(-limit);
643
+ }
644
+ return entries;
645
+ }
646
+ /**
647
+ * Format transcript entries for display
648
+ */
649
+ formatTranscript(entries) {
650
+ const lines = [];
651
+ for (const entry of entries) {
652
+ if (entry.type === 'progress' && entry.data) {
653
+ lines.push(this.formatProgress(entry));
654
+ }
655
+ else if (entry.type === 'assistant' && entry.message?.content) {
656
+ // Handle both string and array content formats
657
+ if (typeof entry.message.content === 'string') {
658
+ const text = entry.message.content.trim();
659
+ if (text.length > 0) {
660
+ const preview = text.length > TEXT_PREVIEW_LENGTH ? text.substring(0, TEXT_PREVIEW_LENGTH) + '...' : text;
661
+ lines.push(`${this.formatTime(entry.timestamp)} 💬 ${preview.replace(/\n/g, ' ')}`);
662
+ }
663
+ }
664
+ else {
665
+ for (const content of entry.message.content) {
666
+ if (content.type === 'tool_use' && content.name) {
667
+ lines.push(this.formatToolCall(entry.timestamp, content.name, content.input || {}));
668
+ }
669
+ else if (content.type === 'text' && content.text) {
670
+ const text = content.text.trim();
671
+ if (text.length > 0) {
672
+ const preview = text.length > TEXT_PREVIEW_LENGTH ? text.substring(0, TEXT_PREVIEW_LENGTH) + '...' : text;
673
+ lines.push(`${this.formatTime(entry.timestamp)} 💬 ${preview.replace(/\n/g, ' ')}`);
674
+ }
675
+ }
676
+ }
677
+ }
678
+ }
679
+ else if (entry.type === 'user' && entry.message?.content) {
680
+ // Handle both string and array content formats
681
+ if (typeof entry.message.content === 'string') {
682
+ const text = entry.message.content.trim();
683
+ if (text.length < 100 && !text.includes('{')) {
684
+ lines.push(`${this.formatTime(entry.timestamp)} 📥 User: ${text.substring(0, USER_TEXT_PREVIEW_LENGTH)}`);
685
+ }
686
+ }
687
+ else {
688
+ const firstContent = entry.message.content[0];
689
+ if (firstContent?.type === 'text' && firstContent.text) {
690
+ const text = firstContent.text.trim();
691
+ if (text.length < 100 && !text.includes('{')) {
692
+ lines.push(`${this.formatTime(entry.timestamp)} 📥 User: ${text.substring(0, USER_TEXT_PREVIEW_LENGTH)}`);
693
+ }
694
+ }
695
+ }
696
+ }
697
+ }
698
+ return lines;
699
+ }
700
+ // ========== Private Methods ==========
701
+ /**
702
+ * Convert working directory to Claude's project hash format
703
+ */
704
+ getProjectHash(workingDir) {
705
+ return workingDir.replace(/\//g, '-');
706
+ }
707
+ /**
708
+ * Extract a smart, concise title from a task prompt
709
+ * Aims for ~40-50 chars that convey what the agent is doing
710
+ */
711
+ extractSmartTitle(text) {
712
+ const MAX_LEN = SMART_TITLE_MAX_LENGTH;
713
+ // Get first line/sentence
714
+ const title = text.split('\n')[0].trim();
715
+ // If already short enough, use it
716
+ if (title.length <= MAX_LEN) {
717
+ return title.replace(/[.!?,\s]+$/, '');
718
+ }
719
+ // Remove common filler phrases to condense
720
+ const fillers = [
721
+ /^(please |i need you to |i want you to |can you |could you )/i,
722
+ / (the|a|an) /gi,
723
+ / (in|at|on|to|for|of|with|from|by) the /gi,
724
+ / (including|related to|regarding|about) /gi,
725
+ /[""]/g,
726
+ / +/g, // multiple spaces to single
727
+ ];
728
+ let condensed = title;
729
+ for (const filler of fillers) {
730
+ condensed = condensed.replace(filler, (match) => {
731
+ // Keep single space for word boundaries
732
+ if (match.trim() === '')
733
+ return ' ';
734
+ if (/^(the|a|an)$/i.test(match.trim()))
735
+ return ' ';
736
+ if (/including|related to|regarding|about/i.test(match))
737
+ return ': ';
738
+ return ' ';
739
+ });
740
+ }
741
+ condensed = condensed.replace(/ +/g, ' ').trim();
742
+ // If condensed version is short enough, use it
743
+ if (condensed.length <= MAX_LEN) {
744
+ return condensed.replace(/[.!?,:\s]+$/, '');
745
+ }
746
+ // Try to cut at a natural boundary (colon, dash, comma)
747
+ const boundaryMatch = condensed.substring(0, MAX_LEN + 5).match(/^(.{20,}?)[:\-,]/);
748
+ if (boundaryMatch && boundaryMatch[1].length <= MAX_LEN) {
749
+ return boundaryMatch[1].trim();
750
+ }
751
+ // Last resort: truncate at word boundary
752
+ const truncated = condensed.substring(0, MAX_LEN);
753
+ const lastSpace = truncated.lastIndexOf(' ');
754
+ if (lastSpace > 20) {
755
+ return truncated.substring(0, lastSpace).replace(/[.!?,:\s]+$/, '');
756
+ }
757
+ return truncated.replace(/[.!?,:\s]+$/, '');
758
+ }
759
+ /**
760
+ * Extract the short description from the parent session's transcript.
761
+ * This is the most reliable method because it reads the actual Task tool result
762
+ * that spawned this agent, which contains the description directly.
763
+ *
764
+ * The parent transcript contains a 'user' entry with toolUseResult that has:
765
+ * - agentId: the spawned agent's ID
766
+ * - description: the Task description parameter
767
+ *
768
+ * We look for this format:
769
+ * { "type": "user", "toolUseResult": { "agentId": "xxx", "description": "..." } }
770
+ */
771
+ async extractDescriptionFromParentTranscript(projectHash, sessionId, agentId) {
772
+ const cacheKey = `${projectHash}/${sessionId}`;
773
+ const CACHE_TTL_MS = 5000;
774
+ // Check cache first (covers burst of simultaneous agent discoveries)
775
+ const cached = this.parentDescriptionCache.get(cacheKey);
776
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
777
+ return cached.descriptions.get(agentId);
778
+ }
779
+ try {
780
+ // The parent session's transcript is at: ~/.claude/projects/{projectHash}/{sessionId}.jsonl
781
+ const transcriptPath = join(CLAUDE_PROJECTS_DIR, projectHash, `${sessionId}.jsonl`);
782
+ try {
783
+ await statAsync(transcriptPath);
784
+ }
785
+ catch {
786
+ return undefined;
787
+ }
788
+ const content = await readFile(transcriptPath, 'utf8');
789
+ const lines = content.split('\n').filter((l) => l.trim());
790
+ // Parse ALL toolUseResult entries into a Map and cache them
791
+ const descriptions = new Map();
792
+ for (const line of lines) {
793
+ try {
794
+ const entry = JSON.parse(line);
795
+ if (entry.type === 'user' && entry.toolUseResult?.agentId && entry.toolUseResult?.description) {
796
+ descriptions.set(entry.toolUseResult.agentId, entry.toolUseResult.description);
797
+ }
798
+ }
799
+ catch {
800
+ // Skip malformed lines
801
+ }
802
+ }
803
+ this.parentDescriptionCache.set(cacheKey, { descriptions, timestamp: Date.now() });
804
+ return descriptions.get(agentId);
805
+ }
806
+ catch {
807
+ // Failed to read transcript
808
+ }
809
+ return undefined;
810
+ }
811
+ /**
812
+ * Extract description from agent file by finding first user message
813
+ */
814
+ async extractDescriptionFromFile(filePath) {
815
+ try {
816
+ // Only read the first 8KB — more than enough for 5 JSONL lines
817
+ const stream = createReadStream(filePath, { end: 8191 });
818
+ const rl = createInterface({ input: stream });
819
+ return await new Promise((resolve) => {
820
+ let lineCount = 0;
821
+ let resolved = false;
822
+ rl.on('line', (line) => {
823
+ if (resolved || lineCount >= 5) {
824
+ rl.close();
825
+ stream.destroy();
826
+ return;
827
+ }
828
+ lineCount++;
829
+ if (!line.trim())
830
+ return;
831
+ try {
832
+ const entry = JSON.parse(line);
833
+ if (entry.type === 'user' && entry.message?.content) {
834
+ let text;
835
+ if (typeof entry.message.content === 'string') {
836
+ text = entry.message.content.trim();
837
+ }
838
+ else if (Array.isArray(entry.message.content)) {
839
+ const firstContent = entry.message.content[0];
840
+ if (firstContent?.type === 'text' && firstContent.text) {
841
+ text = firstContent.text.trim();
842
+ }
843
+ }
844
+ if (text) {
845
+ resolved = true;
846
+ rl.close();
847
+ stream.destroy();
848
+ resolve(this.extractSmartTitle(text));
849
+ }
850
+ }
851
+ }
852
+ catch {
853
+ // Skip malformed lines
854
+ }
855
+ });
856
+ rl.on('close', () => {
857
+ if (!resolved)
858
+ resolve(undefined);
859
+ });
860
+ rl.on('error', () => {
861
+ if (!resolved)
862
+ resolve(undefined);
863
+ });
864
+ });
865
+ }
866
+ catch {
867
+ // Failed to read file
868
+ }
869
+ return undefined;
870
+ }
871
+ /**
872
+ * Scan for all subagent directories (async to avoid blocking event loop)
873
+ */
874
+ async scanForSubagents() {
875
+ try {
876
+ await statAsync(CLAUDE_PROJECTS_DIR);
877
+ }
878
+ catch {
879
+ return;
880
+ }
881
+ try {
882
+ const projects = await readdir(CLAUDE_PROJECTS_DIR);
883
+ for (const project of projects) {
884
+ const projectPath = join(CLAUDE_PROJECTS_DIR, project);
885
+ try {
886
+ const st = await statAsync(projectPath);
887
+ if (!st.isDirectory())
888
+ continue;
889
+ const sessions = await readdir(projectPath);
890
+ for (const session of sessions) {
891
+ const sessionPath = join(projectPath, session);
892
+ try {
893
+ const sessionStat = await statAsync(sessionPath);
894
+ if (!sessionStat.isDirectory())
895
+ continue;
896
+ const subagentDir = join(sessionPath, 'subagents');
897
+ try {
898
+ await statAsync(subagentDir);
899
+ await this.watchSubagentDir(subagentDir, project, session);
900
+ }
901
+ catch {
902
+ // subagent dir doesn't exist - skip
903
+ }
904
+ }
905
+ catch {
906
+ // Skip inaccessible session directories
907
+ }
908
+ }
909
+ }
910
+ catch {
911
+ // Skip inaccessible project directories
912
+ }
913
+ }
914
+ }
915
+ catch (error) {
916
+ this.emit('subagent:error', error);
917
+ }
918
+ }
919
+ /**
920
+ * Watch a subagent directory for new/updated files
921
+ */
922
+ async watchSubagentDir(dir, projectHash, sessionId) {
923
+ if (this.knownSubagentDirs.has(dir))
924
+ return;
925
+ this.knownSubagentDirs.add(dir);
926
+ // Watch existing files (initial scan - skip old files)
927
+ try {
928
+ const files = await readdir(dir);
929
+ for (const file of files) {
930
+ if (file.endsWith('.jsonl')) {
931
+ await this.watchAgentFile(join(dir, file), projectHash, sessionId, true);
932
+ }
933
+ }
934
+ }
935
+ catch {
936
+ return;
937
+ }
938
+ // Watch for new files with debounce to allow content to be written
939
+ try {
940
+ const watcher = watch(dir, (_eventType, filename) => {
941
+ if (filename?.endsWith('.jsonl')) {
942
+ const filePath = join(dir, filename);
943
+ // Wait 100ms for file content to be written before processing
944
+ // Even if file is empty after debounce, we still watch it - the
945
+ // description retry mechanisms in processEntry and the file change
946
+ // handler will extract description when content arrives
947
+ setTimeout(() => {
948
+ if (existsSync(filePath)) {
949
+ this.watchAgentFile(filePath, projectHash, sessionId);
950
+ }
951
+ }, FILE_CONTENT_DEBOUNCE_MS);
952
+ }
953
+ });
954
+ // Handle watcher errors to prevent unhandled exceptions
955
+ // Store handler reference for proper cleanup
956
+ const errorHandler = (error) => {
957
+ this.emit('subagent:error', error instanceof Error ? error : new Error(String(error)));
958
+ watcher.close();
959
+ this.dirWatcherErrorHandlers.delete(dir);
960
+ this.dirWatchers.delete(dir);
961
+ this.knownSubagentDirs.delete(dir);
962
+ };
963
+ watcher.on('error', errorHandler);
964
+ this.dirWatcherErrorHandlers.set(dir, errorHandler);
965
+ this.dirWatchers.set(dir, watcher);
966
+ }
967
+ catch {
968
+ // Watch failed
969
+ }
970
+ }
971
+ /**
972
+ * Watch a specific agent transcript file
973
+ * @param filePath Path to the agent transcript file
974
+ * @param projectHash Claude project hash
975
+ * @param sessionId Claude session ID
976
+ * @param isInitialScan If true, skip files older than STARTUP_MAX_FILE_AGE_MS
977
+ */
978
+ async watchAgentFile(filePath, projectHash, sessionId, isInitialScan = false) {
979
+ if (this.fileWatchers.has(filePath))
980
+ return;
981
+ const agentId = basename(filePath).replace('agent-', '').replace('.jsonl', '');
982
+ // Initial info - handle race condition where file may be deleted between discovery and stat
983
+ let stat;
984
+ try {
985
+ stat = await statAsync(filePath);
986
+ }
987
+ catch {
988
+ // File was deleted between discovery and stat - skip this agent
989
+ return;
990
+ }
991
+ // On initial scan, skip old files to avoid loading stale historical data
992
+ if (isInitialScan) {
993
+ const fileAge = Date.now() - stat.mtime.getTime();
994
+ if (fileAge > STARTUP_MAX_FILE_AGE_MS) {
995
+ return; // Skip old files on startup
996
+ }
997
+ }
998
+ // Extract description - prefer reading from parent transcript (most reliable)
999
+ // The parent transcript has the exact Task tool call with description parameter
1000
+ let description = await this.extractDescriptionFromParentTranscript(projectHash, sessionId, agentId);
1001
+ // Fallback: extract a smart title from the subagent's prompt if parent lookup failed
1002
+ if (!description) {
1003
+ description = await this.extractDescriptionFromFile(filePath);
1004
+ }
1005
+ // Skip internal Claude Code agents (e.g., suggestion mode) - not real subagents
1006
+ if (this.isInternalAgent(description)) {
1007
+ return;
1008
+ }
1009
+ const info = {
1010
+ agentId,
1011
+ sessionId,
1012
+ projectHash,
1013
+ filePath,
1014
+ startedAt: stat.birthtime.toISOString(),
1015
+ lastActivityAt: stat.mtime.getTime(),
1016
+ status: 'active',
1017
+ toolCallCount: 0,
1018
+ entryCount: 0,
1019
+ fileSize: stat.size,
1020
+ description,
1021
+ };
1022
+ // Enforce MAX_TRACKED_AGENTS during insertion — evict oldest inactive agent
1023
+ if (this.agentInfo.size >= MAX_TRACKED_AGENTS) {
1024
+ let oldestId = null;
1025
+ let oldestTime = Infinity;
1026
+ for (const [id, existing] of this.agentInfo) {
1027
+ if (existing.status !== 'active' && existing.lastActivityAt < oldestTime) {
1028
+ oldestTime = existing.lastActivityAt;
1029
+ oldestId = id;
1030
+ }
1031
+ }
1032
+ if (oldestId) {
1033
+ this.removeAgent(oldestId);
1034
+ }
1035
+ }
1036
+ this.agentInfo.set(agentId, info);
1037
+ this.emit('subagent:discovered', info);
1038
+ // Read existing content
1039
+ this.tailFile(filePath, agentId, sessionId, 0)
1040
+ .then((position) => {
1041
+ this.filePositions.set(filePath, position);
1042
+ })
1043
+ .catch((err) => {
1044
+ // Log but don't throw - non-critical background operation
1045
+ console.warn(`[SubagentWatcher] Failed to read initial content for ${agentId}:`, err);
1046
+ });
1047
+ // Watch for changes
1048
+ try {
1049
+ const watcher = watch(filePath, async (eventType) => {
1050
+ if (eventType === 'change') {
1051
+ const currentPos = this.filePositions.get(filePath) || 0;
1052
+ const newPos = await this.tailFile(filePath, agentId, sessionId, currentPos);
1053
+ this.filePositions.set(filePath, newPos);
1054
+ // Update info
1055
+ const existingInfo = this.agentInfo.get(agentId);
1056
+ if (existingInfo) {
1057
+ try {
1058
+ const newStat = await statAsync(filePath);
1059
+ existingInfo.lastActivityAt = Date.now();
1060
+ existingInfo.fileSize = newStat.size;
1061
+ existingInfo.status = 'active';
1062
+ }
1063
+ catch {
1064
+ // Stat failed
1065
+ }
1066
+ // Retry description extraction if missing (race condition fix)
1067
+ if (!existingInfo.description) {
1068
+ // First try parent transcript (most reliable)
1069
+ let extractedDescription = await this.extractDescriptionFromParentTranscript(existingInfo.projectHash, existingInfo.sessionId, agentId);
1070
+ // Fallback to subagent file
1071
+ if (!extractedDescription) {
1072
+ extractedDescription = await this.extractDescriptionFromFile(filePath);
1073
+ }
1074
+ if (extractedDescription) {
1075
+ // Check if this is an internal agent - if so, remove it
1076
+ if (this.isInternalAgent(extractedDescription)) {
1077
+ this.removeAgent(agentId);
1078
+ return;
1079
+ }
1080
+ existingInfo.description = extractedDescription;
1081
+ this.emit('subagent:updated', existingInfo);
1082
+ }
1083
+ }
1084
+ // Reset idle timer
1085
+ this.resetIdleTimer(agentId);
1086
+ }
1087
+ }
1088
+ });
1089
+ // Handle watcher errors to prevent unhandled exceptions
1090
+ // Store handler reference for proper cleanup
1091
+ const errorHandler = (error) => {
1092
+ this.emit('subagent:error', error instanceof Error ? error : new Error(String(error)), agentId);
1093
+ watcher.close();
1094
+ this.fileWatcherErrorHandlers.delete(filePath);
1095
+ this.fileWatchers.delete(filePath);
1096
+ };
1097
+ watcher.on('error', errorHandler);
1098
+ this.fileWatcherErrorHandlers.set(filePath, errorHandler);
1099
+ this.fileWatchers.set(filePath, watcher);
1100
+ this.resetIdleTimer(agentId);
1101
+ }
1102
+ catch {
1103
+ // Watch failed
1104
+ }
1105
+ }
1106
+ /**
1107
+ * Tail a file from a specific position
1108
+ */
1109
+ async tailFile(filePath, agentId, sessionId, fromPosition) {
1110
+ return new Promise((resolve) => {
1111
+ let position = fromPosition;
1112
+ const stream = createReadStream(filePath, { start: fromPosition });
1113
+ const rl = createInterface({ input: stream });
1114
+ rl.on('line', (line) => {
1115
+ const lineBytes = Buffer.byteLength(line, 'utf8') + 1;
1116
+ position += lineBytes; // Always advance past the line
1117
+ try {
1118
+ const entry = JSON.parse(line);
1119
+ this.processEntry(entry, agentId, sessionId).catch(() => {
1120
+ // processEntry failure is non-critical
1121
+ });
1122
+ // Update entry count
1123
+ const info = this.agentInfo.get(agentId);
1124
+ if (info) {
1125
+ info.entryCount++;
1126
+ }
1127
+ }
1128
+ catch {
1129
+ // Malformed JSON line — skip it
1130
+ }
1131
+ });
1132
+ rl.on('close', () => {
1133
+ resolve(position);
1134
+ });
1135
+ rl.on('error', () => {
1136
+ resolve(position);
1137
+ });
1138
+ });
1139
+ }
1140
+ /**
1141
+ * Process a transcript entry and emit appropriate events
1142
+ */
1143
+ async processEntry(entry, agentId, sessionId) {
1144
+ const info = this.agentInfo.get(agentId);
1145
+ // Extract model from assistant messages (first one sets the model)
1146
+ if (info && entry.type === 'assistant' && entry.message?.model && !info.model) {
1147
+ info.model = entry.message.model;
1148
+ info.modelShort = this.extractModelShort(entry.message.model);
1149
+ this.emit('subagent:updated', info);
1150
+ }
1151
+ // Aggregate token usage from messages
1152
+ if (info && entry.message?.usage) {
1153
+ if (entry.message.usage.input_tokens) {
1154
+ info.totalInputTokens = (info.totalInputTokens || 0) + entry.message.usage.input_tokens;
1155
+ }
1156
+ if (entry.message.usage.output_tokens) {
1157
+ info.totalOutputTokens = (info.totalOutputTokens || 0) + entry.message.usage.output_tokens;
1158
+ }
1159
+ }
1160
+ // Check if this is first user message and description is missing
1161
+ if (info && !info.description && entry.type === 'user' && entry.message?.content) {
1162
+ // First try parent transcript (most reliable)
1163
+ let description = await this.extractDescriptionFromParentTranscript(info.projectHash, info.sessionId, agentId);
1164
+ // Fallback: extract smart title from the prompt content
1165
+ if (!description) {
1166
+ let text;
1167
+ if (typeof entry.message.content === 'string') {
1168
+ text = entry.message.content.trim();
1169
+ }
1170
+ else if (Array.isArray(entry.message.content)) {
1171
+ const firstContent = entry.message.content[0];
1172
+ if (firstContent?.type === 'text' && firstContent.text) {
1173
+ text = firstContent.text.trim();
1174
+ }
1175
+ }
1176
+ if (text) {
1177
+ description = this.extractSmartTitle(text);
1178
+ }
1179
+ }
1180
+ if (description) {
1181
+ // Check if this is an internal agent - if so, remove it
1182
+ if (this.isInternalAgent(description)) {
1183
+ this.removeAgent(agentId);
1184
+ return;
1185
+ }
1186
+ info.description = description;
1187
+ this.emit('subagent:updated', info);
1188
+ }
1189
+ }
1190
+ if (entry.type === 'progress' && entry.data) {
1191
+ const progress = {
1192
+ agentId,
1193
+ sessionId,
1194
+ timestamp: entry.timestamp,
1195
+ progressType: entry.data.type,
1196
+ query: entry.data.query,
1197
+ resultCount: entry.data.resultCount,
1198
+ // Extract hook event info if present
1199
+ hookEvent: entry.data.hookEvent,
1200
+ hookName: entry.data.hookName ||
1201
+ (entry.data.hookEvent && entry.data.tool_name
1202
+ ? `${entry.data.hookEvent}:${entry.data.tool_name}`
1203
+ : undefined),
1204
+ };
1205
+ this.emit('subagent:progress', progress);
1206
+ }
1207
+ else if (entry.type === 'assistant' && entry.message?.content) {
1208
+ // Handle both string and array content formats
1209
+ if (typeof entry.message.content === 'string') {
1210
+ const text = entry.message.content.trim();
1211
+ if (text.length > 0) {
1212
+ const message = {
1213
+ agentId,
1214
+ sessionId,
1215
+ timestamp: entry.timestamp,
1216
+ role: 'assistant',
1217
+ text: text.substring(0, MESSAGE_TEXT_LIMIT),
1218
+ };
1219
+ this.emit('subagent:message', message);
1220
+ }
1221
+ }
1222
+ else {
1223
+ for (const content of entry.message.content) {
1224
+ if (content.type === 'tool_use' && content.name) {
1225
+ // Store toolUseId for linking to results, with timestamp for TTL cleanup
1226
+ if (content.id) {
1227
+ if (!this.pendingToolCalls.has(agentId)) {
1228
+ this.pendingToolCalls.set(agentId, new Map());
1229
+ }
1230
+ const agentCalls = this.pendingToolCalls.get(agentId);
1231
+ // Enforce size limit to prevent memory leak from rapid tool calls
1232
+ if (agentCalls.size >= MAX_PENDING_TOOL_CALLS) {
1233
+ // FIFO eviction: delete first (oldest) entry using Map insertion order
1234
+ const firstKey = agentCalls.keys().next().value;
1235
+ if (firstKey !== undefined)
1236
+ agentCalls.delete(firstKey);
1237
+ }
1238
+ agentCalls.set(content.id, {
1239
+ toolName: content.name,
1240
+ timestamp: Date.now(),
1241
+ });
1242
+ }
1243
+ const toolCall = {
1244
+ agentId,
1245
+ sessionId,
1246
+ timestamp: entry.timestamp,
1247
+ tool: content.name,
1248
+ input: this.getTruncatedInput(content.name, content.input || {}),
1249
+ toolUseId: content.id,
1250
+ fullInput: content.input || {},
1251
+ };
1252
+ this.emit('subagent:tool_call', toolCall);
1253
+ // Update tool call count
1254
+ const agentInfo = this.agentInfo.get(agentId);
1255
+ if (agentInfo) {
1256
+ agentInfo.toolCallCount++;
1257
+ }
1258
+ }
1259
+ else if (content.type === 'tool_result' && content.tool_use_id) {
1260
+ // Extract tool result
1261
+ const resultContent = this.extractToolResultContent(content.content);
1262
+ const agentPendingCalls = this.pendingToolCalls.get(agentId);
1263
+ const pendingCall = agentPendingCalls?.get(content.tool_use_id);
1264
+ const toolName = pendingCall?.toolName;
1265
+ // Delete after lookup to prevent memory leak
1266
+ agentPendingCalls?.delete(content.tool_use_id);
1267
+ const toolResult = {
1268
+ agentId,
1269
+ sessionId,
1270
+ timestamp: entry.timestamp,
1271
+ toolUseId: content.tool_use_id,
1272
+ tool: toolName,
1273
+ preview: resultContent.substring(0, MESSAGE_TEXT_LIMIT),
1274
+ contentLength: resultContent.length,
1275
+ isError: content.is_error || false,
1276
+ };
1277
+ this.emit('subagent:tool_result', toolResult);
1278
+ }
1279
+ else if (content.type === 'text' && content.text) {
1280
+ const text = content.text.trim();
1281
+ if (text.length > 0) {
1282
+ const message = {
1283
+ agentId,
1284
+ sessionId,
1285
+ timestamp: entry.timestamp,
1286
+ role: 'assistant',
1287
+ text: text.substring(0, MESSAGE_TEXT_LIMIT), // Limit text length
1288
+ };
1289
+ this.emit('subagent:message', message);
1290
+ }
1291
+ }
1292
+ }
1293
+ }
1294
+ }
1295
+ else if (entry.type === 'user' && entry.message?.content) {
1296
+ // Handle both string and array content formats - also check for tool_result in user messages
1297
+ if (typeof entry.message.content === 'string') {
1298
+ const userText = entry.message.content.trim();
1299
+ if (userText.length > 0 && userText.length < 500) {
1300
+ const message = {
1301
+ agentId,
1302
+ sessionId,
1303
+ timestamp: entry.timestamp,
1304
+ role: 'user',
1305
+ text: userText,
1306
+ };
1307
+ this.emit('subagent:message', message);
1308
+ }
1309
+ }
1310
+ else {
1311
+ // Check for tool_result blocks in user messages (common pattern)
1312
+ for (const content of entry.message.content) {
1313
+ if (content.type === 'tool_result' && content.tool_use_id) {
1314
+ const resultContent = this.extractToolResultContent(content.content);
1315
+ const agentPendingCalls = this.pendingToolCalls.get(agentId);
1316
+ const pendingCall = agentPendingCalls?.get(content.tool_use_id);
1317
+ const toolName = pendingCall?.toolName;
1318
+ // Delete after lookup to prevent memory leak
1319
+ agentPendingCalls?.delete(content.tool_use_id);
1320
+ const toolResult = {
1321
+ agentId,
1322
+ sessionId,
1323
+ timestamp: entry.timestamp,
1324
+ toolUseId: content.tool_use_id,
1325
+ tool: toolName,
1326
+ preview: resultContent.substring(0, MESSAGE_TEXT_LIMIT),
1327
+ contentLength: resultContent.length,
1328
+ isError: content.is_error || false,
1329
+ };
1330
+ this.emit('subagent:tool_result', toolResult);
1331
+ }
1332
+ else if (content.type === 'text' && content.text) {
1333
+ const userText = content.text.trim();
1334
+ if (userText.length > 0 && userText.length < 500) {
1335
+ const message = {
1336
+ agentId,
1337
+ sessionId,
1338
+ timestamp: entry.timestamp,
1339
+ role: 'user',
1340
+ text: userText,
1341
+ };
1342
+ this.emit('subagent:message', message);
1343
+ }
1344
+ }
1345
+ }
1346
+ }
1347
+ }
1348
+ }
1349
+ /**
1350
+ * Extract text content from tool_result content field
1351
+ */
1352
+ extractToolResultContent(content) {
1353
+ if (!content)
1354
+ return '';
1355
+ if (typeof content === 'string')
1356
+ return content;
1357
+ if (Array.isArray(content)) {
1358
+ return content
1359
+ .filter((c) => c.type === 'text' && c.text)
1360
+ .map((c) => c.text)
1361
+ .join('\n');
1362
+ }
1363
+ return '';
1364
+ }
1365
+ /**
1366
+ * Get truncated input for display (keeps primary param, truncates large content)
1367
+ */
1368
+ getTruncatedInput(_tool, input) {
1369
+ const truncated = {};
1370
+ for (const [key, value] of Object.entries(input)) {
1371
+ if (typeof value === 'string' && value.length > INPUT_TRUNCATE_LENGTH) {
1372
+ // Keep short preview of long strings
1373
+ truncated[key] = value.substring(0, INPUT_TRUNCATE_LENGTH) + '...';
1374
+ }
1375
+ else {
1376
+ truncated[key] = value;
1377
+ }
1378
+ }
1379
+ return truncated;
1380
+ }
1381
+ /**
1382
+ * Reset idle timer for an agent
1383
+ */
1384
+ resetIdleTimer(agentId) {
1385
+ const existing = this.idleTimers.get(agentId);
1386
+ if (existing) {
1387
+ clearTimeout(existing);
1388
+ }
1389
+ const timer = setTimeout(() => {
1390
+ // Guard against race condition: agent may have been deleted before timer fires
1391
+ const info = this.agentInfo.get(agentId);
1392
+ if (!info) {
1393
+ // Agent was deleted - clean up timer reference
1394
+ this.idleTimers.delete(agentId);
1395
+ return;
1396
+ }
1397
+ if (info.status === 'active') {
1398
+ info.status = 'idle';
1399
+ }
1400
+ }, IDLE_TIMEOUT_MS);
1401
+ this.idleTimers.set(agentId, timer);
1402
+ }
1403
+ /**
1404
+ * Format a tool call for display
1405
+ */
1406
+ formatToolCall(timestamp, name, input) {
1407
+ const icons = {
1408
+ WebSearch: '🔍',
1409
+ WebFetch: '🌐',
1410
+ Read: '📖',
1411
+ Write: '📝',
1412
+ Edit: '✏️',
1413
+ Bash: '💻',
1414
+ Glob: '📁',
1415
+ Grep: '🔎',
1416
+ Task: '🤖',
1417
+ };
1418
+ const icon = icons[name] || '🔧';
1419
+ let details = '';
1420
+ if (name === 'WebSearch' && input.query) {
1421
+ details = `"${input.query}"`;
1422
+ }
1423
+ else if (name === 'WebFetch' && input.url) {
1424
+ details = input.url;
1425
+ }
1426
+ else if (name === 'Read' && input.file_path) {
1427
+ details = input.file_path;
1428
+ }
1429
+ else if ((name === 'Write' || name === 'Edit') && input.file_path) {
1430
+ details = input.file_path;
1431
+ }
1432
+ else if (name === 'Bash' && input.command) {
1433
+ const cmd = input.command;
1434
+ details = cmd.length > COMMAND_DISPLAY_LENGTH ? cmd.substring(0, COMMAND_DISPLAY_LENGTH) + '...' : cmd;
1435
+ }
1436
+ else if (name === 'Glob' && input.pattern) {
1437
+ details = input.pattern;
1438
+ }
1439
+ else if (name === 'Grep' && input.pattern) {
1440
+ details = input.pattern;
1441
+ }
1442
+ else if (name === 'Task' && input.description) {
1443
+ details = input.description;
1444
+ }
1445
+ return `${this.formatTime(timestamp)} ${icon} ${name}: ${details}`;
1446
+ }
1447
+ /**
1448
+ * Format a progress event for display
1449
+ */
1450
+ formatProgress(entry) {
1451
+ const data = entry.data;
1452
+ if (data.type === 'query_update') {
1453
+ return `${this.formatTime(entry.timestamp)} ⟳ Searching: "${data.query}"`;
1454
+ }
1455
+ else if (data.type === 'search_results_received') {
1456
+ return `${this.formatTime(entry.timestamp)} ✓ Got ${data.resultCount} results`;
1457
+ }
1458
+ return `${this.formatTime(entry.timestamp)} Progress: ${data.type}`;
1459
+ }
1460
+ /**
1461
+ * Format timestamp for display
1462
+ */
1463
+ formatTime(timestamp) {
1464
+ return new Date(timestamp).toLocaleTimeString();
1465
+ }
1466
+ }
1467
+ // Export singleton instance
1468
+ export const subagentWatcher = new SubagentWatcher();
1469
+ //# sourceMappingURL=subagent-watcher.js.map