@adhdev/daemon-core 0.8.75 → 0.8.76

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.75",
3
+ "version": "0.8.76",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.75",
3
+ "version": "0.8.76",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -181,10 +181,17 @@ export class DaemonAgentStreamManager {
181
181
  }
182
182
 
183
183
  /** Collect active extension session state */
184
- async collectActiveSession(cdp: DaemonCdpManager, parentSessionId: string): Promise<AgentStreamState | null> {
184
+ async collectActiveSession(
185
+ cdp: DaemonCdpManager,
186
+ parentSessionId: string,
187
+ attemptedSessionIds: Set<string> = new Set(),
188
+ originSessionId?: string,
189
+ ): Promise<AgentStreamState | null> {
185
190
  if (!this.enabled) return null;
186
191
  const activeSessionId = this.getActiveSessionId(parentSessionId);
187
192
  if (!activeSessionId) return null;
193
+ const resolvedOriginSessionId = originSessionId || activeSessionId;
194
+ attemptedSessionIds.add(activeSessionId);
188
195
  let agent = this.managedBySessionId.get(activeSessionId);
189
196
  if (!agent) {
190
197
  agent = await this.connectManagedSession(cdp, parentSessionId, activeSessionId) || undefined;
@@ -200,18 +207,50 @@ export class DaemonAgentStreamManager {
200
207
  const evaluate: AgentEvaluateFn = (expr, timeout) =>
201
208
  cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
202
209
  const state = await agent.adapter.readChat(evaluate);
203
- const stateError = this.getStateError(state);
204
- const selectedModelValue = typeof state.controlValues?.model === 'string' ? state.controlValues.model : '';
205
- LOG.debug('AgentStream', `[AgentStream] readChat(${type}) result: status=${state.status} msgs=${state.messages?.length || 0} model=${selectedModelValue}${state.status === 'error' ? ' error=' + JSON.stringify(stateError) : ''}`);
206
- if (state.status === 'error' && this.isRecoverableSessionError(stateError)) {
210
+ const resolvedProviderSessionId = typeof state.providerSessionId === 'string' && state.providerSessionId.trim()
211
+ ? state.providerSessionId.trim()
212
+ : (typeof state.sessionId === 'string' && state.sessionId.trim() && state.sessionId !== agent.runtimeSessionId
213
+ ? state.sessionId.trim()
214
+ : undefined);
215
+ const normalizedState: AgentStreamState = {
216
+ ...state,
217
+ sessionId: agent.runtimeSessionId,
218
+ ...(resolvedProviderSessionId ? { providerSessionId: resolvedProviderSessionId } : {}),
219
+ };
220
+ const stateError = this.getStateError(normalizedState);
221
+ const selectedModelValue = typeof normalizedState.controlValues?.model === 'string' ? normalizedState.controlValues.model : '';
222
+ LOG.debug('AgentStream', `[AgentStream] readChat(${type}) result: status=${normalizedState.status} msgs=${normalizedState.messages?.length || 0} model=${selectedModelValue}${normalizedState.status === 'error' ? ' error=' + JSON.stringify(stateError) : ''}`);
223
+ if (normalizedState.status === 'error' && this.isRecoverableSessionError(stateError)) {
207
224
  throw new Error(stateError);
208
225
  }
209
- agent.lastState = state;
226
+ agent.lastState = normalizedState;
210
227
  agent.lastError = null;
211
- if (state.status === 'panel_hidden') {
228
+ if (normalizedState.status === 'panel_hidden') {
229
+ const discovered = await cdp.discoverAgentWebviews().catch(() => [] as AgentWebviewTarget[]);
230
+ const fallbackTarget = discovered.find((entry) => {
231
+ if (entry.agentType === type) return false;
232
+ const fallbackSessionId = this.resolveSessionIdForTarget(parentSessionId, entry.agentType);
233
+ return !!fallbackSessionId
234
+ && fallbackSessionId !== activeSessionId
235
+ && !attemptedSessionIds.has(fallbackSessionId);
236
+ });
237
+ if (fallbackTarget) {
238
+ const fallbackSessionId = this.resolveSessionIdForTarget(parentSessionId, fallbackTarget.agentType);
239
+ if (fallbackSessionId && fallbackSessionId !== activeSessionId && !attemptedSessionIds.has(fallbackSessionId)) {
240
+ this.logFn(`[AgentStream] Active session ${type} is hidden; switching to visible agent ${fallbackTarget.agentType} (${parentSessionId})`);
241
+ await this.setActiveSession(cdp, parentSessionId, fallbackSessionId);
242
+ await this.syncActiveSession(cdp, parentSessionId);
243
+ const fallbackState = await this.collectActiveSession(cdp, parentSessionId, attemptedSessionIds, resolvedOriginSessionId);
244
+ if (fallbackState?.status === 'panel_hidden' && resolvedOriginSessionId !== fallbackSessionId) {
245
+ await this.setActiveSession(cdp, parentSessionId, resolvedOriginSessionId);
246
+ await this.syncActiveSession(cdp, parentSessionId);
247
+ }
248
+ return fallbackState;
249
+ }
250
+ }
212
251
  agent.lastHiddenCheckTime = Date.now();
213
252
  }
214
- return state;
253
+ return normalizedState;
215
254
  } catch (e) {
216
255
  const errorMsg = (e as Error)?.message || String(e);
217
256
  this.logFn(`[AgentStream] readChat(${type}) error: ${errorMsg.slice(0, 200)}`);
@@ -206,6 +206,7 @@ export class AgentStreamPoller {
206
206
  try {
207
207
  await agentStreamManager.syncActiveSession(cdp, parentSessionId);
208
208
  let stream = await agentStreamManager.collectActiveSession(cdp, parentSessionId);
209
+ resolvedActiveSessionId = stream?.sessionId || agentStreamManager.getActiveSessionId(parentSessionId) || resolvedActiveSessionId;
209
210
  if (stream?.status === 'waiting_approval') {
210
211
  const autoApprove = providerLoader.getSettings(stream.agentType).autoApprove !== false;
211
212
  if (autoApprove && resolvedActiveSessionId) {
@@ -16,7 +16,7 @@ import * as fs from 'fs';
16
16
  import { LOG } from '../logging/logger.js';
17
17
  import type { CdpTargetFilter } from '../providers/contracts.js';
18
18
 
19
- interface CdpTarget {
19
+ export interface CdpTarget {
20
20
  id: string;
21
21
  type: string;
22
22
  title: string;
@@ -24,6 +24,44 @@ interface CdpTarget {
24
24
  webSocketDebuggerUrl: string;
25
25
  }
26
26
 
27
+ function normalizeTitle(value: string | null | undefined): string {
28
+ return String(value || '').trim().replace(/\s+/g, ' ').toLowerCase();
29
+ }
30
+
31
+ function titlesMatch(lhs: string | null | undefined, rhs: string | null | undefined): boolean {
32
+ const a = normalizeTitle(lhs);
33
+ const b = normalizeTitle(rhs);
34
+ if (!a || !b) return false;
35
+ return a === b || a.includes(b) || b.includes(a);
36
+ }
37
+
38
+ export function resolveCdpPageTarget(params: {
39
+ pages: CdpTarget[];
40
+ pinnedTargetId?: string | null;
41
+ previousPageTitle?: string | null;
42
+ }): { target: CdpTarget | null; retargeted: boolean } {
43
+ const { pages, pinnedTargetId, previousPageTitle } = params;
44
+ if (pages.length === 0) return { target: null, retargeted: false };
45
+
46
+ if (!pinnedTargetId) {
47
+ return { target: pages[0] || null, retargeted: false };
48
+ }
49
+
50
+ const exact = pages.find((page) => page.id === pinnedTargetId);
51
+ if (exact) return { target: exact, retargeted: false };
52
+
53
+ const titleMatchesList = pages.filter((page) => titlesMatch(page.title, previousPageTitle));
54
+ if (titleMatchesList.length === 1) {
55
+ return { target: titleMatchesList[0], retargeted: true };
56
+ }
57
+
58
+ if (pages.length === 1) {
59
+ return { target: pages[0], retargeted: true };
60
+ }
61
+
62
+ return { target: null, retargeted: false };
63
+ }
64
+
27
65
  export interface AgentWebviewTarget {
28
66
  targetId: string;
29
67
  extensionId: string;
@@ -204,22 +242,36 @@ export class DaemonCdpManager {
204
242
  return;
205
243
  }
206
244
 
207
- // Exclude non-main tabs
208
- const mainPages = pages.filter(t => !this.isNonMainTitle(t.title || ''));
209
- const list = mainPages.length > 0 ? mainPages : pages;
245
+ // Keep reconnect target selection aligned with initial scan rules:
246
+ // prefer visible workbench pages that satisfy provider URL filters,
247
+ // then fall back to title-filtered pages only when nothing else matches.
248
+ const titleFilteredPages = pages.filter(t => !this.isNonMainTitle(t.title || ''));
249
+ const mainPages = titleFilteredPages.filter(t => this.isMainPageUrl(t.url));
250
+ const list = mainPages.length > 0
251
+ ? mainPages
252
+ : (titleFilteredPages.length > 0 ? titleFilteredPages : pages);
210
253
 
211
254
  this.log(`[CDP] pages(${list.length}): ${list.map(t => `"${t.title}"`).join(', ')}`);
212
255
 
213
- // If targetId is specified, select only matching page
214
- if (this._targetId) {
215
- const specific = list.find(t => t.id === this._targetId);
216
- if (specific) {
217
- this._pageTitle = specific.title || '';
218
- resolve(specific);
219
- } else {
220
- this.log(`[CDP] Target ${this._targetId} not found in page list`);
221
- resolve(null);
256
+ const previousTargetId = this._targetId;
257
+ const selected = resolveCdpPageTarget({
258
+ pages: list,
259
+ pinnedTargetId: previousTargetId,
260
+ previousPageTitle: this._pageTitle,
261
+ });
262
+ if (selected.target) {
263
+ if (selected.retargeted && previousTargetId && previousTargetId !== selected.target.id) {
264
+ this.log(`[CDP] Target ${previousTargetId} rekeyed to ${selected.target.id}`);
265
+ this._targetId = selected.target.id;
222
266
  }
267
+ this._pageTitle = selected.target.title || '';
268
+ resolve(selected.target);
269
+ return;
270
+ }
271
+
272
+ if (previousTargetId) {
273
+ this.log(`[CDP] Target ${previousTargetId} not found in page list`);
274
+ resolve(null);
223
275
  return;
224
276
  }
225
277
 
@@ -0,0 +1,26 @@
1
+ export interface AsyncBatchOptions {
2
+ concurrency?: number;
3
+ }
4
+
5
+ export async function runAsyncBatch<T>(
6
+ items: Iterable<T>,
7
+ worker: (item: T, index: number) => Promise<void>,
8
+ options: AsyncBatchOptions = {},
9
+ ): Promise<void> {
10
+ const list = Array.from(items);
11
+ if (list.length === 0) return;
12
+
13
+ const concurrency = Math.max(1, Math.min(list.length, Math.floor(options.concurrency || 1)));
14
+ let nextIndex = 0;
15
+
16
+ const runners = Array.from({ length: concurrency }, async () => {
17
+ while (true) {
18
+ const currentIndex = nextIndex;
19
+ nextIndex += 1;
20
+ if (currentIndex >= list.length) return;
21
+ await worker(list[currentIndex], currentIndex);
22
+ }
23
+ });
24
+
25
+ await Promise.all(runners);
26
+ }
@@ -301,8 +301,11 @@ function applyProviderPatch(h: CommandHelpers, args: any, payload: any): void {
301
301
  }
302
302
 
303
303
  async function executeProviderScript(h: CommandHelpers, args: any, scriptName: string): Promise<CommandResult> {
304
+ const explicitTargetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
305
+ const targetSession = explicitTargetSessionId ? h.ctx.sessionRegistry?.get(explicitTargetSessionId) : undefined;
304
306
  const resolvedProviderType =
305
- h.currentSession?.providerType
307
+ targetSession?.providerType
308
+ || h.currentSession?.providerType
306
309
  || h.currentProviderType
307
310
  || args?.agentType
308
311
  || args?.providerType;
@@ -358,8 +361,8 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
358
361
  if (!scriptCode) return { success: false, error: `Script '${actualScriptName}' returned null` };
359
362
 
360
363
  const cdpKey = provider.category === 'ide'
361
- ? (h.currentSession?.cdpManagerKey || h.currentManagerKey || resolvedProviderType)
362
- : (h.currentSession?.cdpManagerKey || h.currentManagerKey);
364
+ ? (targetSession?.cdpManagerKey || h.currentSession?.cdpManagerKey || h.currentManagerKey || resolvedProviderType)
365
+ : (targetSession?.cdpManagerKey || h.currentSession?.cdpManagerKey || h.currentManagerKey);
363
366
  LOG.info('Command', `[ExtScript] provider=${provider.type} category=${provider.category} cdpKey=${cdpKey}`);
364
367
  const cdp = h.getCdp(cdpKey);
365
368
  if (!cdp?.isConnected) return { success: false, error: `No CDP connection for ${cdpKey || 'any'}` };
@@ -368,9 +371,9 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
368
371
  let result: unknown;
369
372
 
370
373
  if (provider.category === 'extension') {
371
- const runtimeSessionId = h.currentSession?.sessionId || args?.targetSessionId;
374
+ const runtimeSessionId = explicitTargetSessionId || h.currentSession?.sessionId;
372
375
  if (!runtimeSessionId) return { success: false, error: `No target session found for ${resolvedProviderType}` };
373
- const parentSessionId = h.currentSession?.parentSessionId;
376
+ const parentSessionId = targetSession?.parentSessionId || h.currentSession?.parentSessionId;
374
377
  if (parentSessionId) {
375
378
  await h.agentStream?.setActiveSession(cdp, parentSessionId, runtimeSessionId);
376
379
  await h.agentStream?.syncActiveSession(cdp, parentSessionId);