@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.
- package/dist/cli-adapters/cli-state-engine.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +255 -140
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +218 -109
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-registry.d.ts +25 -0
- package/dist/shared-types.d.ts +5 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +2 -0
- package/src/cli-adapters/cli-state-engine.ts +41 -12
- package/src/cli-adapters/provider-cli-adapter.ts +15 -13
- package/src/commands/cli-manager.ts +4 -0
- package/src/commands/router.ts +13 -4
- package/src/config/config.ts +12 -0
- package/src/index.ts +3 -1
- package/src/mesh/coordinator-registry.ts +75 -0
- package/src/providers/cli-provider-instance.ts +19 -2
- package/src/shared-types.ts +2 -0
- package/src/status/builders.ts +23 -6
|
@@ -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[];
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -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
|
@@ -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()
|
|
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,
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/router.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
5539
|
+
sessionId: launchSessionId,
|
|
5531
5540
|
mcpConfigWritten: true,
|
|
5532
5541
|
};
|
|
5533
5542
|
} catch (e: any) {
|
package/src/config/config.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1075
|
+
buttons.join('|'),
|
|
1059
1076
|
buttonIndex,
|
|
1060
1077
|
].join('::');
|
|
1061
1078
|
if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
|
package/src/shared-types.ts
CHANGED
|
@@ -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;
|
package/src/status/builders.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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));
|