@adhdev/daemon-core 0.6.77 → 0.7.0

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.
@@ -11,6 +11,7 @@
11
11
  import { DaemonCdpManager, AgentWebviewTarget } from '../cdp/manager.js';
12
12
  import { ProviderLoader } from '../providers/provider-loader.js';
13
13
  import { ProviderStreamAdapter } from './provider-adapter.js';
14
+ import { SessionRegistry } from '../sessions/registry.js';
14
15
  import { LOG } from '../logging/logger.js';
15
16
  import type {
16
17
  IAgentStreamAdapter,
@@ -21,7 +22,9 @@ import type {
21
22
 
22
23
  export interface ManagedAgent {
23
24
  adapter: IAgentStreamAdapter;
24
- sessionId: string;
25
+ runtimeSessionId: string;
26
+ parentSessionId: string;
27
+ cdpSessionId: string;
25
28
  target: AgentWebviewTarget;
26
29
  lastState: AgentStreamState | null;
27
30
  lastError: string | null;
@@ -29,17 +32,19 @@ export interface ManagedAgent {
29
32
  }
30
33
 
31
34
  export class DaemonAgentStreamManager {
32
- private allAdapters: IAgentStreamAdapter[] = [];
33
- private managed = new Map<string, ManagedAgent>();
35
+ private adaptersByType = new Map<string, IAgentStreamAdapter>();
36
+ private managedBySessionId = new Map<string, ManagedAgent>();
34
37
  private enabled = true;
35
38
  private logFn: (msg: string) => void;
36
- private lastDiscoveryTime = 0;
37
- private discoveryIntervalMs = 10_000;
38
-
39
-
40
- private _activeAgentType: string | null = null;
41
-
42
- constructor(logFn?: (msg: string) => void, providerLoader?: ProviderLoader) {
39
+ private lastDiscoveryTimeByParent = new Map<string, number>();
40
+ private discoveryIntervalMsByParent = new Map<string, number>();
41
+ private activeSessionIdByParent = new Map<string, string | null>();
42
+
43
+ constructor(
44
+ logFn?: (msg: string) => void,
45
+ providerLoader?: ProviderLoader,
46
+ private readonly sessionRegistry?: SessionRegistry,
47
+ ) {
43
48
  this.logFn = logFn || LOG.forComponent('AgentStream').asLogFn();
44
49
 
45
50
  // Create adapter for all extension providers
@@ -50,7 +55,7 @@ export class DaemonAgentStreamManager {
50
55
  const resolved = providerLoader.resolve(p.type);
51
56
  if (!resolved) continue;
52
57
  const adapter = new ProviderStreamAdapter(resolved);
53
- this.allAdapters.push(adapter);
58
+ this.adaptersByType.set(p.type, adapter);
54
59
  this.logFn(`[AgentStream] Adapter created: ${p.type} (${p.name}) scripts=${Object.keys(resolved.scripts || {}).join(',') || 'none'}`);
55
60
  }
56
61
  }
@@ -58,232 +63,290 @@ export class DaemonAgentStreamManager {
58
63
 
59
64
  setEnabled(enabled: boolean) { this.enabled = enabled; }
60
65
  get isEnabled() { return this.enabled; }
61
- get activeAgentType(): string | null { return this._activeAgentType; }
66
+ getActiveSessionId(parentSessionId: string): string | null {
67
+ return this.activeSessionIdByParent.get(parentSessionId) || null;
68
+ }
69
+
70
+ private getSessionTarget(sessionId: string) {
71
+ return this.sessionRegistry?.get(sessionId);
72
+ }
73
+
74
+ resetParentSession(parentSessionId: string): void {
75
+ const activeSessionId = this.activeSessionIdByParent.get(parentSessionId);
76
+ if (activeSessionId) this.managedBySessionId.delete(activeSessionId);
77
+ for (const child of this.sessionRegistry?.listChildren(parentSessionId) || []) {
78
+ this.managedBySessionId.delete(child.sessionId);
79
+ }
80
+ this.activeSessionIdByParent.delete(parentSessionId);
81
+ this.lastDiscoveryTimeByParent.delete(parentSessionId);
82
+ this.discoveryIntervalMsByParent.delete(parentSessionId);
83
+ }
62
84
 
63
85
  /** Panel focus based on provider.js focusPanel or extensionId (currently no-op) */
64
- async ensureAgentPanelOpen(agentType: string, targetIdeType?: string): Promise<void> {
86
+ async ensureSessionPanelOpen(_sessionId: string): Promise<void> {
65
87
  // Extension was removed, so localServer-based panel focus no longer works
66
88
  // Can be replaced with CDP-based focus (future implementation)
67
89
  }
68
90
 
69
- async switchActiveAgent(cdp: DaemonCdpManager, agentType: string | null): Promise<void> {
70
- if (this._activeAgentType === agentType) return;
91
+ async setActiveSession(cdp: DaemonCdpManager, parentSessionId: string, sessionId: string | null): Promise<void> {
92
+ const previousSessionId = this.getActiveSessionId(parentSessionId);
93
+ if (previousSessionId === sessionId) return;
71
94
 
72
- if (this._activeAgentType) {
73
- const prev = this.managed.get(this._activeAgentType);
95
+ if (previousSessionId) {
96
+ const prev = this.managedBySessionId.get(previousSessionId);
74
97
  if (prev) {
75
- try { await cdp.detachAgent(prev.sessionId); } catch { }
76
- this.managed.delete(this._activeAgentType);
77
- this.logFn(`[AgentStream] Deactivated: ${prev.adapter.agentName}`);
98
+ try { await cdp.detachAgent(prev.cdpSessionId); } catch { }
99
+ this.managedBySessionId.delete(previousSessionId);
100
+ this.logFn(`[AgentStream] Deactivated: ${prev.adapter.agentName} (${parentSessionId})`);
78
101
  }
79
102
  }
80
103
 
81
- this._activeAgentType = agentType;
82
- this.lastDiscoveryTime = 0;
83
- this.logFn(`[AgentStream] Active agent: ${agentType || 'none'}`);
104
+ this.activeSessionIdByParent.set(parentSessionId, sessionId);
105
+ this.lastDiscoveryTimeByParent.set(parentSessionId, 0);
106
+ this.logFn(`[AgentStream] Active session (${parentSessionId}): ${sessionId || 'none'}`);
84
107
  }
85
108
 
86
- /** Agent webview discovery + session connection */
87
- async syncAgentSessions(cdp: DaemonCdpManager): Promise<void> {
88
- if (!this.enabled || !this._activeAgentType) return;
109
+ private resolveSessionIdForTarget(parentSessionId: string, agentType: string): string | null {
110
+ const child = (this.sessionRegistry?.listChildren(parentSessionId) || [])
111
+ .find((entry) => entry.providerCategory === 'extension' && entry.providerType === agentType);
112
+ return child?.sessionId || null;
113
+ }
114
+
115
+ private async connectManagedSession(
116
+ cdp: DaemonCdpManager,
117
+ parentSessionId: string,
118
+ runtimeSessionId: string,
119
+ ): Promise<ManagedAgent | null> {
120
+ const target = this.getSessionTarget(runtimeSessionId);
121
+ if (!target || target.providerCategory !== 'extension') return null;
122
+ const adapter = this.adaptersByType.get(target.providerType);
123
+ if (!adapter) return null;
124
+ const targets = await cdp.discoverAgentWebviews();
125
+ const activeTarget = targets.find((entry) => entry.agentType === target.providerType);
126
+ if (!activeTarget) return null;
127
+ const cdpSessionId = await cdp.attachToAgent(activeTarget);
128
+ if (!cdpSessionId) return null;
129
+ const managed: ManagedAgent = {
130
+ adapter,
131
+ runtimeSessionId,
132
+ parentSessionId,
133
+ cdpSessionId,
134
+ target: activeTarget,
135
+ lastState: null,
136
+ lastError: null,
137
+ lastHiddenCheckTime: 0,
138
+ };
139
+ this.managedBySessionId.set(runtimeSessionId, managed);
140
+ this.logFn(`[AgentStream] Connected: ${adapter.agentName} (${parentSessionId})`);
141
+ return managed;
142
+ }
143
+
144
+ /** Agent webview discovery + session connection */
145
+ async syncActiveSession(cdp: DaemonCdpManager, parentSessionId: string): Promise<void> {
146
+ const activeSessionId = this.getActiveSessionId(parentSessionId);
147
+ if (!this.enabled || !activeSessionId) return;
89
148
 
90
149
  const now = Date.now();
91
- if (this.managed.has(this._activeAgentType) && (now - this.lastDiscoveryTime) < this.discoveryIntervalMs) {
150
+ const managed = this.managedBySessionId.get(activeSessionId);
151
+ const lastDiscoveryTime = this.lastDiscoveryTimeByParent.get(parentSessionId) || 0;
152
+ const discoveryIntervalMs = this.discoveryIntervalMsByParent.get(parentSessionId) || 10_000;
153
+ if (managed && (now - lastDiscoveryTime) < discoveryIntervalMs) {
92
154
  return;
93
155
  }
94
- this.lastDiscoveryTime = now;
156
+ this.lastDiscoveryTimeByParent.set(parentSessionId, now);
95
157
 
96
158
  try {
97
- const targets = await cdp.discoverAgentWebviews();
98
- const activeTarget = targets.find(t => t.agentType === this._activeAgentType);
99
-
100
- if (activeTarget && !this.managed.has(this._activeAgentType)) {
101
- const adapter = this.allAdapters.find(a => a.agentType === this._activeAgentType);
102
- if (adapter) {
103
- const sessionId = await cdp.attachToAgent(activeTarget);
104
- if (sessionId) {
105
- this.managed.set(this._activeAgentType, {
106
- adapter,
107
- sessionId,
108
- target: activeTarget,
109
- lastState: null,
110
- lastError: null,
111
- lastHiddenCheckTime: 0,
112
- });
113
- this.logFn(`[AgentStream] Connected: ${adapter.agentName}`);
114
- }
115
- }
159
+ if (!managed) {
160
+ await this.connectManagedSession(cdp, parentSessionId, activeSessionId);
116
161
  }
117
-
118
- // Cleanup inactive agents
119
- for (const [type, agent] of this.managed) {
120
- if (type !== this._activeAgentType) {
121
- await cdp.detachAgent(agent.sessionId);
122
- this.managed.delete(type);
123
- }
124
- }
125
-
126
- this.discoveryIntervalMs = this.managed.has(this._activeAgentType) ? 30_000 : 10_000;
162
+ this.discoveryIntervalMsByParent.set(parentSessionId, this.managedBySessionId.has(activeSessionId) ? 30_000 : 10_000);
127
163
  } catch (e) {
128
- this.logFn(`[AgentStream] sync error: ${(e as Error).message}`);
164
+ this.logFn(`[AgentStream] sync error (${parentSessionId}): ${(e as Error).message}`);
129
165
  }
130
166
  }
131
167
 
132
- /** Collect active agent status */
133
- async collectAgentStreams(cdp: DaemonCdpManager): Promise<AgentStreamState[]> {
134
- if (!this.enabled) return [];
135
-
136
- const results: AgentStreamState[] = [];
137
-
138
- if (this._activeAgentType && this.managed.has(this._activeAgentType)) {
139
- const agent = this.managed.get(this._activeAgentType)!;
140
- const type = this._activeAgentType;
168
+ /** Collect active extension session state */
169
+ async collectActiveSession(cdp: DaemonCdpManager, parentSessionId: string): Promise<AgentStreamState | null> {
170
+ if (!this.enabled) return null;
171
+ const activeSessionId = this.getActiveSessionId(parentSessionId);
172
+ if (!activeSessionId) return null;
173
+ let agent = this.managedBySessionId.get(activeSessionId);
174
+ if (!agent) {
175
+ agent = await this.connectManagedSession(cdp, parentSessionId, activeSessionId) || undefined;
176
+ }
177
+ if (!agent) return null;
178
+ const type = agent.adapter.agentType;
179
+ const isHidden = agent.lastState?.status === 'panel_hidden';
180
+ const hiddenCacheFresh = isHidden && (Date.now() - agent.lastHiddenCheckTime < 30000);
141
181
 
142
- const isHidden = agent.lastState?.status === 'panel_hidden';
143
- const hiddenCacheFresh = isHidden && (Date.now() - agent.lastHiddenCheckTime < 30000);
182
+ if (hiddenCacheFresh) return agent.lastState!;
144
183
 
145
- if (hiddenCacheFresh) {
146
- results.push(agent.lastState!);
147
- } else {
148
- try {
149
- const evaluate: AgentEvaluateFn = (expr, timeout) =>
150
- cdp.evaluateInSessionFrame(agent.sessionId, expr, timeout);
151
- const state = await agent.adapter.readChat(evaluate);
152
- LOG.debug('AgentStream', `[AgentStream] readChat(${type}) result: status=${state.status} msgs=${state.messages?.length || 0} model=${state.model || ''}${state.status === 'error' ? ' error=' + JSON.stringify((state as any).error || (state as any)._error || 'unknown') : ''}`);
153
- agent.lastState = state;
154
- agent.lastError = null;
155
- if (state.status === 'panel_hidden') {
156
- agent.lastHiddenCheckTime = Date.now();
157
- }
158
- results.push(state);
159
- } catch (e) {
160
- const errorMsg = (e as Error)?.message || String(e);
161
- this.logFn(`[AgentStream] readChat(${type}) error: ${errorMsg.slice(0, 200)}`);
162
- agent.lastError = errorMsg;
163
- results.push({
164
- agentType: type,
165
- agentName: agent.adapter.agentName,
166
- extensionId: agent.adapter.extensionId,
167
- status: 'disconnected',
168
- messages: agent.lastState?.messages || [],
169
- inputContent: '',
170
- });
171
- if (errorMsg.includes('timeout') || errorMsg.includes('not connected') || errorMsg.includes('Session')) {
172
- try { await cdp.detachAgent(agent.sessionId); } catch { }
173
- this.managed.delete(type);
174
- this.lastDiscoveryTime = 0;
175
- }
176
- }
184
+ try {
185
+ const evaluate: AgentEvaluateFn = (expr, timeout) =>
186
+ cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
187
+ const state = await agent.adapter.readChat(evaluate);
188
+ LOG.debug('AgentStream', `[AgentStream] readChat(${type}) result: status=${state.status} msgs=${state.messages?.length || 0} model=${state.model || ''}${state.status === 'error' ? ' error=' + JSON.stringify((state as any).error || (state as any)._error || 'unknown') : ''}`);
189
+ agent.lastState = state;
190
+ agent.lastError = null;
191
+ if (state.status === 'panel_hidden') {
192
+ agent.lastHiddenCheckTime = Date.now();
177
193
  }
194
+ return state;
195
+ } catch (e) {
196
+ const errorMsg = (e as Error)?.message || String(e);
197
+ this.logFn(`[AgentStream] readChat(${type}) error: ${errorMsg.slice(0, 200)}`);
198
+ agent.lastError = errorMsg;
199
+ if (errorMsg.includes('timeout') || errorMsg.includes('not connected') || errorMsg.includes('Session')) {
200
+ try { await cdp.detachAgent(agent.cdpSessionId); } catch { }
201
+ this.managedBySessionId.delete(activeSessionId);
202
+ this.lastDiscoveryTimeByParent.set(parentSessionId, 0);
203
+ }
204
+ return {
205
+ agentType: type,
206
+ agentName: agent.adapter.agentName,
207
+ extensionId: agent.adapter.extensionId,
208
+ status: 'disconnected',
209
+ messages: agent.lastState?.messages || [],
210
+ inputContent: '',
211
+ };
178
212
  }
179
-
180
- return results;
181
213
  }
182
214
 
183
- async sendToAgent(cdp: DaemonCdpManager, agentType: string, text: string, targetIdeType?: string): Promise<boolean> {
184
- await this.ensureAgentPanelOpen(agentType, targetIdeType);
185
- const agent = this.managed.get(agentType);
215
+ async sendToSession(cdp: DaemonCdpManager, sessionId: string, text: string): Promise<boolean> {
216
+ await this.ensureSessionPanelOpen(sessionId);
217
+ const target = this.getSessionTarget(sessionId);
218
+ if (!target?.parentSessionId) return false;
219
+ await this.setActiveSession(cdp, target.parentSessionId, sessionId);
220
+ await this.syncActiveSession(cdp, target.parentSessionId);
221
+ const agent = this.managedBySessionId.get(sessionId);
186
222
  if (!agent) return false;
187
223
  try {
188
224
  const evaluate: AgentEvaluateFn = (expr, timeout) =>
189
- cdp.evaluateInSessionFrame(agent.sessionId, expr, timeout);
225
+ cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
190
226
  await agent.adapter.sendMessage(evaluate, text);
191
227
  return true;
192
228
  } catch (e) {
193
- this.logFn(`[AgentStream] sendToAgent(${agentType}) error: ${(e as Error).message}`);
229
+ this.logFn(`[AgentStream] sendToSession(${sessionId}) error: ${(e as Error).message}`);
194
230
  return false;
195
231
  }
196
232
  }
197
233
 
198
- async resolveAgentAction(cdp: DaemonCdpManager, agentType: string, action: 'approve' | 'reject', targetIdeType?: string): Promise<boolean> {
199
- await this.ensureAgentPanelOpen(agentType, targetIdeType);
200
- const agent = this.managed.get(agentType);
234
+ async resolveSessionAction(cdp: DaemonCdpManager, sessionId: string, action: 'approve' | 'reject'): Promise<boolean> {
235
+ await this.ensureSessionPanelOpen(sessionId);
236
+ const target = this.getSessionTarget(sessionId);
237
+ if (!target?.parentSessionId) return false;
238
+ await this.setActiveSession(cdp, target.parentSessionId, sessionId);
239
+ await this.syncActiveSession(cdp, target.parentSessionId);
240
+ const agent = this.managedBySessionId.get(sessionId);
201
241
  if (!agent) return false;
202
242
  try {
203
243
  const evaluate: AgentEvaluateFn = (expr, timeout) =>
204
- cdp.evaluateInSessionFrame(agent.sessionId, expr, timeout);
244
+ cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
205
245
  return await agent.adapter.resolveAction(evaluate, action);
206
246
  } catch (e) {
207
- this.logFn(`[AgentStream] resolveAction(${agentType}) error: ${(e as Error).message}`);
247
+ this.logFn(`[AgentStream] resolveAction(${sessionId}) error: ${(e as Error).message}`);
208
248
  return false;
209
249
  }
210
250
  }
211
251
 
212
- async newAgentSession(cdp: DaemonCdpManager, agentType: string, targetIdeType?: string): Promise<boolean> {
213
- await this.ensureAgentPanelOpen(agentType, targetIdeType);
214
- const agent = this.managed.get(agentType);
252
+ async newSession(cdp: DaemonCdpManager, sessionId: string): Promise<boolean> {
253
+ await this.ensureSessionPanelOpen(sessionId);
254
+ const target = this.getSessionTarget(sessionId);
255
+ if (!target?.parentSessionId) return false;
256
+ await this.setActiveSession(cdp, target.parentSessionId, sessionId);
257
+ await this.syncActiveSession(cdp, target.parentSessionId);
258
+ const agent = this.managedBySessionId.get(sessionId);
215
259
  if (!agent) return false;
216
260
  try {
217
261
  const evaluate: AgentEvaluateFn = (expr, timeout) =>
218
- cdp.evaluateInSessionFrame(agent.sessionId, expr, timeout);
262
+ cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
219
263
  await agent.adapter.newSession(evaluate);
220
264
  return true;
221
265
  } catch (e) {
222
- this.logFn(`[AgentStream] newSession(${agentType}) error: ${(e as Error).message}`);
266
+ this.logFn(`[AgentStream] newSession(${sessionId}) error: ${(e as Error).message}`);
223
267
  return false;
224
268
  }
225
269
  }
226
270
 
227
- async listAgentChats(cdp: DaemonCdpManager, agentType: string): Promise<AgentChatListItem[]> {
228
- let agent = this.managed.get(agentType);
229
- // on-demand: try activate+sync if not in managed list
230
- if (!agent) {
231
- this.logFn(`[AgentStream] listChats: ${agentType} not managed, trying on-demand activation`);
232
- await this.switchActiveAgent(cdp, agentType);
233
- await this.syncAgentSessions(cdp);
234
- agent = this.managed.get(agentType);
235
- }
271
+ async listSessionChats(cdp: DaemonCdpManager, sessionId: string): Promise<AgentChatListItem[]> {
272
+ const target = this.getSessionTarget(sessionId);
273
+ if (!target?.parentSessionId) return [];
274
+ await this.setActiveSession(cdp, target.parentSessionId, sessionId);
275
+ await this.syncActiveSession(cdp, target.parentSessionId);
276
+ const agent = this.managedBySessionId.get(sessionId);
236
277
  if (!agent || typeof agent.adapter.listChats !== 'function') return [];
237
278
  try {
238
279
  const evaluate: AgentEvaluateFn = (expr, timeout) =>
239
- cdp.evaluateInSessionFrame(agent!.sessionId, expr, timeout);
280
+ cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
240
281
  return await agent.adapter.listChats(evaluate);
241
282
  } catch (e) {
242
- this.logFn(`[AgentStream] listChats(${agentType}) error: ${(e as Error).message}`);
283
+ this.logFn(`[AgentStream] listChats(${sessionId}) error: ${(e as Error).message}`);
243
284
  return [];
244
285
  }
245
286
  }
246
287
 
247
- async switchAgentSession(cdp: DaemonCdpManager, agentType: string, sessionId: string): Promise<boolean> {
248
- let agent = this.managed.get(agentType);
249
- if (!agent) {
250
- this.logFn(`[AgentStream] switchSession: ${agentType} not managed, trying on-demand activation`);
251
- await this.switchActiveAgent(cdp, agentType);
252
- await this.syncAgentSessions(cdp);
253
- agent = this.managed.get(agentType);
254
- }
288
+ async switchConversation(cdp: DaemonCdpManager, sessionId: string, conversationId: string): Promise<boolean> {
289
+ const target = this.getSessionTarget(sessionId);
290
+ if (!target?.parentSessionId) return false;
291
+ await this.setActiveSession(cdp, target.parentSessionId, sessionId);
292
+ await this.syncActiveSession(cdp, target.parentSessionId);
293
+ const agent = this.managedBySessionId.get(sessionId);
255
294
  if (!agent || typeof agent.adapter.switchSession !== 'function') return false;
256
295
  try {
257
296
  const evaluate: AgentEvaluateFn = (expr, timeout) =>
258
- cdp.evaluateInSessionFrame(agent!.sessionId, expr, timeout);
259
- return await agent.adapter.switchSession(evaluate, sessionId);
297
+ cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
298
+ return await agent.adapter.switchSession(evaluate, conversationId);
260
299
  } catch (e) {
261
- this.logFn(`[AgentStream] switchSession(${agentType}) error: ${(e as Error).message}`);
300
+ this.logFn(`[AgentStream] switchSession(${sessionId}) error: ${(e as Error).message}`);
262
301
  return false;
263
302
  }
264
303
  }
265
304
 
266
- async focusAgentEditor(cdp: DaemonCdpManager, agentType: string): Promise<boolean> {
267
- const agent = this.managed.get(agentType);
305
+ async focusSession(cdp: DaemonCdpManager, sessionId: string): Promise<boolean> {
306
+ const target = this.getSessionTarget(sessionId);
307
+ if (!target?.parentSessionId) return false;
308
+ await this.setActiveSession(cdp, target.parentSessionId, sessionId);
309
+ await this.syncActiveSession(cdp, target.parentSessionId);
310
+ const agent = this.managedBySessionId.get(sessionId);
268
311
  if (!agent || typeof agent.adapter.focusEditor !== 'function') return false;
269
312
  try {
270
313
  const evaluate: AgentEvaluateFn = (expr, timeout) =>
271
- cdp.evaluateInSessionFrame(agent.sessionId, expr, timeout);
314
+ cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
272
315
  await agent.adapter.focusEditor(evaluate);
273
316
  return true;
274
317
  } catch (e) {
275
- this.logFn(`[AgentStream] focusEditor(${agentType}) error: ${(e as Error).message}`);
318
+ this.logFn(`[AgentStream] focusEditor(${sessionId}) error: ${(e as Error).message}`);
276
319
  return false;
277
320
  }
278
321
  }
279
322
 
280
- getConnectedAgents(): string[] { return Array.from(this.managed.keys()); }
281
- getManagedAgent(agentType: string): ManagedAgent | undefined { return this.managed.get(agentType); }
323
+ getConnectedSessions(parentSessionId?: string): string[] {
324
+ if (parentSessionId) {
325
+ return [...this.managedBySessionId.values()]
326
+ .filter((entry) => entry.parentSessionId === parentSessionId)
327
+ .map((entry) => entry.runtimeSessionId);
328
+ }
329
+ return [...this.managedBySessionId.keys()];
330
+ }
282
331
 
283
- async dispose(cdp: DaemonCdpManager): Promise<void> {
284
- for (const [, agent] of this.managed) {
285
- try { await cdp.detachAgent(agent.sessionId); } catch { }
332
+ getManagedSession(sessionId: string): ManagedAgent | undefined {
333
+ return this.managedBySessionId.get(sessionId);
334
+ }
335
+
336
+ async dispose(cdpManagers: Map<string, DaemonCdpManager>): Promise<void> {
337
+ for (const managed of this.managedBySessionId.values()) {
338
+ const managerKey = this.getSessionTarget(managed.runtimeSessionId)?.cdpManagerKey;
339
+ const cdp = managerKey ? cdpManagers.get(managerKey) : null;
340
+ if (!cdp) continue;
341
+ try { await cdp.detachAgent(managed.cdpSessionId); } catch { }
286
342
  }
287
- this.managed.clear();
343
+ this.managedBySessionId.clear();
344
+ this.activeSessionIdByParent.clear();
345
+ this.lastDiscoveryTimeByParent.clear();
346
+ this.discoveryIntervalMsByParent.clear();
347
+ }
348
+
349
+ resolveSessionForAgent(parentSessionId: string, agentType: string): string | null {
350
+ return this.resolveSessionIdForTarget(parentSessionId, agentType);
288
351
  }
289
352
  }