@adhdev/daemon-core 0.6.79 → 0.7.1

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.
@@ -15,6 +15,7 @@ import type { DaemonAgentStreamManager } from './manager.js';
15
15
  import type { ProviderLoader } from '../providers/provider-loader.js';
16
16
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
17
17
  import { registerExtensionProviders } from '../cdp/setup.js';
18
+ import type { SessionRegistry } from '../sessions/registry.js';
18
19
  import { LOG } from '../logging/logger.js';
19
20
  import type { AgentStreamState } from './types.js';
20
21
 
@@ -25,6 +26,7 @@ export interface AgentStreamPollerDeps {
25
26
  providerLoader: ProviderLoader;
26
27
  instanceManager: ProviderInstanceManager;
27
28
  cdpManagers: Map<string, DaemonCdpManager>;
29
+ sessionRegistry: SessionRegistry;
28
30
  /** Callback when agent streams are updated */
29
31
  onStreamsUpdated?: (ideType: string, streams: AgentStreamState[]) => void;
30
32
  }
@@ -43,8 +45,8 @@ export class AgentStreamPoller {
43
45
  }
44
46
 
45
47
  /** Reset active IDE tracking (e.g., when IDE is stopped) */
46
- resetActiveIde(ideType: string): void {
47
- this.deps.agentStreamManager.resetScope(ideType);
48
+ resetActiveIde(parentSessionId: string): void {
49
+ this.deps.agentStreamManager.resetParentSession(parentSessionId);
48
50
  }
49
51
 
50
52
  /** Start polling (idempotent — ignored if already started) */
@@ -71,6 +73,7 @@ export class AgentStreamPoller {
71
73
  providerLoader,
72
74
  instanceManager,
73
75
  cdpManagers,
76
+ sessionRegistry,
74
77
  } = this.deps;
75
78
 
76
79
  if (!agentStreamManager || cdpManagers.size === 0) return;
@@ -82,6 +85,7 @@ export class AgentStreamPoller {
82
85
 
83
86
  // 1b. Dynamically add/remove IDE instance extensions
84
87
  const ideInstance = instanceManager.getInstance(`ide:${ideType}`) as any;
88
+ const parentSessionId = ideInstance?.getInstanceId?.();
85
89
  if (ideInstance?.getExtensionTypes && ideInstance?.addExtension && ideInstance?.removeExtension) {
86
90
  const currentExtTypes = new Set(ideInstance.getExtensionTypes() as string[]);
87
91
  const enabledExtTypes = new Set(
@@ -91,6 +95,10 @@ export class AgentStreamPoller {
91
95
  // Remove disabled extensions
92
96
  for (const extType of currentExtTypes) {
93
97
  if (!enabledExtTypes.has(extType)) {
98
+ const extInstance = ideInstance.getExtension?.(extType);
99
+ if (extInstance?.getInstanceId) {
100
+ sessionRegistry.unregister(extInstance.getInstanceId());
101
+ }
94
102
  ideInstance.removeExtension(extType);
95
103
  LOG.info('AgentStream', `Extension removed: ${extType} (disabled for ${ideType})`);
96
104
  }
@@ -103,6 +111,18 @@ export class AgentStreamPoller {
103
111
  if (extProvider) {
104
112
  const extSettings = providerLoader.getSettings(extType);
105
113
  ideInstance.addExtension(extProvider, extSettings);
114
+ const extInstance = ideInstance.getExtension?.(extType);
115
+ if (parentSessionId && extInstance?.getInstanceId) {
116
+ sessionRegistry.register({
117
+ sessionId: extInstance.getInstanceId(),
118
+ parentSessionId,
119
+ providerType: extType,
120
+ providerCategory: 'extension',
121
+ transport: 'cdp-webview',
122
+ cdpManagerKey: ideType,
123
+ instanceKey: `ide:${ideType}`,
124
+ });
125
+ }
106
126
  LOG.info('AgentStream', `Extension added: ${extType} (enabled for ${ideType})`);
107
127
  }
108
128
  }
@@ -110,47 +130,50 @@ export class AgentStreamPoller {
110
130
  }
111
131
 
112
132
  // 1c. If the active agent stream belongs to a now-disabled extension, detach it
113
- const activeType = agentStreamManager.getActiveAgentType(ideType);
114
- if (activeType) {
115
- const enabledExtTypes = new Set(
116
- providerLoader.getEnabledExtensionProviders(ideType).map((p: any) => p.type)
117
- );
118
- if (!enabledExtTypes.has(activeType)) {
119
- LOG.info('AgentStream', `Active agent ${activeType} was disabled for ${ideType} — detaching`);
120
- await agentStreamManager.switchActiveAgent(cdp, ideType, null);
133
+ const activeSessionId = parentSessionId ? agentStreamManager.getActiveSessionId(parentSessionId) : null;
134
+ if (activeSessionId) {
135
+ const activeTarget = sessionRegistry.get(activeSessionId);
136
+ const enabledExtTypes = new Set(providerLoader.getEnabledExtensionProviders(ideType).map((p: any) => p.type));
137
+ if (!activeTarget || !enabledExtTypes.has(activeTarget.providerType)) {
138
+ LOG.info('AgentStream', `Active agent ${activeTarget?.providerType || activeSessionId} was disabled for ${ideType} — detaching`);
139
+ await agentStreamManager.setActiveSession(cdp, parentSessionId!, null);
121
140
  // Report empty streams so dashboard removes the tab
122
141
  this.deps.onStreamsUpdated?.(ideType, []);
123
142
  }
124
143
  }
125
144
  if (!cdp.isConnected) {
126
- if (activeType) {
127
- agentStreamManager.resetScope(ideType);
145
+ if (parentSessionId && activeSessionId) {
146
+ agentStreamManager.resetParentSession(parentSessionId);
128
147
  this.deps.onStreamsUpdated?.(ideType, []);
129
148
  }
130
149
  continue;
131
150
  }
132
151
 
133
152
  // ─── Phase 2: Agent session sync + collect ───
134
- let resolvedActiveType = activeType;
153
+ let resolvedActiveSessionId = activeSessionId;
135
154
 
136
155
  // ─── Phase 3: Auto-discover agents ───
137
- if (!resolvedActiveType) {
156
+ if (!resolvedActiveSessionId && parentSessionId) {
138
157
  try {
139
158
  const discovered = await cdp.discoverAgentWebviews();
140
- if (discovered.length > 0) {
141
- resolvedActiveType = discovered[0].agentType;
142
- await agentStreamManager.switchActiveAgent(cdp, ideType, resolvedActiveType);
143
- LOG.info('AgentStream', `Auto-activated: ${resolvedActiveType} (${ideType})`);
159
+ for (const target of discovered) {
160
+ const sessionId = agentStreamManager.resolveSessionForAgent(parentSessionId, target.agentType);
161
+ if (sessionId) {
162
+ resolvedActiveSessionId = sessionId;
163
+ await agentStreamManager.setActiveSession(cdp, parentSessionId, sessionId);
164
+ LOG.info('AgentStream', `Auto-activated: ${target.agentType} (${ideType})`);
165
+ break;
166
+ }
144
167
  }
145
168
  } catch { }
146
169
  }
147
170
 
148
- if (!resolvedActiveType) continue;
171
+ if (!resolvedActiveSessionId || !parentSessionId) continue;
149
172
 
150
173
  try {
151
- await agentStreamManager.syncAgentSessions(cdp, ideType);
152
- const streams = await agentStreamManager.collectAgentStreams(cdp, ideType);
153
- this.deps.onStreamsUpdated?.(ideType, streams);
174
+ await agentStreamManager.syncActiveSession(cdp, parentSessionId);
175
+ const stream = await agentStreamManager.collectActiveSession(cdp, parentSessionId);
176
+ this.deps.onStreamsUpdated?.(ideType, stream ? [stream] : []);
154
177
  } catch { }
155
178
  }
156
179
  }
@@ -20,6 +20,7 @@ import { VersionArchive, detectAllVersions } from '../providers/version-archive.
20
20
  import { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
21
21
  import { DevServer } from '../daemon/dev-server.js';
22
22
  import { detectIDEs } from '../detection/ide-detector.js';
23
+ import { SessionRegistry } from '../sessions/registry.js';
23
24
  import { installGlobalInterceptor, LOG } from '../logging/logger.js';
24
25
  import { loadConfig } from '../config/config.js';
25
26
 
@@ -70,7 +71,7 @@ export interface DaemonComponents {
70
71
  poller: AgentStreamPoller;
71
72
  cdpInitializer: DaemonCdpInitializer;
72
73
  cdpManagers: Map<string, DaemonCdpManager>;
73
- instanceIdMap: Map<string, string>;
74
+ sessionRegistry: SessionRegistry;
74
75
  detectedIdes: { value: any[] };
75
76
  }
76
77
 
@@ -141,13 +142,16 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
141
142
  // 3. Shared state
142
143
  const instanceManager = new ProviderInstanceManager();
143
144
  const cdpManagers = new Map<string, DaemonCdpManager>();
144
- const instanceIdMap = new Map<string, string>();
145
+ const sessionRegistry = new SessionRegistry();
145
146
  const detectedIdesRef = { value: [] as any[] };
147
+ let agentStreamManager: DaemonAgentStreamManager | null = null;
148
+ let poller: AgentStreamPoller | null = null;
146
149
 
147
150
  // 4. CLI Manager
148
151
  const cliManager = new DaemonCliManager({
149
152
  ...config.cliManagerDeps,
150
153
  getInstanceManager: () => instanceManager,
154
+ getSessionRegistry: () => sessionRegistry,
151
155
  }, providerLoader);
152
156
 
153
157
  // 5. Detect IDEs
@@ -161,7 +165,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
161
165
  providerLoader,
162
166
  instanceManager,
163
167
  cdpManagers,
164
- instanceIdMap,
168
+ sessionRegistry,
165
169
  };
166
170
 
167
171
  const cdpInitializer = new DaemonCdpInitializer({
@@ -174,6 +178,21 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
174
178
  // Transport-specific extras
175
179
  await config.onCdpManagerSetup?.(ideType, manager, managerKey);
176
180
  },
181
+ onDisconnected: async (_ideType, _manager, managerKey) => {
182
+ sessionRegistry.unregisterByManagerKey(managerKey);
183
+ const instanceKey = `ide:${managerKey}`;
184
+ const ideInstance = instanceManager.getInstance(instanceKey) as any;
185
+
186
+ if (ideInstance) {
187
+ instanceManager.removeInstance(instanceKey);
188
+ LOG.info('CDP', `Instance removed after disconnect: ${instanceKey}`);
189
+ }
190
+
191
+ if (ideInstance?.getInstanceId) {
192
+ agentStreamManager?.resetParentSession(ideInstance.getInstanceId());
193
+ }
194
+ config.onStatusChange?.();
195
+ },
177
196
  });
178
197
  await cdpInitializer.connectAll(detectedIdesRef.value);
179
198
  cdpInitializer.startPeriodicScan(config.cdpScanIntervalMs ?? 30_000);
@@ -186,20 +205,19 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
186
205
  adapters: cliManager.adapters,
187
206
  providerLoader,
188
207
  instanceManager,
189
- instanceIdMap,
208
+ sessionRegistry,
190
209
  });
191
210
 
192
211
  // 8. AgentStreamManager
193
- const agentStreamManager = new DaemonAgentStreamManager(
212
+ agentStreamManager = new DaemonAgentStreamManager(
194
213
  LOG.forComponent('AgentStream').asLogFn(),
195
214
  providerLoader,
215
+ sessionRegistry,
196
216
  );
197
217
  commandHandler.setAgentStreamManager(agentStreamManager);
198
218
 
199
219
  // 9. Router + Poller (with internal cross-wiring)
200
220
  // Note: poller is declared first so router's onIdeConnected closure captures it
201
- let poller: AgentStreamPoller;
202
-
203
221
  const router = new DaemonCommandRouter({
204
222
  commandHandler,
205
223
  cliManager,
@@ -207,7 +225,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
207
225
  providerLoader,
208
226
  instanceManager,
209
227
  detectedIdes: detectedIdesRef,
210
- instanceIdMap,
228
+ sessionRegistry,
211
229
  onCdpManagerCreated: async (ideType: string, manager: DaemonCdpManager) => {
212
230
  // For launch_ide: register instance + extension providers
213
231
  await setupIdeInstance(cdpSetupContext, { ideType, manager });
@@ -224,6 +242,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
224
242
  providerLoader,
225
243
  instanceManager,
226
244
  cdpManagers,
245
+ sessionRegistry,
227
246
  onStreamsUpdated: config.onStreamsUpdated,
228
247
  });
229
248
  poller.start();
@@ -241,7 +260,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
241
260
  poller,
242
261
  cdpInitializer,
243
262
  cdpManagers,
244
- instanceIdMap,
263
+ sessionRegistry,
245
264
  detectedIdes: detectedIdesRef,
246
265
  };
247
266
  }
@@ -24,6 +24,13 @@ export interface CdpInitializerConfig {
24
24
  enabledIdes?: string[];
25
25
  /** Callback when a new CDP manager is connected */
26
26
  onConnected?: (ideType: string, manager: DaemonCdpManager, managerKey: string) => void | Promise<void>;
27
+ /** Callback when a stale/disconnected CDP manager is removed */
28
+ onDisconnected?: (
29
+ ideType: string,
30
+ manager: DaemonCdpManager,
31
+ managerKey: string,
32
+ reason: 'ide_closed' | 'target_closed' | 'target_rekeyed',
33
+ ) => void | Promise<void>;
27
34
  }
28
35
 
29
36
  export class DaemonCdpInitializer {
@@ -88,6 +95,7 @@ export class DaemonCdpInitializer {
88
95
 
89
96
  // 1. Try multi-window: list all workbench pages on this port
90
97
  const targets = await DaemonCdpManager.listAllTargets(port);
98
+ await this.pruneStaleManagers(port, ide, targets);
91
99
 
92
100
  if (targets.length === 0) {
93
101
  // Prevent duplicate fallback connection
@@ -153,6 +161,45 @@ export class DaemonCdpInitializer {
153
161
  }
154
162
  }
155
163
 
164
+ private async pruneStaleManagers(
165
+ port: number,
166
+ ide: string,
167
+ targets: Array<{ id: string }>,
168
+ ): Promise<void> {
169
+ const trackedTargetIds = new Set(targets.map((target) => target.id));
170
+ const removals: Array<{
171
+ key: string;
172
+ manager: DaemonCdpManager;
173
+ reason: 'ide_closed' | 'target_closed' | 'target_rekeyed';
174
+ }> = [];
175
+
176
+ for (const [key, manager] of this.config.cdpManagers.entries()) {
177
+ if (!(key === ide || key.startsWith(`${ide}_`))) continue;
178
+ if (manager.getPort() !== port) continue;
179
+
180
+ if (targets.length === 0) {
181
+ removals.push({ key, manager, reason: 'ide_closed' });
182
+ continue;
183
+ }
184
+
185
+ if (manager.targetId && !trackedTargetIds.has(manager.targetId)) {
186
+ removals.push({ key, manager, reason: 'target_closed' });
187
+ continue;
188
+ }
189
+
190
+ if (key === ide && !manager.targetId && targets.length > 1) {
191
+ removals.push({ key, manager, reason: 'target_rekeyed' });
192
+ }
193
+ }
194
+
195
+ for (const { key, manager, reason } of removals) {
196
+ try { manager.disconnect(); } catch { /* noop */ }
197
+ this.config.cdpManagers.delete(key);
198
+ LOG.info('CDP', `Removed stale manager: ${key} (${reason})`);
199
+ await this.config.onDisconnected?.(ide, manager, key, reason);
200
+ }
201
+ }
202
+
156
203
  // ─── Periodic scanning ───
157
204
 
158
205
  /**
@@ -828,7 +828,10 @@ export class DaemonCdpManager {
828
828
  async attachToAgent(target: AgentWebviewTarget): Promise<string | null> {
829
829
  if (!this.isConnected) return null;
830
830
  for (const [sid, t] of this.agentSessions) {
831
- if (t.agentType === target.agentType) return sid;
831
+ if (t.targetId === target.targetId) return sid;
832
+ if (t.agentType === target.agentType && t.targetId !== target.targetId) {
833
+ await this.detachAgent(sid).catch(() => { });
834
+ }
832
835
  }
833
836
  try {
834
837
  // Attach via Browser WS (iframes can only be attached from browser-level)
@@ -971,6 +974,21 @@ export class DaemonCdpManager {
971
974
 
972
975
  private async getCurrentPageWebviewUrls(): Promise<Set<string>> {
973
976
  if (!this.isConnected) return new Set();
977
+
978
+ try {
979
+ const urls = new Set<string>();
980
+ const { frameTree } = await this.sendInternal('Page.getFrameTree', {}, 5000);
981
+ const visit = (node: any) => {
982
+ const url = node?.frame?.url;
983
+ if (typeof url === 'string' && url.includes('vscode-webview')) {
984
+ urls.add(url);
985
+ }
986
+ for (const child of node?.childFrames || []) visit(child);
987
+ };
988
+ if (frameTree) visit(frameTree);
989
+ if (urls.size > 0) return urls;
990
+ } catch { /* fall through to DOM scan */ }
991
+
974
992
  try {
975
993
  const raw = await this.evaluate(
976
994
  `JSON.stringify(Array.from(document.querySelectorAll('iframe,webview'))
package/src/cdp/setup.ts CHANGED
@@ -9,14 +9,13 @@ import { DaemonCdpManager } from './manager.js';
9
9
  import { ProviderLoader } from '../providers/provider-loader.js';
10
10
  import { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
11
11
  import { IdeProviderInstance } from '../providers/ide-provider-instance.js';
12
- import type { ProviderModule } from '../providers/contracts.js';
12
+ import { SessionRegistry } from '../sessions/registry.js';
13
13
 
14
14
  export interface CdpSetupContext {
15
15
  providerLoader: ProviderLoader;
16
16
  instanceManager: ProviderInstanceManager;
17
17
  cdpManagers: Map<string, DaemonCdpManager>;
18
- /** UUID instanceId → CDP manager key mapping */
19
- instanceIdMap: Map<string, string>;
18
+ sessionRegistry: SessionRegistry;
20
19
  /** Server connection (optional) */
21
20
  serverConn?: any;
22
21
  }
@@ -58,7 +57,7 @@ export function registerExtensionProviders(
58
57
  * 2. Create IdeProviderInstance
59
58
  * 3. Register in InstanceManager
60
59
  * 4. Register enabled extensions
61
- * 5. Update instanceIdMap (IDE + extension UUIDs)
60
+ * 5. Register runtime sessions (workspace + extension children)
62
61
  *
63
62
  * @returns The created IdeProviderInstance, or null if provider not found
64
63
  */
@@ -66,7 +65,7 @@ export async function setupIdeInstance(
66
65
  ctx: CdpSetupContext,
67
66
  opts: SetupIdeInstanceOptions,
68
67
  ): Promise<IdeProviderInstance | null> {
69
- const { providerLoader, instanceManager, instanceIdMap } = ctx;
68
+ const { providerLoader, instanceManager, sessionRegistry } = ctx;
70
69
  const { ideType, manager, settings } = opts;
71
70
  const managerKey = opts.managerKey || ideType;
72
71
 
@@ -91,18 +90,34 @@ export async function setupIdeInstance(
91
90
  settings: resolvedSettings,
92
91
  });
93
92
 
94
- // 5. Map IDE instance UUID → manager key
95
- instanceIdMap.set(ideInstance.getInstanceId(), managerKey);
93
+ // 5. Register workspace session
94
+ sessionRegistry.register({
95
+ sessionId: ideInstance.getInstanceId(),
96
+ parentSessionId: null,
97
+ providerType: ideType,
98
+ providerCategory: 'ide',
99
+ transport: 'cdp-page',
100
+ cdpManagerKey: managerKey,
101
+ instanceKey: `ide:${managerKey}`,
102
+ });
96
103
 
97
104
  // 6. Register enabled extensions
98
105
  const extensionProviders = providerLoader.getEnabledByCategory('extension', ideType);
99
106
  for (const extProvider of extensionProviders) {
100
107
  const extSettings = providerLoader.getSettings(extProvider.type);
101
108
  await ideInstance.addExtension(extProvider, extSettings);
102
- // Map extension UUIDs too (CDP uses parent IDE)
103
- for (const ext of ideInstance.getExtensionInstances()) {
104
- instanceIdMap.set(ext.getInstanceId(), managerKey);
105
- }
109
+ }
110
+
111
+ for (const ext of ideInstance.getExtensionInstances()) {
112
+ sessionRegistry.register({
113
+ sessionId: ext.getInstanceId(),
114
+ parentSessionId: ideInstance.getInstanceId(),
115
+ providerType: ext.type,
116
+ providerCategory: 'extension',
117
+ transport: 'cdp-webview',
118
+ cdpManagerKey: managerKey,
119
+ instanceKey: `ide:${managerKey}`,
120
+ });
106
121
  }
107
122
 
108
123
  return ideInstance;