@foothill/agent-move 1.0.6

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 (163) hide show
  1. package/.github/screenshot.png +0 -0
  2. package/README.md +266 -0
  3. package/bin/cli.js +229 -0
  4. package/package.json +53 -0
  5. package/packages/client/dist/assets/BufferResource-Ddjob236.js +185 -0
  6. package/packages/client/dist/assets/CanvasRenderer-B0w6SYyW.js +1 -0
  7. package/packages/client/dist/assets/Filter-NcMGuiK-.js +1 -0
  8. package/packages/client/dist/assets/RenderTargetSystem-DgAzY5_U.js +172 -0
  9. package/packages/client/dist/assets/WebGLRenderer-DUWXDPIX.js +156 -0
  10. package/packages/client/dist/assets/WebGPURenderer-C1HbrllR.js +41 -0
  11. package/packages/client/dist/assets/browserAll-CaF1Fl0O.js +14 -0
  12. package/packages/client/dist/assets/index-CMmR_RuS.css +1 -0
  13. package/packages/client/dist/assets/index-Dh8yWoLP.js +711 -0
  14. package/packages/client/dist/assets/webworkerAll-BJ6UhC7r.js +83 -0
  15. package/packages/client/dist/favicon.svg +27 -0
  16. package/packages/client/dist/index.html +167 -0
  17. package/packages/client/package.json +19 -0
  18. package/packages/server/dist/config.d.ts +12 -0
  19. package/packages/server/dist/config.d.ts.map +1 -0
  20. package/packages/server/dist/config.js +19 -0
  21. package/packages/server/dist/config.js.map +1 -0
  22. package/packages/server/dist/hooks/hook-event-manager.d.ts +39 -0
  23. package/packages/server/dist/hooks/hook-event-manager.d.ts.map +1 -0
  24. package/packages/server/dist/hooks/hook-event-manager.js +161 -0
  25. package/packages/server/dist/hooks/hook-event-manager.js.map +1 -0
  26. package/packages/server/dist/hooks/hook-installer.d.ts +14 -0
  27. package/packages/server/dist/hooks/hook-installer.d.ts.map +1 -0
  28. package/packages/server/dist/hooks/hook-installer.js +179 -0
  29. package/packages/server/dist/hooks/hook-installer.js.map +1 -0
  30. package/packages/server/dist/index.d.ts +4 -0
  31. package/packages/server/dist/index.d.ts.map +1 -0
  32. package/packages/server/dist/index.js +3544 -0
  33. package/packages/server/dist/index.js.map +1 -0
  34. package/packages/server/dist/routes/api.d.ts +4 -0
  35. package/packages/server/dist/routes/api.d.ts.map +1 -0
  36. package/packages/server/dist/routes/api.js +27 -0
  37. package/packages/server/dist/routes/api.js.map +1 -0
  38. package/packages/server/dist/state/activity-processor.d.ts +31 -0
  39. package/packages/server/dist/state/activity-processor.d.ts.map +1 -0
  40. package/packages/server/dist/state/activity-processor.js +417 -0
  41. package/packages/server/dist/state/activity-processor.js.map +1 -0
  42. package/packages/server/dist/state/agent-state-manager.d.ts +107 -0
  43. package/packages/server/dist/state/agent-state-manager.d.ts.map +1 -0
  44. package/packages/server/dist/state/agent-state-manager.js +1622 -0
  45. package/packages/server/dist/state/agent-state-manager.js.map +1 -0
  46. package/packages/server/dist/state/anomaly-detector.d.ts +23 -0
  47. package/packages/server/dist/state/anomaly-detector.d.ts.map +1 -0
  48. package/packages/server/dist/state/anomaly-detector.js +272 -0
  49. package/packages/server/dist/state/anomaly-detector.js.map +1 -0
  50. package/packages/server/dist/state/identity-manager.d.ts +31 -0
  51. package/packages/server/dist/state/identity-manager.d.ts.map +1 -0
  52. package/packages/server/dist/state/identity-manager.js +63 -0
  53. package/packages/server/dist/state/identity-manager.js.map +1 -0
  54. package/packages/server/dist/state/role-resolver.d.ts +23 -0
  55. package/packages/server/dist/state/role-resolver.d.ts.map +1 -0
  56. package/packages/server/dist/state/role-resolver.js +43 -0
  57. package/packages/server/dist/state/role-resolver.js.map +1 -0
  58. package/packages/server/dist/state/task-graph-manager.d.ts +31 -0
  59. package/packages/server/dist/state/task-graph-manager.d.ts.map +1 -0
  60. package/packages/server/dist/state/task-graph-manager.js +191 -0
  61. package/packages/server/dist/state/task-graph-manager.js.map +1 -0
  62. package/packages/server/dist/state/tool-chain-tracker.d.ts +23 -0
  63. package/packages/server/dist/state/tool-chain-tracker.d.ts.map +1 -0
  64. package/packages/server/dist/state/tool-chain-tracker.js +113 -0
  65. package/packages/server/dist/state/tool-chain-tracker.js.map +1 -0
  66. package/packages/server/dist/watcher/agent-watcher.d.ts +16 -0
  67. package/packages/server/dist/watcher/agent-watcher.d.ts.map +1 -0
  68. package/packages/server/dist/watcher/agent-watcher.js +0 -0
  69. package/packages/server/dist/watcher/agent-watcher.js.map +1 -0
  70. package/packages/server/dist/watcher/claude-paths.d.ts +32 -0
  71. package/packages/server/dist/watcher/claude-paths.d.ts.map +1 -0
  72. package/packages/server/dist/watcher/claude-paths.js +104 -0
  73. package/packages/server/dist/watcher/claude-paths.js.map +1 -0
  74. package/packages/server/dist/watcher/file-watcher.d.ts +17 -0
  75. package/packages/server/dist/watcher/file-watcher.d.ts.map +1 -0
  76. package/packages/server/dist/watcher/file-watcher.js +347 -0
  77. package/packages/server/dist/watcher/file-watcher.js.map +1 -0
  78. package/packages/server/dist/watcher/git-info.d.ts +3 -0
  79. package/packages/server/dist/watcher/git-info.d.ts.map +1 -0
  80. package/packages/server/dist/watcher/git-info.js +35 -0
  81. package/packages/server/dist/watcher/git-info.js.map +1 -0
  82. package/packages/server/dist/watcher/jsonl-parser.d.ts +21 -0
  83. package/packages/server/dist/watcher/jsonl-parser.d.ts.map +1 -0
  84. package/packages/server/dist/watcher/jsonl-parser.js +95 -0
  85. package/packages/server/dist/watcher/jsonl-parser.js.map +1 -0
  86. package/packages/server/dist/watcher/opencode/opencode-parser.d.ts +58 -0
  87. package/packages/server/dist/watcher/opencode/opencode-parser.d.ts.map +1 -0
  88. package/packages/server/dist/watcher/opencode/opencode-parser.js +256 -0
  89. package/packages/server/dist/watcher/opencode/opencode-parser.js.map +1 -0
  90. package/packages/server/dist/watcher/opencode/opencode-paths.d.ts +20 -0
  91. package/packages/server/dist/watcher/opencode/opencode-paths.d.ts.map +1 -0
  92. package/packages/server/dist/watcher/opencode/opencode-paths.js +35 -0
  93. package/packages/server/dist/watcher/opencode/opencode-paths.js.map +1 -0
  94. package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts +49 -0
  95. package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts.map +1 -0
  96. package/packages/server/dist/watcher/opencode/opencode-watcher.js +1292 -0
  97. package/packages/server/dist/watcher/opencode/opencode-watcher.js.map +1 -0
  98. package/packages/server/dist/watcher/session-scanner.d.ts +7 -0
  99. package/packages/server/dist/watcher/session-scanner.d.ts.map +1 -0
  100. package/packages/server/dist/watcher/session-scanner.js +69 -0
  101. package/packages/server/dist/watcher/session-scanner.js.map +1 -0
  102. package/packages/server/dist/ws/broadcaster.d.ts +18 -0
  103. package/packages/server/dist/ws/broadcaster.d.ts.map +1 -0
  104. package/packages/server/dist/ws/broadcaster.js +152 -0
  105. package/packages/server/dist/ws/broadcaster.js.map +1 -0
  106. package/packages/server/dist/ws/ws-handler.d.ts +6 -0
  107. package/packages/server/dist/ws/ws-handler.d.ts.map +1 -0
  108. package/packages/server/dist/ws/ws-handler.js +55 -0
  109. package/packages/server/dist/ws/ws-handler.js.map +1 -0
  110. package/packages/server/package.json +25 -0
  111. package/packages/shared/dist/constants/colors.d.ts +46 -0
  112. package/packages/shared/dist/constants/colors.d.ts.map +1 -0
  113. package/packages/shared/dist/constants/colors.js +178 -0
  114. package/packages/shared/dist/constants/colors.js.map +1 -0
  115. package/packages/shared/dist/constants/names.d.ts +6 -0
  116. package/packages/shared/dist/constants/names.d.ts.map +1 -0
  117. package/packages/shared/dist/constants/names.js +28 -0
  118. package/packages/shared/dist/constants/names.js.map +1 -0
  119. package/packages/shared/dist/constants/tools.d.ts +15 -0
  120. package/packages/shared/dist/constants/tools.d.ts.map +1 -0
  121. package/packages/shared/dist/constants/tools.js +120 -0
  122. package/packages/shared/dist/constants/tools.js.map +1 -0
  123. package/packages/shared/dist/constants/zones.d.ts +37 -0
  124. package/packages/shared/dist/constants/zones.d.ts.map +1 -0
  125. package/packages/shared/dist/constants/zones.js +128 -0
  126. package/packages/shared/dist/constants/zones.js.map +1 -0
  127. package/packages/shared/dist/index.d.ts +15 -0
  128. package/packages/shared/dist/index.d.ts.map +1 -0
  129. package/packages/shared/dist/index.js +7 -0
  130. package/packages/shared/dist/index.js.map +1 -0
  131. package/packages/shared/dist/types/agent.d.ts +85 -0
  132. package/packages/shared/dist/types/agent.d.ts.map +1 -0
  133. package/packages/shared/dist/types/agent.js +2 -0
  134. package/packages/shared/dist/types/agent.js.map +1 -0
  135. package/packages/shared/dist/types/anomaly.d.ts +18 -0
  136. package/packages/shared/dist/types/anomaly.d.ts.map +1 -0
  137. package/packages/shared/dist/types/anomaly.js +6 -0
  138. package/packages/shared/dist/types/anomaly.js.map +1 -0
  139. package/packages/shared/dist/types/hooks.d.ts +51 -0
  140. package/packages/shared/dist/types/hooks.d.ts.map +1 -0
  141. package/packages/shared/dist/types/hooks.js +2 -0
  142. package/packages/shared/dist/types/hooks.js.map +1 -0
  143. package/packages/shared/dist/types/jsonl.d.ts +62 -0
  144. package/packages/shared/dist/types/jsonl.d.ts.map +1 -0
  145. package/packages/shared/dist/types/jsonl.js +3 -0
  146. package/packages/shared/dist/types/jsonl.js.map +1 -0
  147. package/packages/shared/dist/types/task-graph.d.ts +20 -0
  148. package/packages/shared/dist/types/task-graph.d.ts.map +1 -0
  149. package/packages/shared/dist/types/task-graph.js +2 -0
  150. package/packages/shared/dist/types/task-graph.js.map +1 -0
  151. package/packages/shared/dist/types/tool-chain.d.ts +17 -0
  152. package/packages/shared/dist/types/tool-chain.d.ts.map +1 -0
  153. package/packages/shared/dist/types/tool-chain.js +2 -0
  154. package/packages/shared/dist/types/tool-chain.js.map +1 -0
  155. package/packages/shared/dist/types/websocket.d.ts +132 -0
  156. package/packages/shared/dist/types/websocket.d.ts.map +1 -0
  157. package/packages/shared/dist/types/websocket.js +2 -0
  158. package/packages/shared/dist/types/websocket.js.map +1 -0
  159. package/packages/shared/dist/types/zone.d.ts +21 -0
  160. package/packages/shared/dist/types/zone.d.ts.map +1 -0
  161. package/packages/shared/dist/types/zone.js +2 -0
  162. package/packages/shared/dist/types/zone.js.map +1 -0
  163. package/packages/shared/package.json +15 -0
@@ -0,0 +1,1622 @@
1
+ // dist/state/agent-state-manager.js
2
+ import { EventEmitter as EventEmitter2 } from "events";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { EventEmitter } from "events";
6
+ import { execSync } from "child_process";
7
+ var DEFAULT_ANOMALY_CONFIG = {
8
+ tokenVelocityThreshold: 1e5,
9
+ retryLoopThreshold: 5,
10
+ stuckTimeoutSeconds: 120
11
+ };
12
+ var TOOL_ZONE_MAP = {
13
+ // Files zone
14
+ Read: "files",
15
+ Write: "files",
16
+ Edit: "files",
17
+ Patch: "files",
18
+ Glob: "files",
19
+ NotebookEdit: "files",
20
+ // Terminal zone
21
+ Bash: "terminal",
22
+ // Search zone
23
+ Grep: "search",
24
+ WebSearch: "search",
25
+ // Web zone
26
+ WebFetch: "web",
27
+ mcp__chrome_devtools__navigate_page: "web",
28
+ mcp__chrome_devtools__click: "web",
29
+ mcp__chrome_devtools__fill: "web",
30
+ mcp__chrome_devtools__take_screenshot: "web",
31
+ mcp__chrome_devtools__take_snapshot: "web",
32
+ mcp__chrome_devtools__evaluate_script: "web",
33
+ // Thinking zone
34
+ EnterPlanMode: "thinking",
35
+ ExitPlanMode: "thinking",
36
+ AskUserQuestion: "thinking",
37
+ // Messaging zone
38
+ SendMessage: "messaging",
39
+ // Tasks zone
40
+ TaskCreate: "tasks",
41
+ TaskUpdate: "tasks",
42
+ TaskList: "tasks",
43
+ TaskGet: "tasks",
44
+ TodoRead: "tasks",
45
+ TodoWrite: "tasks",
46
+ // Spawn zone
47
+ Agent: "spawn",
48
+ TeamCreate: "spawn",
49
+ TeamDelete: "spawn"
50
+ };
51
+ function getZoneForTool(toolName) {
52
+ if (toolName.startsWith("mcp__"))
53
+ return "web";
54
+ return TOOL_ZONE_MAP[toolName] ?? "thinking";
55
+ }
56
+ var ZONES = [
57
+ // Row 0
58
+ {
59
+ id: "search",
60
+ label: "Search",
61
+ description: "Grep, WebSearch \u2014 Research & lookup",
62
+ icon: "\u{1F4DA}",
63
+ color: 15381256,
64
+ colStart: 0,
65
+ colSpan: 5,
66
+ rowStart: 0,
67
+ rowSpan: 1,
68
+ x: 0,
69
+ y: 0,
70
+ width: 0,
71
+ height: 0
72
+ },
73
+ {
74
+ id: "terminal",
75
+ label: "Terminal",
76
+ description: "Bash commands \u2014 Server room",
77
+ icon: "\u{1F4BB}",
78
+ color: 2278750,
79
+ colStart: 5,
80
+ colSpan: 3,
81
+ rowStart: 0,
82
+ rowSpan: 1,
83
+ x: 0,
84
+ y: 0,
85
+ width: 0,
86
+ height: 0
87
+ },
88
+ {
89
+ id: "web",
90
+ label: "Web",
91
+ description: "WebFetch, Browser \u2014 Network hub",
92
+ icon: "\u{1F310}",
93
+ color: 9133302,
94
+ colStart: 8,
95
+ colSpan: 4,
96
+ rowStart: 0,
97
+ rowSpan: 1,
98
+ x: 0,
99
+ y: 0,
100
+ width: 0,
101
+ height: 0
102
+ },
103
+ // Row 1
104
+ {
105
+ id: "files",
106
+ label: "Files",
107
+ description: "Read, Write, Edit, Glob \u2014 File storage",
108
+ icon: "\u{1F4C1}",
109
+ color: 3900150,
110
+ colStart: 0,
111
+ colSpan: 4,
112
+ rowStart: 1,
113
+ rowSpan: 1,
114
+ x: 0,
115
+ y: 0,
116
+ width: 0,
117
+ height: 0
118
+ },
119
+ {
120
+ id: "thinking",
121
+ label: "Thinking",
122
+ description: "Planning, Questions \u2014 Conference area",
123
+ icon: "\u{1F4AD}",
124
+ color: 16347926,
125
+ colStart: 4,
126
+ colSpan: 5,
127
+ rowStart: 1,
128
+ rowSpan: 1,
129
+ x: 0,
130
+ y: 0,
131
+ width: 0,
132
+ height: 0
133
+ },
134
+ {
135
+ id: "messaging",
136
+ label: "Messaging",
137
+ description: "SendMessage, Teams \u2014 Chat & relax",
138
+ icon: "\u{1F4AC}",
139
+ color: 15485081,
140
+ colStart: 9,
141
+ colSpan: 3,
142
+ rowStart: 1,
143
+ rowSpan: 1,
144
+ x: 0,
145
+ y: 0,
146
+ width: 0,
147
+ height: 0
148
+ },
149
+ // Row 2
150
+ {
151
+ id: "spawn",
152
+ label: "Spawn",
153
+ description: "Agent spawn/despawn \u2014 Entry portal",
154
+ icon: "\u{1F300}",
155
+ color: 11032055,
156
+ colStart: 0,
157
+ colSpan: 3,
158
+ rowStart: 2,
159
+ rowSpan: 1,
160
+ x: 0,
161
+ y: 0,
162
+ width: 0,
163
+ height: 0
164
+ },
165
+ {
166
+ id: "idle",
167
+ label: "Idle",
168
+ description: "Idle agents rest here \u2014 Kitchen & lounge",
169
+ icon: "\u2615",
170
+ color: 7041664,
171
+ colStart: 3,
172
+ colSpan: 5,
173
+ rowStart: 2,
174
+ rowSpan: 1,
175
+ x: 0,
176
+ y: 0,
177
+ width: 0,
178
+ height: 0
179
+ },
180
+ {
181
+ id: "tasks",
182
+ label: "Tasks",
183
+ description: "TaskCreate, TaskUpdate \u2014 Kanban & planning",
184
+ icon: "\u{1F4CB}",
185
+ color: 1357990,
186
+ colStart: 8,
187
+ colSpan: 4,
188
+ rowStart: 2,
189
+ rowSpan: 1,
190
+ x: 0,
191
+ y: 0,
192
+ width: 0,
193
+ height: 0
194
+ }
195
+ ];
196
+ var ZONE_MAP = new Map(ZONES.map((z) => [z.id, z]));
197
+ var AGENT_PALETTES = [
198
+ { name: "blue", body: 4886745, outline: 2906762, highlight: 8042741, eye: 16777215, skin: 16767916 },
199
+ { name: "green", body: 5025616, outline: 3046706, highlight: 8505220, eye: 16777215, skin: 16767916 },
200
+ { name: "red", body: 15037299, outline: 12986408, highlight: 16764370, eye: 16777215, skin: 16767916 },
201
+ { name: "purple", body: 11225020, outline: 6953882, highlight: 13538264, eye: 16777215, skin: 16767916 },
202
+ { name: "orange", body: 16750592, outline: 15094016, highlight: 16764032, eye: 16777215, skin: 16767916 },
203
+ { name: "cyan", body: 2541274, outline: 33679, highlight: 8445674, eye: 16777215, skin: 16767916 },
204
+ { name: "pink", body: 15753874, outline: 12720219, highlight: 16301008, eye: 16777215, skin: 16767916 },
205
+ { name: "teal", body: 2533018, outline: 26972, highlight: 8440772, eye: 16777215, skin: 16767916 },
206
+ { name: "amber", body: 16761095, outline: 16748288, highlight: 16769154, eye: 3355443, skin: 16767916 },
207
+ { name: "indigo", body: 6056896, outline: 2635155, highlight: 10463450, eye: 16777215, skin: 16767916 },
208
+ { name: "lime", body: 10275941, outline: 5606191, highlight: 12968357, eye: 3355443, skin: 16767916 },
209
+ { name: "brown", body: 9268835, outline: 5125166, highlight: 12364452, eye: 16777215, skin: 16767916 }
210
+ ];
211
+ function getProjectColorIndex(projectPath) {
212
+ let hash = 0;
213
+ for (let i = 0; i < projectPath.length; i++) {
214
+ hash = (hash << 5) - hash + projectPath.charCodeAt(i) | 0;
215
+ }
216
+ return Math.abs(hash) % AGENT_PALETTES.length;
217
+ }
218
+ var config = {
219
+ port: parseInt(process.env.AGENT_MOVE_PORT || "3333", 10),
220
+ claudeHome: join(homedir(), ".claude"),
221
+ idleTimeoutMs: 45e3,
222
+ /** How long after going idle before an agent is automatically shutdown/removed */
223
+ shutdownTimeoutMs: 30 * 60 * 1e3,
224
+ // 30 minutes
225
+ /** How recently a session file must be modified to be considered "active" on startup */
226
+ activeThresholdMs: 10 * 60 * 1e3,
227
+ // 10 minutes
228
+ /** Enable OpenCode session watching (auto-detected if storage dir exists) */
229
+ enableOpenCode: process.env.AGENT_MOVE_OPENCODE !== "false"
230
+ };
231
+ var COOLDOWN_MS = 6e4;
232
+ var TOKEN_WINDOW_MS = 6e4;
233
+ var anomalyIdCounter = 0;
234
+ var AnomalyDetector = class extends EventEmitter {
235
+ agents = /* @__PURE__ */ new Map();
236
+ agentNames = /* @__PURE__ */ new Map();
237
+ config;
238
+ stuckInterval = null;
239
+ constructor(config2) {
240
+ super();
241
+ this.config = { ...DEFAULT_ANOMALY_CONFIG, ...config2 };
242
+ }
243
+ getState(agentId) {
244
+ let state = this.agents.get(agentId);
245
+ if (!state) {
246
+ state = {
247
+ lastTools: [],
248
+ tokenSamples: [],
249
+ lastActivityAt: Date.now(),
250
+ lastAlertAt: /* @__PURE__ */ new Map()
251
+ };
252
+ this.agents.set(agentId, state);
253
+ }
254
+ return state;
255
+ }
256
+ setAgentName(agentId, name) {
257
+ this.agentNames.set(agentId, name);
258
+ }
259
+ checkToolUse(agentId, toolName) {
260
+ const state = this.getState(agentId);
261
+ state.lastActivityAt = Date.now();
262
+ state.lastTools.push(toolName);
263
+ if (state.lastTools.length > 20)
264
+ state.lastTools.shift();
265
+ const threshold = this.config.retryLoopThreshold;
266
+ if (state.lastTools.length >= threshold) {
267
+ const recent = state.lastTools.slice(-threshold);
268
+ if (recent.every((t) => t === recent[0])) {
269
+ this.alert(agentId, "retry-loop", `${recent[0]} called ${threshold}+ times consecutively`, "warning", {
270
+ tool: recent[0],
271
+ consecutiveCount: threshold
272
+ });
273
+ }
274
+ }
275
+ }
276
+ checkTokenUsage(agentId, inputTokens, outputTokens) {
277
+ const state = this.getState(agentId);
278
+ state.lastActivityAt = Date.now();
279
+ const now = Date.now();
280
+ const total = inputTokens + outputTokens;
281
+ state.tokenSamples.push({ ts: now, total });
282
+ const cutoff = now - TOKEN_WINDOW_MS;
283
+ while (state.tokenSamples.length > 0 && state.tokenSamples[0].ts < cutoff) {
284
+ state.tokenSamples.shift();
285
+ }
286
+ if (state.tokenSamples.length >= 2) {
287
+ const totalTokens = state.tokenSamples.reduce((sum, s) => sum + s.total, 0);
288
+ const elapsed = (state.tokenSamples[state.tokenSamples.length - 1].ts - state.tokenSamples[0].ts) / 6e4;
289
+ if (elapsed > 0.1) {
290
+ const velocity = totalTokens / elapsed;
291
+ if (velocity >= this.config.tokenVelocityThreshold) {
292
+ this.alert(agentId, "token-spike", `Token velocity ${Math.round(velocity)}/min exceeds threshold`, "critical", {
293
+ velocity: Math.round(velocity),
294
+ threshold: this.config.tokenVelocityThreshold
295
+ });
296
+ }
297
+ }
298
+ }
299
+ }
300
+ checkStuck(agentId, lastActivityAt) {
301
+ const state = this.getState(agentId);
302
+ const elapsed = (Date.now() - lastActivityAt) / 1e3;
303
+ if (elapsed >= this.config.stuckTimeoutSeconds) {
304
+ this.alert(agentId, "stuck-agent", `No activity for ${Math.round(elapsed)}s`, "warning", {
305
+ idleSeconds: Math.round(elapsed),
306
+ threshold: this.config.stuckTimeoutSeconds
307
+ });
308
+ }
309
+ state.lastActivityAt = lastActivityAt;
310
+ }
311
+ startStuckDetection(getActiveAgents) {
312
+ if (this.stuckInterval)
313
+ return;
314
+ this.stuckInterval = setInterval(() => {
315
+ for (const agent of getActiveAgents()) {
316
+ if (!agent.isIdle) {
317
+ this.checkStuck(agent.id, agent.lastActivityAt);
318
+ }
319
+ }
320
+ }, 3e4);
321
+ }
322
+ removeAgent(agentId) {
323
+ this.agents.delete(agentId);
324
+ this.agentNames.delete(agentId);
325
+ }
326
+ alert(agentId, kind, message, severity, details) {
327
+ const state = this.getState(agentId);
328
+ const now = Date.now();
329
+ const lastAlert = state.lastAlertAt.get(kind) ?? 0;
330
+ if (now - lastAlert < COOLDOWN_MS)
331
+ return;
332
+ state.lastAlertAt.set(kind, now);
333
+ const anomaly = {
334
+ id: `anomaly-${++anomalyIdCounter}`,
335
+ agentId,
336
+ agentName: this.agentNames.get(agentId) ?? agentId.slice(0, 10),
337
+ kind,
338
+ message,
339
+ severity,
340
+ timestamp: now,
341
+ details
342
+ };
343
+ this.emit("anomaly", anomaly);
344
+ }
345
+ dispose() {
346
+ if (this.stuckInterval) {
347
+ clearInterval(this.stuckInterval);
348
+ this.stuckInterval = null;
349
+ }
350
+ }
351
+ };
352
+ var MAX_DURATION_SAMPLES = 50;
353
+ var ToolChainTracker = class {
354
+ lastTool = /* @__PURE__ */ new Map();
355
+ transitions = /* @__PURE__ */ new Map();
356
+ toolCounts = /* @__PURE__ */ new Map();
357
+ // Hook-sourced outcome tracking
358
+ toolSuccesses = /* @__PURE__ */ new Map();
359
+ toolFailures = /* @__PURE__ */ new Map();
360
+ toolDurationSamples = /* @__PURE__ */ new Map();
361
+ /** Per-agent pending tool start: agentId → {tool, startTime} */
362
+ pendingStart = /* @__PURE__ */ new Map();
363
+ recordToolUse(agentId, toolName) {
364
+ this.toolCounts.set(toolName, (this.toolCounts.get(toolName) ?? 0) + 1);
365
+ const prev = this.lastTool.get(agentId);
366
+ this.lastTool.set(agentId, toolName);
367
+ if (prev && prev !== toolName) {
368
+ const key = `${prev}\u2192${toolName}`;
369
+ this.transitions.set(key, (this.transitions.get(key) ?? 0) + 1);
370
+ }
371
+ }
372
+ /** Called from hookPreToolUse — records when a tool started for duration tracking */
373
+ recordToolStart(agentId, toolName) {
374
+ this.pendingStart.set(agentId, { tool: toolName, startTime: Date.now() });
375
+ }
376
+ /** Called from hookPostToolUse — records outcome and duration */
377
+ recordToolOutcome(agentId, success) {
378
+ const pending = this.pendingStart.get(agentId);
379
+ if (!pending)
380
+ return;
381
+ this.pendingStart.delete(agentId);
382
+ const { tool, startTime } = pending;
383
+ const duration = Date.now() - startTime;
384
+ if (success) {
385
+ this.toolSuccesses.set(tool, (this.toolSuccesses.get(tool) ?? 0) + 1);
386
+ } else {
387
+ this.toolFailures.set(tool, (this.toolFailures.get(tool) ?? 0) + 1);
388
+ }
389
+ let samples = this.toolDurationSamples.get(tool);
390
+ if (!samples) {
391
+ samples = [];
392
+ this.toolDurationSamples.set(tool, samples);
393
+ }
394
+ samples.push(duration);
395
+ if (samples.length > MAX_DURATION_SAMPLES)
396
+ samples.shift();
397
+ }
398
+ resetAgent(agentId) {
399
+ this.lastTool.delete(agentId);
400
+ this.pendingStart.delete(agentId);
401
+ }
402
+ /** Migrate per-agent state from an old ID to a new canonical ID (used on agent merge). */
403
+ migrateAgent(fromId, toId) {
404
+ const last = this.lastTool.get(fromId);
405
+ if (last !== void 0) {
406
+ if (!this.lastTool.has(toId))
407
+ this.lastTool.set(toId, last);
408
+ this.lastTool.delete(fromId);
409
+ }
410
+ const pending = this.pendingStart.get(fromId);
411
+ if (pending !== void 0) {
412
+ if (!this.pendingStart.has(toId))
413
+ this.pendingStart.set(toId, pending);
414
+ this.pendingStart.delete(fromId);
415
+ }
416
+ }
417
+ reset() {
418
+ this.lastTool.clear();
419
+ this.transitions.clear();
420
+ this.toolCounts.clear();
421
+ this.toolSuccesses.clear();
422
+ this.toolFailures.clear();
423
+ this.toolDurationSamples.clear();
424
+ this.pendingStart.clear();
425
+ }
426
+ hasActiveAgents() {
427
+ return this.lastTool.size > 0;
428
+ }
429
+ getSnapshot() {
430
+ const transitions = [];
431
+ for (const [key, count] of this.transitions) {
432
+ const [from, to] = key.split("\u2192");
433
+ transitions.push({ from, to, count });
434
+ }
435
+ transitions.sort((a, b) => b.count - a.count);
436
+ const toolCounts = {};
437
+ for (const [tool, count] of this.toolCounts)
438
+ toolCounts[tool] = count;
439
+ const toolSuccesses = {};
440
+ for (const [tool, count] of this.toolSuccesses)
441
+ toolSuccesses[tool] = count;
442
+ const toolFailures = {};
443
+ for (const [tool, count] of this.toolFailures)
444
+ toolFailures[tool] = count;
445
+ const toolAvgDuration = {};
446
+ for (const [tool, samples] of this.toolDurationSamples) {
447
+ if (samples.length > 0) {
448
+ toolAvgDuration[tool] = Math.round(samples.reduce((a, b) => a + b, 0) / samples.length);
449
+ }
450
+ }
451
+ return {
452
+ transitions,
453
+ tools: Array.from(this.toolCounts.keys()).sort(),
454
+ toolCounts,
455
+ toolSuccesses,
456
+ toolFailures,
457
+ toolAvgDuration
458
+ };
459
+ }
460
+ };
461
+ var TaskGraphManager = class {
462
+ tasks = /* @__PURE__ */ new Map();
463
+ /** Per-agent-root counters to match Claude's sequential task IDs per team */
464
+ counters = /* @__PURE__ */ new Map();
465
+ /**
466
+ * Build a scoped key from rootSessionId + short task ID.
467
+ * This prevents cross-team ID collisions in the global map.
468
+ */
469
+ scopedKey(root, shortId) {
470
+ return `${root}::${shortId}`;
471
+ }
472
+ /**
473
+ * Process a tool use that may affect the task graph.
474
+ * Returns true if the graph changed.
475
+ */
476
+ processToolUse(agentId, agentName, toolName, toolInput, projectName, rootSessionId) {
477
+ const root = rootSessionId ?? agentId;
478
+ if (toolName === "TaskCreate") {
479
+ return this.handleCreate(agentId, agentName, toolInput, projectName, root);
480
+ }
481
+ if (toolName === "TaskUpdate") {
482
+ return this.handleUpdate(agentId, agentName, toolInput, projectName, root);
483
+ }
484
+ return false;
485
+ }
486
+ handleCreate(agentId, agentName, toolInput, projectName, root) {
487
+ const input = toolInput;
488
+ if (!input)
489
+ return false;
490
+ const count = (this.counters.get(root) ?? 0) + 1;
491
+ this.counters.set(root, count);
492
+ const shortId = String(count);
493
+ const key = this.scopedKey(root, shortId);
494
+ const subject = input.subject ?? `Task ${shortId}`;
495
+ const description = input.description;
496
+ const node = {
497
+ id: shortId,
498
+ subject,
499
+ description,
500
+ status: "pending",
501
+ owner: void 0,
502
+ agentId,
503
+ agentName,
504
+ projectName,
505
+ blocks: [],
506
+ blockedBy: [],
507
+ timestamp: Date.now(),
508
+ _rootKey: key
509
+ };
510
+ this.tasks.set(key, node);
511
+ return true;
512
+ }
513
+ handleUpdate(agentId, agentName, toolInput, projectName, root) {
514
+ const input = toolInput;
515
+ if (!input)
516
+ return false;
517
+ const taskId = input.taskId;
518
+ if (!taskId)
519
+ return false;
520
+ const key = this.scopedKey(root, taskId);
521
+ let task = this.tasks.get(key);
522
+ if (!task) {
523
+ task = {
524
+ id: taskId,
525
+ subject: `Task ${taskId}`,
526
+ status: "pending",
527
+ agentId,
528
+ agentName,
529
+ projectName,
530
+ blocks: [],
531
+ blockedBy: [],
532
+ timestamp: Date.now(),
533
+ _rootKey: key
534
+ };
535
+ this.tasks.set(key, task);
536
+ }
537
+ let changed = false;
538
+ if (input.status !== void 0) {
539
+ const newStatus = input.status;
540
+ if (["pending", "in_progress", "completed", "deleted"].includes(newStatus)) {
541
+ task.status = newStatus;
542
+ changed = true;
543
+ }
544
+ }
545
+ if (input.owner !== void 0) {
546
+ task.owner = input.owner;
547
+ changed = true;
548
+ }
549
+ if (input.subject !== void 0) {
550
+ task.subject = input.subject;
551
+ changed = true;
552
+ }
553
+ if (input.description !== void 0) {
554
+ task.description = input.description;
555
+ changed = true;
556
+ }
557
+ if (input.addBlocks) {
558
+ const blocks = input.addBlocks;
559
+ for (const id of blocks) {
560
+ if (!task.blocks.includes(id)) {
561
+ task.blocks.push(id);
562
+ changed = true;
563
+ }
564
+ const otherKey = this.scopedKey(root, id);
565
+ const other = this.tasks.get(otherKey);
566
+ if (other && !other.blockedBy.includes(taskId)) {
567
+ other.blockedBy.push(taskId);
568
+ changed = true;
569
+ }
570
+ }
571
+ }
572
+ if (input.addBlockedBy) {
573
+ const blockedBy = input.addBlockedBy;
574
+ for (const id of blockedBy) {
575
+ if (!task.blockedBy.includes(id)) {
576
+ task.blockedBy.push(id);
577
+ changed = true;
578
+ }
579
+ const otherKey = this.scopedKey(root, id);
580
+ const other = this.tasks.get(otherKey);
581
+ if (other && !other.blocks.includes(taskId)) {
582
+ other.blocks.push(taskId);
583
+ changed = true;
584
+ }
585
+ }
586
+ }
587
+ return changed;
588
+ }
589
+ /**
590
+ * Mark a task as completed (hook-sourced — TaskCompleted event).
591
+ * Returns true if the status actually changed.
592
+ */
593
+ processTaskCompleted(taskId, root) {
594
+ const key = this.scopedKey(root, taskId);
595
+ const task = this.tasks.get(key);
596
+ if (!task || task.status === "completed")
597
+ return false;
598
+ task.status = "completed";
599
+ return true;
600
+ }
601
+ /**
602
+ * Remove all tasks created by a given agent.
603
+ * Cleans up dependency references from remaining tasks.
604
+ * Returns true if any tasks were removed.
605
+ */
606
+ removeAgentTasks(agentId) {
607
+ const toRemove = /* @__PURE__ */ new Set();
608
+ for (const [key, task] of this.tasks) {
609
+ if (task.agentId === agentId)
610
+ toRemove.add(key);
611
+ }
612
+ if (toRemove.size === 0)
613
+ return false;
614
+ const removedKeys = /* @__PURE__ */ new Set();
615
+ for (const key of toRemove) {
616
+ removedKeys.add(key);
617
+ this.tasks.delete(key);
618
+ }
619
+ for (const [taskKey, task] of this.tasks) {
620
+ const root = taskKey.split("::")[0];
621
+ task.blocks = task.blocks.filter((id) => !removedKeys.has(`${root}::${id}`));
622
+ task.blockedBy = task.blockedBy.filter((id) => !removedKeys.has(`${root}::${id}`));
623
+ }
624
+ return true;
625
+ }
626
+ getSnapshot() {
627
+ const live = Array.from(this.tasks.values()).filter((t) => t.status !== "deleted");
628
+ const rootOf = (key) => key.split("::")[0];
629
+ const shortToRoot = /* @__PURE__ */ new Map();
630
+ for (const t of live) {
631
+ const root = rootOf(t._rootKey);
632
+ if (!shortToRoot.has(root))
633
+ shortToRoot.set(root, /* @__PURE__ */ new Map());
634
+ shortToRoot.get(root).set(t.id, t._rootKey);
635
+ }
636
+ return {
637
+ tasks: live.map((t) => {
638
+ const lookup = shortToRoot.get(rootOf(t._rootKey));
639
+ return {
640
+ ...t,
641
+ blocks: t.blocks.map((id) => lookup?.get(id) ?? id),
642
+ blockedBy: t.blockedBy.map((id) => lookup?.get(id) ?? id)
643
+ };
644
+ })
645
+ };
646
+ }
647
+ };
648
+ function resolveAgentId(sessionToAgent, sessionId) {
649
+ return sessionToAgent.get(sessionId) ?? sessionId;
650
+ }
651
+ function transferAndRemove(deps, sourceId, target, redirectSessionId) {
652
+ const { agents, hiddenAgents, sessionToAgent, toolChainTracker, clearTimers } = deps;
653
+ const source = agents.get(sourceId);
654
+ if (source) {
655
+ target.totalInputTokens += source.totalInputTokens;
656
+ target.totalOutputTokens += source.totalOutputTokens;
657
+ target.cacheReadTokens += source.cacheReadTokens;
658
+ target.cacheCreationTokens += source.cacheCreationTokens;
659
+ }
660
+ agents.delete(sourceId);
661
+ hiddenAgents.delete(sourceId);
662
+ clearTimers(sourceId);
663
+ toolChainTracker.migrateAgent(sourceId, target.id);
664
+ sessionToAgent.set(redirectSessionId, target.id);
665
+ target.isIdle = false;
666
+ target.isDone = false;
667
+ target.lastActivityAt = Date.now();
668
+ }
669
+ function mergeIntoNamed(deps, hiddenId, agentName) {
670
+ const { agents, namedAgentMap } = deps;
671
+ const hidden = agents.get(hiddenId);
672
+ if (!hidden)
673
+ return null;
674
+ const key = `${hidden.rootSessionId}:${agentName}`;
675
+ const existingId = namedAgentMap.get(key);
676
+ if (!existingId || existingId === hiddenId)
677
+ return null;
678
+ const existing = agents.get(existingId);
679
+ if (!existing)
680
+ return null;
681
+ transferAndRemove(deps, hiddenId, existing, hiddenId);
682
+ console.log(`Merged session ${hiddenId.slice(0, 12)}\u2026 into agent "${agentName}" (${existingId.slice(0, 12)}\u2026)`);
683
+ return existing;
684
+ }
685
+ function promoteAgent(deps, agentId) {
686
+ const { agents, hiddenAgents, identityTimers, recordTimeline, emit } = deps;
687
+ hiddenAgents.delete(agentId);
688
+ const idTimer = identityTimers.get(agentId);
689
+ if (idTimer)
690
+ clearTimeout(idTimer);
691
+ identityTimers.delete(agentId);
692
+ const agent = agents.get(agentId);
693
+ if (!agent)
694
+ return;
695
+ console.log(`Promoting hidden agent ${agentId.slice(0, 12)}\u2026 (name: ${agent.agentName ?? "unknown"})`);
696
+ const now = Date.now();
697
+ const spawnEvent = { type: "agent:spawn", agent: { ...agent }, timestamp: now };
698
+ recordTimeline(spawnEvent);
699
+ emit("agent:spawn", spawnEvent);
700
+ const updateEvent = { type: "agent:update", agent: { ...agent }, timestamp: now };
701
+ recordTimeline(updateEvent);
702
+ emit("agent:update", updateEvent);
703
+ }
704
+ function determineRole(_activity, sessionInfo) {
705
+ if (sessionInfo.isSubagent)
706
+ return "subagent";
707
+ return "main";
708
+ }
709
+ function determineRoleForNamed(_parentId, pendingTeam) {
710
+ if (pendingTeam)
711
+ return "team-member";
712
+ return "subagent";
713
+ }
714
+ function findParentId(agents, projectDir) {
715
+ for (const [id, agent] of agents) {
716
+ if (agent.projectPath === projectDir && agent.role === "team-lead") {
717
+ return id;
718
+ }
719
+ }
720
+ for (const [id, agent] of agents) {
721
+ if (agent.projectPath === projectDir && agent.role === "main") {
722
+ return id;
723
+ }
724
+ }
725
+ for (const [id, agent] of agents) {
726
+ if (agent.projectPath === projectDir && agent.role !== "subagent" && agent.role !== "team-member") {
727
+ return id;
728
+ }
729
+ }
730
+ return null;
731
+ }
732
+ function getParentTeamName(agents, rootSessionId) {
733
+ for (const agent of agents.values()) {
734
+ if (agent.rootSessionId === rootSessionId && agent.teamName) {
735
+ return agent.teamName;
736
+ }
737
+ }
738
+ return null;
739
+ }
740
+ var branchCache = /* @__PURE__ */ new Map();
741
+ var CACHE_TTL = 3e4;
742
+ var MAX_CACHE_SIZE = 100;
743
+ function getGitBranch(projectPath) {
744
+ const cached = branchCache.get(projectPath);
745
+ if (cached && Date.now() - cached.ts < CACHE_TTL)
746
+ return cached.branch;
747
+ try {
748
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
749
+ cwd: projectPath,
750
+ timeout: 5e3,
751
+ encoding: "utf-8",
752
+ stdio: ["ignore", "pipe", "ignore"]
753
+ }).trim() || null;
754
+ branchCache.set(projectPath, { branch, ts: Date.now() });
755
+ pruneCache();
756
+ return branch;
757
+ } catch {
758
+ branchCache.set(projectPath, { branch: null, ts: Date.now() });
759
+ return null;
760
+ }
761
+ }
762
+ function pruneCache() {
763
+ if (branchCache.size <= MAX_CACHE_SIZE)
764
+ return;
765
+ const entries = [...branchCache.entries()].sort((a, b) => a[1].ts - b[1].ts);
766
+ const toRemove = entries.slice(0, branchCache.size - MAX_CACHE_SIZE);
767
+ for (const [key] of toRemove)
768
+ branchCache.delete(key);
769
+ }
770
+ var LONG_RUNNING_TOOLS = /* @__PURE__ */ new Set([
771
+ "Bash",
772
+ "Agent",
773
+ "WebFetch",
774
+ "WebSearch",
775
+ // Tools that block waiting for user input
776
+ "AskUserQuestion",
777
+ // Reasoning / extended thinking pseudo-tool (emitted for OpenCode reasoning parts)
778
+ "thinking",
779
+ // Browser/playwright tools that wait for navigation or network
780
+ "mcp__playwright__browser_navigate",
781
+ "mcp__playwright__browser_wait_for",
782
+ "mcp__playwright__browser_run_code",
783
+ "mcp__chrome-devtools__navigate_page",
784
+ "mcp__chrome-devtools__wait_for",
785
+ "mcp__chrome-devtools__evaluate_script",
786
+ "mcp__chrome-devtools__performance_start_trace",
787
+ "mcp__chrome-devtools__performance_stop_trace"
788
+ ]);
789
+ var USER_BLOCKING_TOOLS = /* @__PURE__ */ new Set([
790
+ "AskUserQuestion"
791
+ ]);
792
+ function processToolUseActivity(deps, agent, activity, now) {
793
+ const { pendingTool, anomalyDetector, toolChainTracker, taskGraphManager, namedAgentMap, addHistory, summarizeToolInput, queuePendingInfo, queueRecipient, emit } = deps;
794
+ const agentId = agent.id;
795
+ const toolName = activity.toolName ?? "";
796
+ if (LONG_RUNNING_TOOLS.has(toolName)) {
797
+ pendingTool.add(agentId);
798
+ } else {
799
+ pendingTool.delete(agentId);
800
+ }
801
+ agent.isWaitingForUser = USER_BLOCKING_TOOLS.has(toolName);
802
+ const prevZone = agent.currentZone;
803
+ agent.currentTool = activity.toolName ?? null;
804
+ agent.currentActivity = summarizeToolInput(activity.toolInput) || null;
805
+ agent.currentZone = getZoneForTool(activity.toolName ?? "");
806
+ agent.toolUseCount++;
807
+ anomalyDetector.setAgentName(agentId, agent.agentName ?? agent.projectName ?? agentId.slice(0, 10));
808
+ anomalyDetector.checkToolUse(agentId, toolName);
809
+ toolChainTracker.recordToolUse(agentId, toolName);
810
+ if (toolName === "TaskCreate" || toolName === "TaskUpdate") {
811
+ const graphChanged = taskGraphManager.processToolUse(agentId, agent.agentName ?? agentId.slice(0, 10), toolName, activity.toolInput, agent.projectName, agent.rootSessionId);
812
+ if (graphChanged) {
813
+ emit("taskgraph:changed", { data: taskGraphManager.getSnapshot(), timestamp: Date.now() });
814
+ }
815
+ }
816
+ if (agent.projectPath) {
817
+ agent.gitBranch = getGitBranch(agent.projectPath);
818
+ }
819
+ if (activity.toolInput) {
820
+ const input = activity.toolInput;
821
+ const filePath = input.file_path;
822
+ if (filePath) {
823
+ agent.recentFiles = [filePath, ...agent.recentFiles.filter((f) => f !== filePath)].slice(0, 10);
824
+ }
825
+ }
826
+ if (activity.toolName === "EnterPlanMode") {
827
+ agent.isPlanning = true;
828
+ } else if (activity.toolName === "ExitPlanMode") {
829
+ agent.isPlanning = false;
830
+ }
831
+ if (activity.toolName === "TeamCreate" && activity.toolInput) {
832
+ agent.teamName = activity.toolInput.team_name ?? null;
833
+ agent.role = "team-lead";
834
+ if (!agent.agentName) {
835
+ agent.agentName = "team-lead";
836
+ }
837
+ namedAgentMap.set(`${agent.rootSessionId}:team-lead`, agentId);
838
+ }
839
+ if (activity.toolName === "SendMessage") {
840
+ agent.currentZone = "messaging";
841
+ if (activity.toolInput) {
842
+ const input = activity.toolInput;
843
+ const recipient = input.recipient;
844
+ agent.messageTarget = recipient ?? null;
845
+ const senderIdentity = agent.agentName || (agent.role === "team-lead" ? "team-lead" : null);
846
+ if (recipient && senderIdentity) {
847
+ queueRecipient(agent.rootSessionId, senderIdentity, recipient);
848
+ }
849
+ }
850
+ } else {
851
+ agent.messageTarget = null;
852
+ }
853
+ if (activity.toolName === "Agent" && activity.toolInput) {
854
+ const input = activity.toolInput;
855
+ const desc = input.description ?? input.prompt ?? "";
856
+ const name = input.name ?? null;
857
+ const teamName = input.team_name ?? null;
858
+ queuePendingInfo(agentId, {
859
+ name,
860
+ task: desc ? desc.length > 80 ? desc.slice(0, 77) + "..." : desc : null,
861
+ team: teamName
862
+ });
863
+ }
864
+ let diffData;
865
+ if (activity.toolName === "Edit" && activity.toolInput) {
866
+ const input = activity.toolInput;
867
+ const fp = input.file_path ?? "";
868
+ const oldStr = input.old_string ?? "";
869
+ const newStr = input.new_string ?? "";
870
+ if (fp && (oldStr || newStr)) {
871
+ diffData = {
872
+ filePath: fp,
873
+ oldText: oldStr.length > 500 ? oldStr.slice(0, 500) + "..." : oldStr,
874
+ newText: newStr.length > 500 ? newStr.slice(0, 500) + "..." : newStr
875
+ };
876
+ }
877
+ }
878
+ if (diffData) {
879
+ agent.recentDiffs.unshift({ ...diffData, timestamp: now });
880
+ if (agent.recentDiffs.length > 10)
881
+ agent.recentDiffs.pop();
882
+ }
883
+ addHistory(agentId, {
884
+ timestamp: now,
885
+ kind: "tool",
886
+ tool: activity.toolName ?? void 0,
887
+ toolArgs: summarizeToolInput(activity.toolInput),
888
+ zone: agent.currentZone,
889
+ diff: diffData
890
+ });
891
+ if (prevZone !== agent.currentZone) {
892
+ addHistory(agentId, {
893
+ timestamp: now,
894
+ kind: "zone-change",
895
+ zone: agent.currentZone,
896
+ prevZone
897
+ });
898
+ }
899
+ if (activity.inputTokens !== void 0 || activity.outputTokens !== void 0) {
900
+ agent.totalInputTokens += activity.inputTokens ?? 0;
901
+ agent.totalOutputTokens += activity.outputTokens ?? 0;
902
+ agent.cacheReadTokens += activity.cacheReadTokens ?? 0;
903
+ agent.cacheCreationTokens += activity.cacheCreationTokens ?? 0;
904
+ if (activity.inputTokens !== void 0) {
905
+ agent.contextTokens = activity.inputTokens + (activity.cacheReadTokens ?? 0);
906
+ agent.contextCacheTokens = activity.cacheReadTokens ?? 0;
907
+ }
908
+ }
909
+ }
910
+ function processToolActivity(deps, agent, activity, now) {
911
+ const { pendingTool, anomalyDetector, addHistory } = deps;
912
+ const agentId = agent.id;
913
+ switch (activity.type) {
914
+ case "tool_use": {
915
+ processToolUseActivity(deps, agent, activity, now);
916
+ break;
917
+ }
918
+ case "text":
919
+ pendingTool.delete(agentId);
920
+ agent.isWaitingForUser = false;
921
+ if (activity.text) {
922
+ agent.speechText = activity.text;
923
+ agent.currentActivity = activity.text;
924
+ if (!agent.taskDescription) {
925
+ agent.taskDescription = activity.text;
926
+ }
927
+ addHistory(agentId, {
928
+ timestamp: now,
929
+ kind: "text",
930
+ text: activity.text
931
+ });
932
+ }
933
+ if (activity.inputTokens !== void 0 || activity.outputTokens !== void 0) {
934
+ agent.totalInputTokens += activity.inputTokens ?? 0;
935
+ agent.totalOutputTokens += activity.outputTokens ?? 0;
936
+ agent.cacheReadTokens += activity.cacheReadTokens ?? 0;
937
+ agent.cacheCreationTokens += activity.cacheCreationTokens ?? 0;
938
+ if (activity.inputTokens !== void 0) {
939
+ agent.contextTokens = activity.inputTokens + (activity.cacheReadTokens ?? 0);
940
+ agent.contextCacheTokens = activity.cacheReadTokens ?? 0;
941
+ }
942
+ }
943
+ break;
944
+ case "token_usage":
945
+ pendingTool.delete(agentId);
946
+ agent.isWaitingForUser = false;
947
+ agent.totalInputTokens += activity.inputTokens ?? 0;
948
+ agent.totalOutputTokens += activity.outputTokens ?? 0;
949
+ agent.cacheReadTokens += activity.cacheReadTokens ?? 0;
950
+ agent.cacheCreationTokens += activity.cacheCreationTokens ?? 0;
951
+ if (activity.inputTokens !== void 0) {
952
+ agent.contextTokens = activity.inputTokens + (activity.cacheReadTokens ?? 0);
953
+ agent.contextCacheTokens = activity.cacheReadTokens ?? 0;
954
+ }
955
+ anomalyDetector.checkTokenUsage(agentId, activity.inputTokens ?? 0, activity.outputTokens ?? 0);
956
+ addHistory(agentId, {
957
+ timestamp: now,
958
+ kind: "tokens",
959
+ inputTokens: activity.inputTokens ?? 0,
960
+ outputTokens: activity.outputTokens ?? 0
961
+ });
962
+ break;
963
+ }
964
+ }
965
+ var MAX_HISTORY_PER_AGENT = 500;
966
+ var MAX_HISTORY_AGE_MS = 30 * 60 * 1e3;
967
+ var MAX_TIMELINE_EVENTS = 5e3;
968
+ var IDENTITY_TIMEOUT_MS = 15e3;
969
+ var AgentStateManager = class extends EventEmitter2 {
970
+ agents = /* @__PURE__ */ new Map();
971
+ idleTimers = /* @__PURE__ */ new Map();
972
+ shutdownTimers = /* @__PURE__ */ new Map();
973
+ /** Agents with a pending tool call (waiting for result) */
974
+ pendingTool = /* @__PURE__ */ new Set();
975
+ activityHistory = /* @__PURE__ */ new Map();
976
+ timelineBuffer = [];
977
+ /** Compound queue of subagent metadata from Agent tool calls, keyed by parent agent ID */
978
+ pendingSubagentInfo = /* @__PURE__ */ new Map();
979
+ /** Maps sessionId → canonical agent ID (for merging multiple sessions into one agent) */
980
+ sessionToAgent = /* @__PURE__ */ new Map();
981
+ /** Maps rootSessionId:agentName → canonical agent ID (scoped per terminal session) */
982
+ namedAgentMap = /* @__PURE__ */ new Map();
983
+ /** Agents that are hidden pending identity confirmation — no events emitted */
984
+ hiddenAgents = /* @__PURE__ */ new Set();
985
+ /** Timers to promote hidden agents after timeout */
986
+ identityTimers = /* @__PURE__ */ new Map();
987
+ /** Queued recipient names from SendMessage calls: rootSessionId → [{sender, recipient}] */
988
+ pendingRecipients = /* @__PURE__ */ new Map();
989
+ /** Anomaly detection for token spikes, retry loops, stuck agents */
990
+ anomalyDetector = new AnomalyDetector();
991
+ /** Tool chain transition tracking */
992
+ toolChainTracker = new ToolChainTracker();
993
+ /** Task dependency graph tracking */
994
+ taskGraphManager = new TaskGraphManager();
995
+ constructor() {
996
+ super();
997
+ this.anomalyDetector.startStuckDetection(() => Array.from(this.agents.values()).filter((a) => !this.hiddenAgents.has(a.id)).map((a) => ({ id: a.id, lastActivityAt: a.lastActivityAt, isIdle: a.isIdle })));
998
+ }
999
+ getAll() {
1000
+ return Array.from(this.agents.values()).filter((a) => !this.hiddenAgents.has(a.id));
1001
+ }
1002
+ get(id) {
1003
+ return this.agents.get(id);
1004
+ }
1005
+ /** Flush pending subagent/recipient queues — call after initial replay to prevent stale matches */
1006
+ flushPendingQueues() {
1007
+ const infoCount = Array.from(this.pendingSubagentInfo.values()).reduce((n, q) => n + q.length, 0);
1008
+ const recipientCount = Array.from(this.pendingRecipients.values()).reduce((n, q) => n + q.length, 0);
1009
+ this.pendingSubagentInfo.clear();
1010
+ this.pendingRecipients.clear();
1011
+ if (infoCount + recipientCount > 0) {
1012
+ console.log(`Flushed ${infoCount} pending subagent infos, ${recipientCount} pending recipients from replay`);
1013
+ }
1014
+ }
1015
+ getHistory(agentId) {
1016
+ return this.activityHistory.get(agentId) ?? [];
1017
+ }
1018
+ getTimeline() {
1019
+ return this.timelineBuffer;
1020
+ }
1021
+ getToolChainSnapshot() {
1022
+ return this.toolChainTracker.getSnapshot();
1023
+ }
1024
+ getTaskGraphSnapshot() {
1025
+ return this.taskGraphManager.getSnapshot();
1026
+ }
1027
+ /** Check if any named agents exist for a given root session */
1028
+ hasNamedAgentsForRoot(rootSessionId) {
1029
+ for (const key of this.namedAgentMap.keys()) {
1030
+ if (key.startsWith(rootSessionId + ":"))
1031
+ return true;
1032
+ }
1033
+ return false;
1034
+ }
1035
+ recordTimeline(event) {
1036
+ this.timelineBuffer.push({
1037
+ type: event.type,
1038
+ agent: { ...event.agent },
1039
+ timestamp: event.timestamp
1040
+ });
1041
+ const cutoff = Date.now() - MAX_HISTORY_AGE_MS;
1042
+ while (this.timelineBuffer.length > 0 && this.timelineBuffer[0].timestamp < cutoff) {
1043
+ this.timelineBuffer.shift();
1044
+ }
1045
+ while (this.timelineBuffer.length > MAX_TIMELINE_EVENTS) {
1046
+ this.timelineBuffer.shift();
1047
+ }
1048
+ }
1049
+ addHistory(agentId, entry) {
1050
+ let entries = this.activityHistory.get(agentId);
1051
+ if (!entries) {
1052
+ entries = [];
1053
+ this.activityHistory.set(agentId, entries);
1054
+ }
1055
+ entries.push(entry);
1056
+ const cutoff = Date.now() - MAX_HISTORY_AGE_MS;
1057
+ while (entries.length > 0 && entries[0].timestamp < cutoff) {
1058
+ entries.shift();
1059
+ }
1060
+ while (entries.length > MAX_HISTORY_PER_AGENT) {
1061
+ entries.shift();
1062
+ }
1063
+ }
1064
+ summarizeToolInput(input) {
1065
+ if (!input)
1066
+ return "";
1067
+ try {
1068
+ const obj = input;
1069
+ const summary = obj.command ?? obj.file_path ?? obj.pattern ?? obj.query ?? obj.url ?? obj.content;
1070
+ if (typeof summary === "string") {
1071
+ return summary.length > 120 ? summary.slice(0, 117) + "..." : summary;
1072
+ }
1073
+ const json = JSON.stringify(input);
1074
+ return json.length > 120 ? json.slice(0, 117) + "..." : json;
1075
+ } catch {
1076
+ return "";
1077
+ }
1078
+ }
1079
+ /**
1080
+ * Resolve a sessionId to its canonical agent ID.
1081
+ */
1082
+ resolveAgentId(sessionId) {
1083
+ return resolveAgentId(this.sessionToAgent, sessionId);
1084
+ }
1085
+ /**
1086
+ * Compute the rootSessionId for a new agent.
1087
+ * - Main agents are their own root.
1088
+ * - Subagents inherit from their parent (or fallback to parentId/own sessionId).
1089
+ */
1090
+ computeRootSessionId(sessionId, parentId, sessionInfo) {
1091
+ if (!sessionInfo.isSubagent)
1092
+ return sessionId;
1093
+ if (parentId) {
1094
+ const parent = this.agents.get(parentId);
1095
+ return parent?.rootSessionId ?? parentId;
1096
+ }
1097
+ return sessionId;
1098
+ }
1099
+ /** Build the deps object required by identity-manager functions. */
1100
+ get identityDeps() {
1101
+ return {
1102
+ agents: this.agents,
1103
+ hiddenAgents: this.hiddenAgents,
1104
+ sessionToAgent: this.sessionToAgent,
1105
+ namedAgentMap: this.namedAgentMap,
1106
+ identityTimers: this.identityTimers,
1107
+ toolChainTracker: this.toolChainTracker,
1108
+ clearTimers: (id) => this.clearTimers(id),
1109
+ recordTimeline: (e) => this.recordTimeline(e),
1110
+ emit: (event, ...args) => this.emit(event, ...args)
1111
+ };
1112
+ }
1113
+ /** Build the deps object required by activity-processor functions. */
1114
+ get activityDeps() {
1115
+ return {
1116
+ pendingTool: this.pendingTool,
1117
+ anomalyDetector: this.anomalyDetector,
1118
+ toolChainTracker: this.toolChainTracker,
1119
+ taskGraphManager: this.taskGraphManager,
1120
+ namedAgentMap: this.namedAgentMap,
1121
+ addHistory: (id, entry) => this.addHistory(id, entry),
1122
+ summarizeToolInput: (input) => this.summarizeToolInput(input),
1123
+ queuePendingInfo: (parentId, info) => this.queuePendingInfo(parentId, info),
1124
+ queueRecipient: (rootSessionId, sender, recipient) => this.queueRecipient(rootSessionId, sender, recipient),
1125
+ emit: (event, ...args) => this.emit(event, ...args)
1126
+ };
1127
+ }
1128
+ /**
1129
+ * Called when ANY new bytes appear in a session's JSONL file,
1130
+ * even if the parser doesn't extract a meaningful activity.
1131
+ * This keeps the agent alive during long-running tool executions
1132
+ * (e.g. Bash commands) where the tool result writes to the file
1133
+ * but doesn't produce a ParsedActivity.
1134
+ */
1135
+ heartbeat(sessionId) {
1136
+ const canonicalId = this.resolveAgentId(sessionId);
1137
+ const agent = this.agents.get(canonicalId);
1138
+ if (!agent || this.hiddenAgents.has(canonicalId))
1139
+ return;
1140
+ if (this.pendingTool.has(canonicalId)) {
1141
+ agent.lastActivityAt = Date.now();
1142
+ this.resetIdleTimer(canonicalId);
1143
+ }
1144
+ }
1145
+ processMessage(sessionId, activity, sessionInfo) {
1146
+ const canonicalId = this.resolveAgentId(sessionId);
1147
+ if (canonicalId !== sessionId) {
1148
+ const agent2 = this.agents.get(canonicalId);
1149
+ if (agent2) {
1150
+ this.applyActivity(agent2, activity, Date.now(), sessionInfo);
1151
+ return;
1152
+ }
1153
+ }
1154
+ let agent = this.agents.get(canonicalId);
1155
+ const now = Date.now();
1156
+ if (agent && this.hiddenAgents.has(canonicalId)) {
1157
+ let discoveredName = activity.agentName ?? null;
1158
+ if (!discoveredName && activity.messageSender) {
1159
+ discoveredName = this.popRecipientBySender(agent.rootSessionId, activity.messageSender);
1160
+ }
1161
+ if (discoveredName) {
1162
+ const merged = mergeIntoNamed(this.identityDeps, canonicalId, discoveredName);
1163
+ if (merged) {
1164
+ this.applyActivity(merged, activity, now, sessionInfo);
1165
+ return;
1166
+ }
1167
+ agent.agentName = discoveredName;
1168
+ const role = determineRoleForNamed(agent.parentId, null);
1169
+ agent.role = role;
1170
+ agent.teamName = role === "team-member" ? agent.teamName || getParentTeamName(this.agents, agent.rootSessionId) : null;
1171
+ this.namedAgentMap.set(`${agent.rootSessionId}:${discoveredName}`, canonicalId);
1172
+ promoteAgent(this.identityDeps, canonicalId);
1173
+ this.applyActivity(agent, activity, now, sessionInfo);
1174
+ return;
1175
+ }
1176
+ this.applyActivitySilent(agent, activity, now, sessionInfo);
1177
+ return;
1178
+ }
1179
+ if (agent && activity.agentName && !agent.agentName) {
1180
+ const key = `${agent.rootSessionId}:${activity.agentName}`;
1181
+ const existingId = this.namedAgentMap.get(key);
1182
+ if (existingId && existingId !== canonicalId) {
1183
+ const existing = this.agents.get(existingId);
1184
+ if (existing) {
1185
+ transferAndRemove(this.identityDeps, canonicalId, existing, sessionId);
1186
+ this.emit("agent:shutdown", {
1187
+ type: "agent:shutdown",
1188
+ agent: { id: canonicalId },
1189
+ timestamp: now
1190
+ });
1191
+ console.log(`Late-merged agent ${canonicalId.slice(0, 12)}\u2026 into "${activity.agentName}" (${existingId.slice(0, 12)}\u2026)`);
1192
+ this.applyActivity(existing, activity, now, sessionInfo);
1193
+ return;
1194
+ }
1195
+ }
1196
+ agent.agentName = activity.agentName;
1197
+ const role = determineRoleForNamed(agent.parentId, null);
1198
+ agent.role = role;
1199
+ agent.teamName = role === "team-member" ? agent.teamName || getParentTeamName(this.agents, agent.rootSessionId) : agent.teamName;
1200
+ this.namedAgentMap.set(key, canonicalId);
1201
+ console.log(`Agent ${canonicalId.slice(0, 12)}\u2026 identified as "${activity.agentName}"`);
1202
+ }
1203
+ if (!agent) {
1204
+ const parentId = sessionInfo.isSubagent ? sessionInfo.parentSessionId ? this.resolveAgentId(sessionInfo.parentSessionId) : findParentId(this.agents, sessionInfo.projectDir) : null;
1205
+ const rootSessionId = this.computeRootSessionId(sessionId, parentId, sessionInfo);
1206
+ const pendingInfo = parentId ? this.popPendingInfo(parentId) : null;
1207
+ const taskDescription = pendingInfo?.task ?? null;
1208
+ const pendingTeam = pendingInfo?.team ?? null;
1209
+ let agentName = pendingInfo?.name ?? activity.agentName ?? null;
1210
+ if (!agentName && activity.messageSender) {
1211
+ agentName = this.popRecipientBySender(rootSessionId, activity.messageSender);
1212
+ }
1213
+ if (agentName) {
1214
+ const key = `${rootSessionId}:${agentName}`;
1215
+ const existingId = this.namedAgentMap.get(key);
1216
+ if (existingId) {
1217
+ const existing = this.agents.get(existingId);
1218
+ if (existing) {
1219
+ this.sessionToAgent.set(sessionId, existingId);
1220
+ existing.isIdle = false;
1221
+ existing.isDone = false;
1222
+ existing.lastActivityAt = now;
1223
+ console.log(`Immediate merge: session ${sessionId.slice(0, 12)}\u2026 \u2192 agent "${agentName}" (${existingId.slice(0, 12)}\u2026)`);
1224
+ this.applyActivity(existing, activity, now, sessionInfo);
1225
+ return;
1226
+ }
1227
+ }
1228
+ }
1229
+ const role = agentName ? determineRoleForNamed(parentId, pendingTeam) : determineRole(activity, sessionInfo);
1230
+ const teamName = role === "team-member" ? pendingTeam || getParentTeamName(this.agents, rootSessionId) : null;
1231
+ agent = {
1232
+ id: sessionId,
1233
+ sessionId,
1234
+ rootSessionId,
1235
+ projectPath: sessionInfo.projectPath,
1236
+ projectName: sessionInfo.projectName,
1237
+ agentName,
1238
+ role,
1239
+ parentId,
1240
+ teamName,
1241
+ currentZone: "spawn",
1242
+ currentTool: null,
1243
+ currentActivity: null,
1244
+ messageTarget: null,
1245
+ taskDescription,
1246
+ speechText: null,
1247
+ lastActivityAt: now,
1248
+ spawnedAt: now,
1249
+ isIdle: false,
1250
+ isDone: false,
1251
+ isPlanning: false,
1252
+ isWaitingForUser: false,
1253
+ phase: "running",
1254
+ lastToolOutcome: null,
1255
+ totalInputTokens: 0,
1256
+ totalOutputTokens: 0,
1257
+ cacheReadTokens: 0,
1258
+ cacheCreationTokens: 0,
1259
+ contextTokens: 0,
1260
+ contextCacheTokens: 0,
1261
+ model: activity.model ?? null,
1262
+ colorIndex: getProjectColorIndex(sessionId),
1263
+ toolUseCount: 0,
1264
+ gitBranch: null,
1265
+ recentFiles: [],
1266
+ recentDiffs: []
1267
+ };
1268
+ this.agents.set(sessionId, agent);
1269
+ this.sessionToAgent.set(sessionId, sessionId);
1270
+ const shouldHide = sessionInfo.isSubagent && !agentName && this.hasNamedAgentsForRoot(rootSessionId);
1271
+ if (shouldHide) {
1272
+ this.hiddenAgents.add(sessionId);
1273
+ console.log(`Hiding new session ${sessionId.slice(0, 12)}\u2026 (pending identity)`);
1274
+ const timer = setTimeout(() => {
1275
+ if (this.hiddenAgents.has(sessionId)) {
1276
+ console.log(`Identity timeout for ${sessionId.slice(0, 12)}\u2026 \u2014 promoting with fallback name`);
1277
+ promoteAgent(this.identityDeps, sessionId);
1278
+ }
1279
+ }, IDENTITY_TIMEOUT_MS);
1280
+ this.identityTimers.set(sessionId, timer);
1281
+ this.applyActivitySilent(agent, activity, now, sessionInfo);
1282
+ return;
1283
+ }
1284
+ if (agentName) {
1285
+ this.namedAgentMap.set(`${rootSessionId}:${agentName}`, sessionId);
1286
+ }
1287
+ this.addHistory(sessionId, { timestamp: now, kind: "spawn", zone: "spawn" });
1288
+ const spawnEvent = { type: "agent:spawn", agent: { ...agent }, timestamp: now };
1289
+ this.recordTimeline(spawnEvent);
1290
+ this.emit("agent:spawn", spawnEvent);
1291
+ }
1292
+ this.applyActivity(agent, activity, now, sessionInfo);
1293
+ }
1294
+ /** Apply activity and emit update event (for visible agents) */
1295
+ applyActivity(agent, activity, now, sessionInfo) {
1296
+ this.mutateAgentState(agent, activity, now, sessionInfo);
1297
+ this.resetIdleTimer(agent.id);
1298
+ const updateEvent = { type: "agent:update", agent: { ...agent }, timestamp: now };
1299
+ this.recordTimeline(updateEvent);
1300
+ this.emit("agent:update", updateEvent);
1301
+ }
1302
+ /** Apply activity silently — no events emitted (for hidden agents) */
1303
+ applyActivitySilent(agent, activity, now, sessionInfo) {
1304
+ this.mutateAgentState(agent, activity, now, sessionInfo);
1305
+ }
1306
+ /** Mutate agent state based on activity (shared between silent and loud paths) */
1307
+ mutateAgentState(agent, activity, now, _sessionInfo) {
1308
+ agent.lastActivityAt = now;
1309
+ agent.isIdle = false;
1310
+ agent.isDone = false;
1311
+ if (agent.phase === "idle")
1312
+ agent.phase = "running";
1313
+ if (activity.model) {
1314
+ agent.model = activity.model;
1315
+ }
1316
+ processToolActivity(this.activityDeps, agent, activity, now);
1317
+ }
1318
+ queuePendingInfo(parentId, info) {
1319
+ let queue = this.pendingSubagentInfo.get(parentId);
1320
+ if (!queue) {
1321
+ queue = [];
1322
+ this.pendingSubagentInfo.set(parentId, queue);
1323
+ }
1324
+ queue.push(info);
1325
+ }
1326
+ popPendingInfo(parentId) {
1327
+ const queue = this.pendingSubagentInfo.get(parentId);
1328
+ if (!queue || queue.length === 0)
1329
+ return null;
1330
+ return queue.shift();
1331
+ }
1332
+ queueRecipient(rootSessionId, sender, recipient) {
1333
+ let queue = this.pendingRecipients.get(rootSessionId);
1334
+ if (!queue) {
1335
+ queue = [];
1336
+ this.pendingRecipients.set(rootSessionId, queue);
1337
+ }
1338
+ queue.push({ sender, recipient });
1339
+ if (queue.length > 50)
1340
+ queue.shift();
1341
+ }
1342
+ /** Pop a queued recipient name matching a specific sender */
1343
+ popRecipientBySender(rootSessionId, sender) {
1344
+ const queue = this.pendingRecipients.get(rootSessionId);
1345
+ if (!queue)
1346
+ return null;
1347
+ const idx = queue.findIndex((e) => e.sender === sender);
1348
+ if (idx === -1)
1349
+ return null;
1350
+ const entry = queue.splice(idx, 1)[0];
1351
+ return entry.recipient;
1352
+ }
1353
+ clearTimers(agentId) {
1354
+ const idle = this.idleTimers.get(agentId);
1355
+ if (idle)
1356
+ clearTimeout(idle);
1357
+ this.idleTimers.delete(agentId);
1358
+ const shutdown = this.shutdownTimers.get(agentId);
1359
+ if (shutdown)
1360
+ clearTimeout(shutdown);
1361
+ this.shutdownTimers.delete(agentId);
1362
+ const identity = this.identityTimers.get(agentId);
1363
+ if (identity)
1364
+ clearTimeout(identity);
1365
+ this.identityTimers.delete(agentId);
1366
+ }
1367
+ resetIdleTimer(agentId) {
1368
+ const existing = this.idleTimers.get(agentId);
1369
+ if (existing)
1370
+ clearTimeout(existing);
1371
+ const existingShutdown = this.shutdownTimers.get(agentId);
1372
+ if (existingShutdown)
1373
+ clearTimeout(existingShutdown);
1374
+ const timer = setTimeout(() => {
1375
+ const agent = this.agents.get(agentId);
1376
+ if (agent && !this.hiddenAgents.has(agentId)) {
1377
+ if (this.pendingTool.has(agentId)) {
1378
+ this.resetIdleTimer(agentId);
1379
+ return;
1380
+ }
1381
+ agent.isIdle = true;
1382
+ agent.isPlanning = false;
1383
+ agent.isWaitingForUser = false;
1384
+ agent.phase = "idle";
1385
+ agent.currentZone = "idle";
1386
+ agent.currentTool = null;
1387
+ agent.currentActivity = null;
1388
+ agent.speechText = null;
1389
+ const ts = Date.now();
1390
+ this.addHistory(agentId, { timestamp: ts, kind: "idle", zone: "idle" });
1391
+ const idleEvent = {
1392
+ type: "agent:idle",
1393
+ agent: { ...agent },
1394
+ timestamp: ts
1395
+ };
1396
+ this.recordTimeline(idleEvent);
1397
+ this.emit("agent:idle", idleEvent);
1398
+ this.startShutdownTimer(agentId);
1399
+ }
1400
+ }, config.idleTimeoutMs);
1401
+ this.idleTimers.set(agentId, timer);
1402
+ }
1403
+ startShutdownTimer(agentId) {
1404
+ const existing = this.shutdownTimers.get(agentId);
1405
+ if (existing)
1406
+ clearTimeout(existing);
1407
+ const timer = setTimeout(() => {
1408
+ const agent = this.agents.get(agentId);
1409
+ if (agent && agent.isIdle && !agent.isDone) {
1410
+ agent.isDone = true;
1411
+ console.log(`Agent marked done: ${agentId} (idle for ${config.shutdownTimeoutMs / 1e3}s)`);
1412
+ const ts = Date.now();
1413
+ const updateEvent = { type: "agent:update", agent: { ...agent }, timestamp: ts };
1414
+ this.recordTimeline(updateEvent);
1415
+ this.emit("agent:update", updateEvent);
1416
+ }
1417
+ }, config.shutdownTimeoutMs);
1418
+ this.shutdownTimers.set(agentId, timer);
1419
+ }
1420
+ removeDone() {
1421
+ const removed = [];
1422
+ const doneIds = [...this.agents.entries()].filter(([, a]) => a.isDone).map(([id]) => id);
1423
+ for (const id of doneIds) {
1424
+ if (this.agents.has(id)) {
1425
+ removed.push(id);
1426
+ this.shutdown(id);
1427
+ }
1428
+ }
1429
+ return removed;
1430
+ }
1431
+ shutdown(sessionId) {
1432
+ const childIds = [...this.agents.entries()].filter(([id, a]) => a.parentId === sessionId && id !== sessionId).map(([id]) => id);
1433
+ this.clearTimers(sessionId);
1434
+ this.hiddenAgents.delete(sessionId);
1435
+ this.pendingTool.delete(sessionId);
1436
+ const ts = Date.now();
1437
+ this.addHistory(sessionId, { timestamp: ts, kind: "shutdown" });
1438
+ const agent = this.agents.get(sessionId);
1439
+ const shutdownEvent = {
1440
+ type: "agent:shutdown",
1441
+ agent: agent ? { ...agent } : { id: sessionId },
1442
+ timestamp: ts
1443
+ };
1444
+ this.recordTimeline(shutdownEvent);
1445
+ this.emit("agent:shutdown", shutdownEvent);
1446
+ if (agent?.agentName) {
1447
+ const key = `${agent.rootSessionId}:${agent.agentName}`;
1448
+ if (this.namedAgentMap.get(key) === sessionId) {
1449
+ this.namedAgentMap.delete(key);
1450
+ }
1451
+ }
1452
+ for (const [sid, target] of this.sessionToAgent) {
1453
+ if (target === sessionId) {
1454
+ this.sessionToAgent.delete(sid);
1455
+ }
1456
+ }
1457
+ this.agents.delete(sessionId);
1458
+ this.activityHistory.delete(sessionId);
1459
+ this.pendingSubagentInfo.delete(sessionId);
1460
+ this.anomalyDetector.removeAgent(sessionId);
1461
+ this.toolChainTracker.resetAgent(sessionId);
1462
+ if (!this.toolChainTracker.hasActiveAgents() && this.agents.size === 0) {
1463
+ this.toolChainTracker.reset();
1464
+ }
1465
+ this.emit("toolchain:changed", { data: this.toolChainTracker.getSnapshot(), timestamp: Date.now() });
1466
+ if (agent?.rootSessionId) {
1467
+ const rootId = agent.rootSessionId;
1468
+ const hasRelated = [...this.agents.values()].some((a) => a.rootSessionId === rootId);
1469
+ if (!hasRelated) {
1470
+ this.pendingRecipients.delete(rootId);
1471
+ }
1472
+ }
1473
+ if (this.taskGraphManager.removeAgentTasks(sessionId)) {
1474
+ this.emit("taskgraph:changed", { data: this.taskGraphManager.getSnapshot(), timestamp: Date.now() });
1475
+ }
1476
+ for (const childId of childIds) {
1477
+ if (this.agents.has(childId)) {
1478
+ console.log(`Cascading shutdown: child ${childId.slice(0, 12)}\u2026 (parent ${sessionId.slice(0, 12)}\u2026 removed)`);
1479
+ this.shutdown(childId);
1480
+ }
1481
+ }
1482
+ }
1483
+ // ---------------------------------------------------------------------------
1484
+ // Hook-based lifecycle methods (Phase B: merge event sources)
1485
+ // ---------------------------------------------------------------------------
1486
+ /** Hook: new Claude Code session started */
1487
+ hookSessionStart(sessionId, _cwd) {
1488
+ const canonicalId = this.resolveAgentId(sessionId);
1489
+ const agent = this.agents.get(canonicalId);
1490
+ if (agent) {
1491
+ agent.isIdle = false;
1492
+ agent.isDone = false;
1493
+ agent.phase = "running";
1494
+ agent.lastActivityAt = Date.now();
1495
+ this.resetIdleTimer(canonicalId);
1496
+ }
1497
+ }
1498
+ /** Hook: session ended — definitively shut down the agent */
1499
+ hookSessionEnd(sessionId) {
1500
+ const canonicalId = this.resolveAgentId(sessionId);
1501
+ if (this.agents.has(canonicalId)) {
1502
+ this.shutdown(canonicalId);
1503
+ }
1504
+ }
1505
+ /** Hook: user submitted a prompt — agent is now running */
1506
+ hookUserPromptSubmit(sessionId) {
1507
+ this.setPhase(sessionId, "running");
1508
+ }
1509
+ /** Hook: agent finished responding (Stop event) */
1510
+ hookStop(sessionId, lastMessage) {
1511
+ const canonicalId = this.resolveAgentId(sessionId);
1512
+ const agent = this.agents.get(canonicalId);
1513
+ if (!agent || this.hiddenAgents.has(canonicalId))
1514
+ return;
1515
+ agent.phase = "idle";
1516
+ agent.isIdle = true;
1517
+ agent.isPlanning = false;
1518
+ agent.isWaitingForUser = false;
1519
+ agent.currentZone = "idle";
1520
+ agent.currentTool = null;
1521
+ agent.currentActivity = null;
1522
+ if (lastMessage) {
1523
+ agent.speechText = lastMessage.length > 200 ? lastMessage.slice(0, 197) + "..." : lastMessage;
1524
+ }
1525
+ const now = Date.now();
1526
+ this.addHistory(canonicalId, { timestamp: now, kind: "idle", zone: "idle" });
1527
+ const idleEvent = { type: "agent:idle", agent: { ...agent }, timestamp: now };
1528
+ this.recordTimeline(idleEvent);
1529
+ this.emit("agent:idle", idleEvent);
1530
+ this.resetIdleTimer(canonicalId);
1531
+ }
1532
+ /** Hook: context compaction is starting */
1533
+ hookPreCompact(sessionId) {
1534
+ this.setPhase(sessionId, "compacting");
1535
+ }
1536
+ /** Hook: tool is about to execute (PreToolUse) */
1537
+ hookPreToolUse(sessionId, toolName, toolInput, _toolUseId) {
1538
+ const canonicalId = this.resolveAgentId(sessionId);
1539
+ const agent = this.agents.get(canonicalId);
1540
+ if (!agent || this.hiddenAgents.has(canonicalId))
1541
+ return;
1542
+ agent.phase = "running";
1543
+ agent.lastToolOutcome = null;
1544
+ agent.currentTool = toolName;
1545
+ agent.currentZone = getZoneForTool(toolName);
1546
+ if (toolInput) {
1547
+ agent.currentActivity = this.summarizeToolInput(toolInput);
1548
+ }
1549
+ agent.lastActivityAt = Date.now();
1550
+ this.resetIdleTimer(canonicalId);
1551
+ this.toolChainTracker.recordToolStart(canonicalId, toolName);
1552
+ const now = Date.now();
1553
+ const updateEvent = { type: "agent:update", agent: { ...agent }, timestamp: now };
1554
+ this.recordTimeline(updateEvent);
1555
+ this.emit("agent:update", updateEvent);
1556
+ }
1557
+ /** Hook: background task completed (TaskCompleted event) */
1558
+ hookTaskCompleted(sessionId, taskId, taskSubject) {
1559
+ const canonicalId = this.resolveAgentId(sessionId);
1560
+ const agent = this.agents.get(canonicalId);
1561
+ const root = agent?.rootSessionId ?? canonicalId;
1562
+ const changed = this.taskGraphManager.processTaskCompleted(taskId, root);
1563
+ if (changed) {
1564
+ this.emit("taskgraph:changed", { data: this.taskGraphManager.getSnapshot(), timestamp: Date.now() });
1565
+ }
1566
+ this.emit("task:completed", {
1567
+ taskId,
1568
+ taskSubject: taskSubject ?? `Task ${taskId}`,
1569
+ agentId: canonicalId
1570
+ });
1571
+ }
1572
+ /** Hook: tool completed (PostToolUse = success, PostToolUseFailure = failure) */
1573
+ hookPostToolUse(sessionId, _toolName, _toolUseId, success) {
1574
+ const canonicalId = this.resolveAgentId(sessionId);
1575
+ const agent = this.agents.get(canonicalId);
1576
+ if (!agent || this.hiddenAgents.has(canonicalId))
1577
+ return;
1578
+ agent.lastToolOutcome = success ? "success" : "failure";
1579
+ this.toolChainTracker.recordToolOutcome(canonicalId, success);
1580
+ agent.lastActivityAt = Date.now();
1581
+ this.resetIdleTimer(canonicalId);
1582
+ const now = Date.now();
1583
+ const updateEvent = { type: "agent:update", agent: { ...agent }, timestamp: now };
1584
+ this.recordTimeline(updateEvent);
1585
+ this.emit("agent:update", updateEvent);
1586
+ }
1587
+ setPhase(sessionId, phase) {
1588
+ const canonicalId = this.resolveAgentId(sessionId);
1589
+ const agent = this.agents.get(canonicalId);
1590
+ if (!agent || this.hiddenAgents.has(canonicalId))
1591
+ return;
1592
+ agent.phase = phase;
1593
+ if (phase === "running" || phase === "compacting") {
1594
+ agent.isIdle = false;
1595
+ agent.isDone = false;
1596
+ agent.lastActivityAt = Date.now();
1597
+ this.resetIdleTimer(canonicalId);
1598
+ }
1599
+ const now = Date.now();
1600
+ const updateEvent = { type: "agent:update", agent: { ...agent }, timestamp: now };
1601
+ this.recordTimeline(updateEvent);
1602
+ this.emit("agent:update", updateEvent);
1603
+ }
1604
+ dispose() {
1605
+ this.anomalyDetector.dispose();
1606
+ this.clearAllTimers();
1607
+ }
1608
+ clearAllTimers() {
1609
+ for (const timer of this.idleTimers.values())
1610
+ clearTimeout(timer);
1611
+ this.idleTimers.clear();
1612
+ for (const timer of this.shutdownTimers.values())
1613
+ clearTimeout(timer);
1614
+ this.shutdownTimers.clear();
1615
+ for (const timer of this.identityTimers.values())
1616
+ clearTimeout(timer);
1617
+ this.identityTimers.clear();
1618
+ }
1619
+ };
1620
+ export {
1621
+ AgentStateManager
1622
+ };