@adhdev/daemon-core 0.9.82-rc.139 → 0.9.82-rc.140

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.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * MeshCoordinatorRegistry — Persisted record of active mesh coordinator sessions.
3
+ *
4
+ * Survives daemon restarts: when a CLI coordinator session is re-attached after
5
+ * a daemon restart the in-memory `settings.meshCoordinatorFor` on the provider
6
+ * instance is gone, so the registry file fills the gap.
7
+ *
8
+ * Keyed by sessionId (the CLI instance key / runtimeKey).
9
+ */
10
+ export interface CoordinatorRegistryEntry {
11
+ meshId: string;
12
+ sessionId: string;
13
+ workspace?: string;
14
+ startedAt: number;
15
+ }
16
+ /** Load persisted coordinator registry from disk into in-memory map. Called once on daemon boot. */
17
+ export declare function loadMeshCoordinatorRegistry(): void;
18
+ /** Register a coordinator session. Persists to disk immediately. */
19
+ export declare function registerMeshCoordinator(entry: CoordinatorRegistryEntry): void;
20
+ /** Remove a coordinator session by sessionId. Persists to disk. */
21
+ export declare function unregisterMeshCoordinator(sessionId: string): void;
22
+ /** Look up a coordinator entry by session ID. Returns undefined if not registered. */
23
+ export declare function getCoordinatorForSession(sessionId: string): CoordinatorRegistryEntry | undefined;
24
+ /** List all coordinator entries for a given workspace path. */
25
+ export declare function listCoordinatorsForWorkspace(workspace: string): CoordinatorRegistryEntry[];
@@ -299,6 +299,11 @@ export interface SessionEntry {
299
299
  seenCompletionMarker?: string;
300
300
  surfaceHidden?: boolean;
301
301
  settings?: Record<string, any>;
302
+ /** Set when this session is acting as a mesh coordinator for the given mesh. */
303
+ coordinator?: {
304
+ meshId: string;
305
+ role: 'coordinator';
306
+ };
302
307
  meshQueueStats?: {
303
308
  total?: number;
304
309
  active?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.139",
3
+ "version": "0.9.82-rc.140",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,6 +37,7 @@ import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
37
37
  import type { IdeProviderInstance } from '../providers/ide-provider-instance.js';
38
38
  import { createDefaultGitCommandServices } from '../git/git-commands.js';
39
39
  import { setupMeshEventForwarding } from '../mesh/mesh-events.js';
40
+ import { loadMeshCoordinatorRegistry } from '../mesh/coordinator-registry.js';
40
41
 
41
42
  // ─── Init Config ───
42
43
 
@@ -131,6 +132,7 @@ export interface DaemonDevSupportOptions {
131
132
  export async function initDaemonComponents(config: DaemonInitConfig): Promise<DaemonComponents> {
132
133
  // 1. Global log interceptor
133
134
  installGlobalInterceptor();
135
+ loadMeshCoordinatorRegistry();
134
136
 
135
137
  // 2. ProviderLoader (provider source mode from config.json)
136
138
  const appConfig = loadConfig();
@@ -107,6 +107,15 @@ export class CliStateEngine {
107
107
  // ── Approval ─────────────────────────────────────
108
108
  lastApprovalResolvedAt = 0;
109
109
  lastResolvedModalMessage = '';
110
+ /**
111
+ * When the engine previously held a modal but the latest parse failed
112
+ * to extract one, we record the timestamp here and only drop the modal
113
+ * after the configured `approvalCooldown` to avoid flapping between
114
+ * waiting_approval and generating on every Claude TUI redraw — that
115
+ * flapping is what fed auto-approve a fresh modal signature on each
116
+ * paint and made the engine type "1" repeatedly into the prompt.
117
+ */
118
+ modalLostAt = 0;
110
119
  private approvalExitTimeout: NodeJS.Timeout | null = null;
111
120
 
112
121
  // ── Response tracking ────────────────────────────
@@ -239,7 +248,17 @@ export class CliStateEngine {
239
248
  } catch { /* ignore parse failures */ }
240
249
  }
241
250
 
242
- if (!this.transport.isAlive() || (this.currentStatus !== 'waiting_approval' && !modal)) return;
251
+ if (!this.transport.isAlive()) return;
252
+ // (fix) Hard fail-safe: never write an approval key without a concrete
253
+ // modal that has buttons. The previous gate `currentStatus !== 'WA' &&
254
+ // !modal` let resolveModal proceed whenever status was still pinned to
255
+ // waiting_approval, even if parseApproval had returned null. Combined
256
+ // with auto-approve and Claude's modal flapping (status==WA but modal
257
+ // briefly null between paints) this typed "1" into the prompt over and
258
+ // over. Require a real modal with at least one button.
259
+ const buttonsValid = Array.isArray(modal?.buttons)
260
+ && modal.buttons.some((b: any) => typeof b === 'string' && b.trim());
261
+ if (!modal || !buttonsValid) return;
243
262
 
244
263
  const currentModalMessage = typeof modal?.message === 'string' ? modal.message.trim() : '';
245
264
  const inCooldown = !!this.lastApprovalResolvedAt
@@ -617,20 +636,30 @@ export class CliStateEngine {
617
636
  if (!modal) {
618
637
  LOG.warn('CLI', `[${this.provider.type}] detectStatus=waiting_approval but parseApproval returned null; ignoring`);
619
638
  // (fix) If we previously surfaced waiting_approval but the
620
- // modal extraction is now failing, do NOT keep the status
621
- // pinned to waiting_approval forever the dashboard would
622
- // show a "waiting" badge with no buttons (activeModal=null)
623
- // and the user perceives the agent as stuck. Drop the modal
624
- // and fall back to generating so the rest of the run can
625
- // settle normally; a future evaluate with a real modal will
626
- // re-enter waiting_approval cleanly.
627
- if (this.currentStatus === 'waiting_approval') {
628
- this.activeModal = null;
629
- this.setStatus('generating', 'approval_lost_modal');
630
- this.callbacks.onStatusChange();
639
+ // modal extraction is now failing, eventually drop the modal
640
+ // and fall back to generating so the dashboard doesn't show
641
+ // a "waiting" badge with no buttons. BUT defer the transition
642
+ // for a hysteresis window Claude TUI re-renders parts of
643
+ // the approval frame multiple times per second and the
644
+ // intermediate parses occasionally lose buttons for a single
645
+ // pass. Flapping the engine status WA → generating → WA →
646
+ // generating between paints fed auto-approve a fresh modal
647
+ // signature each time, which typed the approval key
648
+ // ("1") into the prompt repeatedly. Wait for the modal to
649
+ // stay gone for `approvalCooldown` before clearing.
650
+ if (this.currentStatus === 'waiting_approval' && this.activeModal) {
651
+ const lostAt = this.modalLostAt || Date.now();
652
+ if (!this.modalLostAt) this.modalLostAt = lostAt;
653
+ if (Date.now() - lostAt >= this.timeouts.approvalCooldown) {
654
+ this.activeModal = null;
655
+ this.modalLostAt = 0;
656
+ this.setStatus('generating', 'approval_lost_modal');
657
+ this.callbacks.onStatusChange();
658
+ }
631
659
  }
632
660
  return;
633
661
  }
662
+ this.modalLostAt = 0;
634
663
  this.isWaitingForResponse = true;
635
664
  this.setStatus('waiting_approval', 'script_detect');
636
665
  // (fix) Don't overwrite an already-captured modal with a fresh
@@ -846,22 +846,19 @@ export class ProviderCliAdapter implements CliAdapter {
846
846
  if (liveDetect === 'waiting_approval') {
847
847
  const liveModal = this.runParseApproval(this.terminalScreen.getText())
848
848
  || this.runParseApproval(this.recentOutputBuffer);
849
- if (liveModal) {
849
+ // (fix) Only surface modals that have at least one non-empty
850
+ // button. Half-rendered approval frames produced
851
+ // { message, buttons: [] } briefly — auto-approve would then
852
+ // pick buttonIndex=-1 and resolveModal would write the
853
+ // provider's index-0 key into the prompt. Skip those frames
854
+ // and let a later evaluate find the complete modal.
855
+ const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
856
+ && liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
857
+ if (liveModal && buttonsOk) {
850
858
  effectiveModal = liveModal;
851
- // Promote so subsequent calls don't re-walk the buffer.
852
- // Only set if engine hasn't already captured one — keeps
853
- // the first stable modal as authoritative.
854
859
  if (!this.engine.activeModal) this.engine.activeModal = liveModal;
855
- } else {
856
- LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: detect=waiting_approval but parseApproval still null (recentLen=${this.recentOutputBuffer.length} screenLen=${this.terminalScreen.getText().length})`);
857
860
  }
858
- } else if (liveDetect && liveDetect !== 'generating' && liveDetect !== 'idle') {
859
- LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: detect=${liveDetect} (not waiting_approval)`);
860
- } else if (this.engine.currentStatus === 'waiting_approval' && liveDetect !== 'waiting_approval') {
861
- LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: engine.status=waiting_approval but live detect=${liveDetect}`);
862
861
  }
863
- } else if (!effectiveModal && this.engine.currentStatus === 'waiting_approval') {
864
- LOG.warn('CLI', `[${this.cliType}] getStatus skipped live re-extract: allowParse=${allowParse} isWaitingForResponse=${this.engine.isWaitingForResponse}`);
865
862
  }
866
863
  // Only surface waiting_approval when we ALSO have a concrete modal
867
864
  // (message + buttons). detectStatus alone can fire while parseApproval
@@ -1844,7 +1841,12 @@ export class ProviderCliAdapter implements CliAdapter {
1844
1841
  if (parsedDebugState?.status === 'error') {
1845
1842
  effectiveStatus = 'error';
1846
1843
  }
1847
- if (startupDetectedStatus === 'waiting_approval') {
1844
+ // (fix) Mirror the getStatus contract: never surface waiting_approval
1845
+ // without a concrete modal. detectStatus alone can fire while
1846
+ // parseApproval is still null, and a bare debugState waiting_approval
1847
+ // confused dashboards reading the debug payload.
1848
+ const debugEffectiveModal = startupModal || this.engine.activeModal;
1849
+ if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
1848
1850
  effectiveStatus = 'waiting_approval';
1849
1851
  }
1850
1852
  if (
@@ -18,6 +18,7 @@ import { loadConfig } from '../config/config.js';
18
18
  import { loadState, saveState } from '../config/state-store.js';
19
19
  import { getWorkspaceState, resolveLaunchDirectory } from '../config/workspaces.js';
20
20
  import { appendRecentActivity } from '../config/recent-activity.js';
21
+ import { unregisterMeshCoordinator } from '../mesh/coordinator-registry.js';
21
22
  import { upsertSavedProviderSession } from '../config/saved-sessions.js';
22
23
  import { buildLegacyModelModeSummaryMetadata, normalizeProviderSummaryMetadata } from '../providers/summary-metadata.js';
23
24
  import { CliProviderInstance } from '../providers/cli-provider-instance.js';
@@ -577,6 +578,7 @@ export class DaemonCliManager {
577
578
  this.deps.removeAgentTracking(key);
578
579
  sessionRegistry?.unregisterByInstanceKey(key);
579
580
  instanceManager?.removeInstance(key);
581
+ unregisterMeshCoordinator(key);
580
582
  LOG.info('CLI', `🧹 Auto-cleaned ${status.status} CLI: ${cliType}`);
581
583
  this.deps.onStatusChange();
582
584
  }
@@ -903,6 +905,7 @@ export class DaemonCliManager {
903
905
  this.deps.removeAgentTracking(key);
904
906
  this.deps.getSessionRegistry?.()?.unregisterByInstanceKey(key);
905
907
  this.deps.getInstanceManager()?.removeInstance(key);
908
+ unregisterMeshCoordinator(key);
906
909
  LOG.info('CLI', `🛑 Agent stopped: ${adapter.cliType} in ${adapter.workingDir}`);
907
910
  this.deps.onStatusChange();
908
911
  } else {
@@ -912,6 +915,7 @@ export class DaemonCliManager {
912
915
  this.deps.getSessionRegistry?.()?.unregisterByInstanceKey(key);
913
916
  im.removeInstance(key);
914
917
  this.deps.removeAgentTracking(key);
918
+ unregisterMeshCoordinator(key);
915
919
  LOG.warn('CLI', `🧹 Force-removed orphan entry: ${key}`);
916
920
  this.deps.onStatusChange();
917
921
  }
@@ -38,6 +38,7 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
38
38
  import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
39
39
  import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
40
40
  import { buildSessionEntries } from '../status/builders.js';
41
+ import { registerMeshCoordinator } from '../mesh/coordinator-registry.js';
41
42
  import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
42
43
  import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
43
44
  import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
@@ -5329,11 +5330,15 @@ export class DaemonCommandRouter {
5329
5330
  }
5330
5331
 
5331
5332
  LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
5333
+ const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
5334
+ if (cliCmdSessionId) {
5335
+ registerMeshCoordinator({ meshId, sessionId: cliCmdSessionId, workspace, startedAt: Date.now() });
5336
+ }
5332
5337
  try {
5333
5338
  const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
5334
5339
  appendLedgerEntry(meshId, {
5335
5340
  kind: 'coordinator_started',
5336
- sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
5341
+ sessionId: cliCmdSessionId,
5337
5342
  providerType: cliType,
5338
5343
  payload: { workspace },
5339
5344
  });
@@ -5344,7 +5349,7 @@ export class DaemonCommandRouter {
5344
5349
  meshId,
5345
5350
  cliType,
5346
5351
  workspace,
5347
- sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
5352
+ sessionId: cliCmdSessionId,
5348
5353
  mcpRegistered: true,
5349
5354
  };
5350
5355
  }
@@ -5510,13 +5515,17 @@ export class DaemonCommandRouter {
5510
5515
  }
5511
5516
 
5512
5517
  LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
5518
+ const launchSessionId = launchResult.sessionId || launchResult.id;
5519
+ if (launchSessionId) {
5520
+ registerMeshCoordinator({ meshId, sessionId: launchSessionId, workspace, startedAt: Date.now() });
5521
+ }
5513
5522
 
5514
5523
  // Record coordinator launch in task ledger
5515
5524
  try {
5516
5525
  const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
5517
5526
  appendLedgerEntry(meshId, {
5518
5527
  kind: 'coordinator_started',
5519
- sessionId: launchResult.sessionId || launchResult.id,
5528
+ sessionId: launchSessionId,
5520
5529
  providerType: cliType,
5521
5530
  payload: { workspace },
5522
5531
  });
@@ -5527,7 +5536,7 @@ export class DaemonCommandRouter {
5527
5536
  meshId,
5528
5537
  cliType,
5529
5538
  workspace,
5530
- sessionId: launchResult.sessionId || launchResult.id,
5539
+ sessionId: launchSessionId,
5531
5540
  mcpConfigWritten: true,
5532
5541
  };
5533
5542
  } catch (e: any) {
@@ -272,6 +272,18 @@ export function getConfigDir(): string {
272
272
  return dir;
273
273
  }
274
274
 
275
+ /**
276
+ * Get the daemon runtime data directory (~/.adhdev/daemon/).
277
+ * Distinct from the user-config dir so runtime state can be cleared independently.
278
+ */
279
+ export function getDaemonDataDir(): string {
280
+ const dir = join(getConfigDir(), 'daemon');
281
+ if (!existsSync(dir)) {
282
+ mkdirSync(dir, { recursive: true });
283
+ }
284
+ return dir;
285
+ }
286
+
275
287
  /**
276
288
  * Get the config file path
277
289
  */
package/src/index.ts CHANGED
@@ -143,7 +143,7 @@ export type RecentSessionBucket = 'needs_attention' | 'working' | 'task_complete
143
143
  export type { IDaemonCore, DaemonCoreOptions } from './daemon-core.js';
144
144
 
145
145
  // ── Config ──
146
- export { loadConfig, saveConfig, resetConfig, isSetupComplete, markSetupComplete, updateConfig } from './config/config.js';
146
+ export { loadConfig, saveConfig, resetConfig, isSetupComplete, markSetupComplete, updateConfig, getDaemonDataDir } from './config/config.js';
147
147
  export { getWorkspaceState } from './config/workspaces.js';
148
148
  export { appendRecentActivity, getRecentActivity } from './config/recent-activity.js';
149
149
  export type { RecentActivityEntry } from './config/recent-activity.js';
@@ -160,6 +160,8 @@ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './con
160
160
  // ── Mesh Coordinator ──
161
161
  export { buildCoordinatorSystemPrompt } from './mesh/coordinator-prompt.js';
162
162
  export type { CoordinatorPromptContext } from './mesh/coordinator-prompt.js';
163
+ export { loadMeshCoordinatorRegistry, registerMeshCoordinator, unregisterMeshCoordinator, getCoordinatorForSession, listCoordinatorsForWorkspace } from './mesh/coordinator-registry.js';
164
+ export type { CoordinatorRegistryEntry } from './mesh/coordinator-registry.js';
163
165
  export {
164
166
  MESH_REFINE_CONFIG_LOCATIONS,
165
167
  MESH_REFINE_CONFIG_SCHEMA,
@@ -0,0 +1,75 @@
1
+ /**
2
+ * MeshCoordinatorRegistry — Persisted record of active mesh coordinator sessions.
3
+ *
4
+ * Survives daemon restarts: when a CLI coordinator session is re-attached after
5
+ * a daemon restart the in-memory `settings.meshCoordinatorFor` on the provider
6
+ * instance is gone, so the registry file fills the gap.
7
+ *
8
+ * Keyed by sessionId (the CLI instance key / runtimeKey).
9
+ */
10
+
11
+ import { join } from 'path';
12
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
13
+ import { getDaemonDataDir } from '../config/config.js';
14
+
15
+ export interface CoordinatorRegistryEntry {
16
+ meshId: string;
17
+ sessionId: string;
18
+ workspace?: string;
19
+ startedAt: number;
20
+ }
21
+
22
+ const _registry = new Map<string, CoordinatorRegistryEntry>();
23
+
24
+ function getRegistryPath(): string {
25
+ return join(getDaemonDataDir(), 'mesh-coordinators.json');
26
+ }
27
+
28
+ /** Load persisted coordinator registry from disk into in-memory map. Called once on daemon boot. */
29
+ export function loadMeshCoordinatorRegistry(): void {
30
+ const path = getRegistryPath();
31
+ if (!existsSync(path)) return;
32
+ try {
33
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
34
+ if (!Array.isArray(raw)) return;
35
+ _registry.clear();
36
+ for (const entry of raw) {
37
+ if (typeof entry?.sessionId === 'string' && typeof entry?.meshId === 'string') {
38
+ _registry.set(entry.sessionId, entry as CoordinatorRegistryEntry);
39
+ }
40
+ }
41
+ } catch { /* ignore corrupt file */ }
42
+ }
43
+
44
+ function saveRegistry(): void {
45
+ try {
46
+ writeFileSync(
47
+ getRegistryPath(),
48
+ JSON.stringify([..._registry.values()], null, 2),
49
+ { encoding: 'utf-8', mode: 0o600 },
50
+ );
51
+ } catch { /* best-effort */ }
52
+ }
53
+
54
+ /** Register a coordinator session. Persists to disk immediately. */
55
+ export function registerMeshCoordinator(entry: CoordinatorRegistryEntry): void {
56
+ _registry.set(entry.sessionId, entry);
57
+ saveRegistry();
58
+ }
59
+
60
+ /** Remove a coordinator session by sessionId. Persists to disk. */
61
+ export function unregisterMeshCoordinator(sessionId: string): void {
62
+ if (_registry.delete(sessionId)) {
63
+ saveRegistry();
64
+ }
65
+ }
66
+
67
+ /** Look up a coordinator entry by session ID. Returns undefined if not registered. */
68
+ export function getCoordinatorForSession(sessionId: string): CoordinatorRegistryEntry | undefined {
69
+ return _registry.get(sessionId);
70
+ }
71
+
72
+ /** List all coordinator entries for a given workspace path. */
73
+ export function listCoordinatorsForWorkspace(workspace: string): CoordinatorRegistryEntry[] {
74
+ return [..._registry.values()].filter(e => e.workspace === workspace);
75
+ }
@@ -1052,10 +1052,27 @@ export class CliProviderInstance implements ProviderInstance {
1052
1052
  return autoApproveActive;
1053
1053
  }
1054
1054
  const modal = adapterStatus.activeModal;
1055
- const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
1055
+ // (fix) Do not auto-approve when no concrete modal/buttons are present.
1056
+ // Claude TUI flaps between paints; without this guard adapterStatus
1057
+ // could report status=waiting_approval with activeModal=null (or with
1058
+ // an empty buttons array briefly) and we'd still call
1059
+ // resolveModal(-1) — which used to type "1" into the prompt
1060
+ // repeatedly. Skip until a real modal is captured.
1061
+ const buttons = Array.isArray(modal?.buttons)
1062
+ ? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
1063
+ : [];
1064
+ if (!modal || buttons.length === 0) {
1065
+ return autoApproveActive;
1066
+ }
1067
+ const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(buttons, this.provider);
1068
+ if (buttonIndex < 0) {
1069
+ // No positive button matched — don't pick a random index, just
1070
+ // surface the modal so the user can decide.
1071
+ return autoApproveActive;
1072
+ }
1056
1073
  const signature = [
1057
1074
  typeof modal?.message === 'string' ? modal.message.trim() : '',
1058
- Array.isArray(modal?.buttons) ? modal.buttons.join('|') : '',
1075
+ buttons.join('|'),
1059
1076
  buttonIndex,
1060
1077
  ].join('::');
1061
1078
  if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
@@ -396,6 +396,8 @@ export interface SessionEntry {
396
396
  seenCompletionMarker?: string;
397
397
  surfaceHidden?: boolean;
398
398
  settings?: Record<string, any>;
399
+ /** Set when this session is acting as a mesh coordinator for the given mesh. */
400
+ coordinator?: { meshId: string; role: 'coordinator' };
399
401
  meshQueueStats?: {
400
402
  total?: number;
401
403
  active?: number;
@@ -25,6 +25,7 @@ import {
25
25
  type NormalizeActiveChatOptions,
26
26
  } from './normalize.js';
27
27
  import { getMeshQueueStats } from '../mesh/mesh-work-queue.js';
28
+ import { getCoordinatorForSession } from '../mesh/coordinator-registry.js';
28
29
  import { normalizeProviderStateControlValues } from '../providers/provider-patch-state.js';
29
30
  import { normalizeProviderSummaryMetadata } from '../providers/summary-metadata.js';
30
31
  import {
@@ -164,7 +165,7 @@ const ACP_SESSION_CAPABILITIES: SessionCapability[] = [
164
165
  'set_thought_level',
165
166
  ];
166
167
 
167
- function buildIdeWorkspaceSession(
168
+ function buildWorkspaceSession(
168
169
  state: IdeProviderState,
169
170
  cdpManagers: Map<string, DaemonCdpManager>,
170
171
  options: SessionEntryBuildOptions,
@@ -179,7 +180,10 @@ function buildIdeWorkspaceSession(
179
180
  const git = getGitSummaryForWorkspace(workspace, options);
180
181
  const title = activeChat?.title || state.name;
181
182
  const meshCoordinatorFor = state.settings?.meshCoordinatorFor as string | undefined;
182
- const meshQueueStats = meshCoordinatorFor ? getMeshQueueStats(meshCoordinatorFor) : undefined;
183
+ const registryEntry = state.instanceId ? getCoordinatorForSession(state.instanceId) : undefined;
184
+ const effectiveMeshId = meshCoordinatorFor || registryEntry?.meshId;
185
+ const coordinator = effectiveMeshId ? { meshId: effectiveMeshId, role: 'coordinator' as const } : undefined;
186
+ const meshQueueStats = effectiveMeshId ? getMeshQueueStats(effectiveMeshId) : undefined;
183
187
  return {
184
188
  id: state.instanceId || state.type,
185
189
  parentId: null,
@@ -203,6 +207,7 @@ function buildIdeWorkspaceSession(
203
207
  errorReason: state.errorReason,
204
208
  lastUpdated: state.lastUpdated,
205
209
  settings: state.settings,
210
+ ...(coordinator && { coordinator }),
206
211
  ...(meshQueueStats && { meshQueueStats }),
207
212
  };
208
213
  }
@@ -221,7 +226,10 @@ function buildExtensionAgentSession(
221
226
  const workspace = parent.workspace || null;
222
227
  const git = getGitSummaryForWorkspace(workspace, options);
223
228
  const meshCoordinatorFor = ext.settings?.meshCoordinatorFor as string | undefined;
224
- const meshQueueStats = meshCoordinatorFor ? getMeshQueueStats(meshCoordinatorFor) : undefined;
229
+ const registryEntry = ext.instanceId ? getCoordinatorForSession(ext.instanceId) : undefined;
230
+ const effectiveMeshId = meshCoordinatorFor || registryEntry?.meshId;
231
+ const coordinator = effectiveMeshId ? { meshId: effectiveMeshId, role: 'coordinator' as const } : undefined;
232
+ const meshQueueStats = effectiveMeshId ? getMeshQueueStats(effectiveMeshId) : undefined;
225
233
  return {
226
234
  id: ext.instanceId || `${parent.instanceId}:${ext.type}`,
227
235
  parentId: parent.instanceId || parent.type,
@@ -245,6 +253,7 @@ function buildExtensionAgentSession(
245
253
  errorReason: ext.errorReason,
246
254
  lastUpdated: ext.lastUpdated,
247
255
  settings: ext.settings,
256
+ ...(coordinator && { coordinator }),
248
257
  ...(meshQueueStats && { meshQueueStats }),
249
258
  };
250
259
  }
@@ -287,7 +296,10 @@ function buildCliSession(state: CliProviderState, options: SessionEntryBuildOpti
287
296
  const workspace = state.workspace || null;
288
297
  const git = getGitSummaryForWorkspace(workspace, options);
289
298
  const meshCoordinatorFor = state.settings?.meshCoordinatorFor as string | undefined;
290
- const meshQueueStats = meshCoordinatorFor ? getMeshQueueStats(meshCoordinatorFor) : undefined;
299
+ const registryEntry = state.instanceId ? getCoordinatorForSession(state.instanceId) : undefined;
300
+ const effectiveMeshId = meshCoordinatorFor || registryEntry?.meshId;
301
+ const coordinator = effectiveMeshId ? { meshId: effectiveMeshId, role: 'coordinator' as const } : undefined;
302
+ const meshQueueStats = effectiveMeshId ? getMeshQueueStats(effectiveMeshId) : undefined;
291
303
  return {
292
304
  id: state.instanceId,
293
305
  parentId: null,
@@ -327,6 +339,7 @@ function buildCliSession(state: CliProviderState, options: SessionEntryBuildOpti
327
339
  errorReason: state.errorReason,
328
340
  lastUpdated: state.lastUpdated,
329
341
  settings: state.settings,
342
+ ...(coordinator && { coordinator }),
330
343
  ...(meshQueueStats && { meshQueueStats }),
331
344
  };
332
345
  }
@@ -341,7 +354,10 @@ function buildAcpSession(state: AcpProviderState, options: SessionEntryBuildOpti
341
354
  const workspace = state.workspace || null;
342
355
  const git = getGitSummaryForWorkspace(workspace, options);
343
356
  const meshCoordinatorFor = state.settings?.meshCoordinatorFor as string | undefined;
344
- const meshQueueStats = meshCoordinatorFor ? getMeshQueueStats(meshCoordinatorFor) : undefined;
357
+ const registryEntry = state.instanceId ? getCoordinatorForSession(state.instanceId) : undefined;
358
+ const effectiveMeshId = meshCoordinatorFor || registryEntry?.meshId;
359
+ const coordinator = effectiveMeshId ? { meshId: effectiveMeshId, role: 'coordinator' as const } : undefined;
360
+ const meshQueueStats = effectiveMeshId ? getMeshQueueStats(effectiveMeshId) : undefined;
345
361
  return {
346
362
  id: state.instanceId,
347
363
  parentId: null,
@@ -364,6 +380,7 @@ function buildAcpSession(state: AcpProviderState, options: SessionEntryBuildOpti
364
380
  errorReason: state.errorReason,
365
381
  lastUpdated: state.lastUpdated,
366
382
  settings: state.settings,
383
+ ...(coordinator && { coordinator }),
367
384
  ...(meshQueueStats && { meshQueueStats }),
368
385
  };
369
386
  }
@@ -380,7 +397,7 @@ export function buildSessionEntries(
380
397
  const acpStates = allStates.filter((s): s is AcpProviderState => s.category === 'acp');
381
398
 
382
399
  for (const state of ideStates) {
383
- sessions.push(buildIdeWorkspaceSession(state, cdpManagers, options));
400
+ sessions.push(buildWorkspaceSession(state, cdpManagers, options));
384
401
  for (const ext of state.extensions as ExtensionProviderState[]) {
385
402
  if (!shouldIncludeExtensionSession(ext)) continue;
386
403
  sessions.push(buildExtensionAgentSession(state, ext, options));