@adhdev/daemon-core 0.9.82-rc.364 → 0.9.82-rc.365

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.
@@ -13,6 +13,9 @@ import { DaemonCdpManager } from '../cdp/manager.js';
13
13
  import { registerExtensionProviders } from '../cdp/setup.js';
14
14
  import { DaemonCommandHandler } from './handler.js';
15
15
  import { lowFamilyRegistry } from './low-family/index.js';
16
+ import { medFamilyRegistry } from './med-family/index.js';
17
+ import { launchIde } from './med-family/ide.js';
18
+ import type { MedFamilyContext } from './med-family/index.js';
16
19
  import { DaemonCliManager } from './cli-manager.js';
17
20
  import { supportsExplicitSessionResume } from './cli-manager.js';
18
21
  import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
@@ -114,7 +117,7 @@ function readProviderPriorityFromPolicy(policy: unknown): string[] {
114
117
  * the field entirely (full backward compat). Routing is governed by required_tags;
115
118
  * any legacy `role` field on the input is ignored.
116
119
  */
117
- function normalizeProviderRoles(value: unknown): Array<{ providerType: string; maxParallel?: number }> {
120
+ export function normalizeProviderRoles(value: unknown): Array<{ providerType: string; maxParallel?: number }> {
118
121
  if (!Array.isArray(value)) return [];
119
122
  const byType = new Map<string, { providerType: string; maxParallel?: number }>();
120
123
  for (const raw of value) {
@@ -130,13 +133,13 @@ function normalizeProviderRoles(value: unknown): Array<{ providerType: string; m
130
133
  return [...byType.values()];
131
134
  }
132
135
 
133
- function readObjectRecord(value: unknown): Record<string, any> {
136
+ export function readObjectRecord(value: unknown): Record<string, any> {
134
137
  return value && typeof value === 'object' && !Array.isArray(value)
135
138
  ? value as Record<string, any>
136
139
  : {};
137
140
  }
138
141
 
139
- function readStringValue(...values: unknown[]): string | undefined {
142
+ export function readStringValue(...values: unknown[]): string | undefined {
140
143
  for (const value of values) {
141
144
  if (typeof value === 'string' && value.trim()) return value.trim();
142
145
  }
@@ -224,7 +227,7 @@ function normalizeMeshHostname(value: unknown): string | undefined {
224
227
  return hostname.toLowerCase().replace(/\.$/, '');
225
228
  }
226
229
 
227
- function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
230
+ export function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
228
231
  return readStringValue(
229
232
  node.machineId,
230
233
  node.machine_id,
@@ -1156,7 +1159,7 @@ const MESH_DIRECT_PROBE_REUSE_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_REUSE
1156
1159
  * Lives on the router instance so the gate spans separate mesh_status calls,
1157
1160
  * which is exactly where the refresh storm happens.
1158
1161
  */
1159
- class MeshGitProbeCache {
1162
+ export class MeshGitProbeCache {
1160
1163
  private inflight = new Map<string, Promise<Record<string, unknown> | null>>();
1161
1164
  private recent = new Map<string, { at: number; value: Record<string, unknown> }>();
1162
1165
 
@@ -1311,7 +1314,7 @@ async function probeRemoteMeshGitStatusWithRetry(args: {
1311
1314
  return null;
1312
1315
  }
1313
1316
 
1314
- async function hydrateInlineMeshDirectTruth(args: {
1317
+ export async function hydrateInlineMeshDirectTruth(args: {
1315
1318
  mesh: any;
1316
1319
  meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
1317
1320
  dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
@@ -3045,7 +3048,7 @@ async function runMeshRefineSubmoduleReachabilityGate(
3045
3048
  }
3046
3049
  }
3047
3050
 
3048
- function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
3051
+ export function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
3049
3052
  const plan = resolveMeshRefineValidationPlan(mesh, workspace);
3050
3053
  const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
3051
3054
  displayCommand: command.displayCommand,
@@ -3470,7 +3473,7 @@ function normalizeCommandArgsWithInteractionId(args: any): Record<string, unknow
3470
3473
  * basename to be a literal `*.json`, and re-joins under the verified parent so
3471
3474
  * the returned path can't point outside the tree. Used by get/write_spec_source.
3472
3475
  */
3473
- function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
3476
+ export function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
3474
3477
  const raw = hostAddress.trim();
3475
3478
  if (!raw) throw new Error('hostAddress required');
3476
3479
  const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
@@ -3480,7 +3483,7 @@ function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
3480
3483
  return url.toString();
3481
3484
  }
3482
3485
 
3483
- function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
3486
+ export function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
3484
3487
  const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
3485
3488
  const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
3486
3489
  ? args.memberNode as Record<string, any>
@@ -3862,6 +3865,38 @@ export class DaemonCommandRouter {
3862
3865
  this.deps.onMeshStateChange?.(meshId);
3863
3866
  }
3864
3867
 
3868
+ /**
3869
+ * Build the MedFamilyContext handed to RF-ROUTER MED family handlers. Binds the
3870
+ * router-private collaborators those handlers need (mesh resolution, owner
3871
+ * gating, inline-cache mutation, worktree / session cleanup, refine job
3872
+ * starters, IDE stop/launch) plus the inline-mesh and git-probe caches. The
3873
+ * `launchIde` field closes over the freshly-built context so restart_session /
3874
+ * restart_ide invoke the IDE launch directly instead of recursing through
3875
+ * executeDaemonCommand('launch_ide').
3876
+ */
3877
+ private buildMedFamilyContext(): MedFamilyContext {
3878
+ const ctx: MedFamilyContext = {
3879
+ deps: this.deps,
3880
+ getMeshForCommand: this.getMeshForCommand.bind(this),
3881
+ getCachedInlineMesh: this.getCachedInlineMesh.bind(this),
3882
+ requireMeshHostMutationOwner: this.requireMeshHostMutationOwner.bind(this),
3883
+ invalidateAggregateMeshStatus: this.invalidateAggregateMeshStatus.bind(this),
3884
+ updateInlineMeshNode: this.updateInlineMeshNode.bind(this),
3885
+ removeInlineMeshNode: this.removeInlineMeshNode.bind(this),
3886
+ normalizeMeshSessionCleanupMode: this.normalizeMeshSessionCleanupMode.bind(this),
3887
+ cleanupMeshSessions: this.cleanupMeshSessions.bind(this),
3888
+ cleanupLocalWorktreeNode: this.cleanupLocalWorktreeNode.bind(this),
3889
+ startMeshRefineJob: this.startMeshRefineJob.bind(this),
3890
+ batchRefineMeshNodes: this.batchRefineMeshNodes.bind(this),
3891
+ startMeshRefineBatchJob: this.startMeshRefineBatchJob.bind(this),
3892
+ stopIde: this.stopIde.bind(this),
3893
+ launchIde: (args: any) => launchIde(ctx, args),
3894
+ inlineMeshCache: this.inlineMeshCache,
3895
+ meshGitProbeCache: this.meshGitProbeCache,
3896
+ };
3897
+ return ctx;
3898
+ }
3899
+
3865
3900
 
3866
3901
  private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
3867
3902
  const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
@@ -6183,6 +6218,19 @@ export class DaemonCommandRouter {
6183
6218
  }, args);
6184
6219
  }
6185
6220
 
6221
+ // RF-ROUTER MED family: medium-coupling commands (CLI/ACP agent, IDE
6222
+ // lifecycle, mesh CRUD, mesh queue, mesh host pairing, fast-forward /
6223
+ // refine convergence) are handled by the registry after the LOW family and
6224
+ // before the switch. Unlike LOW handlers, MED handlers need router-private
6225
+ // collaborators, so the context carries bound methods + the inline-mesh /
6226
+ // git-probe caches + the launchIde helper (which breaks the original
6227
+ // launch_ide ↔ restart_* self-recursion). A hit returns the same
6228
+ // CommandRouterResult the inlined case used to; a miss falls through.
6229
+ const medFamilyHandler = medFamilyRegistry.get(cmd);
6230
+ if (medFamilyHandler) {
6231
+ return await medFamilyHandler(this.buildMedFamilyContext(), args);
6232
+ }
6233
+
6186
6234
  switch (cmd) {
6187
6235
  // ─── CLI / ACP commands ───
6188
6236
  case 'mesh_forward_event': {
@@ -6215,1599 +6263,6 @@ export class DaemonCommandRouter {
6215
6263
  return { success: true };
6216
6264
  }
6217
6265
 
6218
- case 'launch_cli': {
6219
- // The coordinator routing anchor (meshCoordinatorDaemonId) is stamped
6220
- // upstream by mesh_launch_session, which resolves
6221
- // coordinatorNode.daemonId || ctx.localDaemonId || ctx.localMachineId and
6222
- // fail-closes for a remote node when none resolve. We deliberately do NOT
6223
- // self-stamp this daemon's own id when the field is missing: for a
6224
- // P2P-relayed remote worker launch, stamping the worker's own id would make
6225
- // the self-forward gate (mesh-events-coordinator: daemonIdsEquivalent) treat the
6226
- // worker as its own coordinator, suppressing the spontaneous completion-event
6227
- // forward and leaving the event in the pending inbox until a read_chat
6228
- // reconcile drains it. If the anchor is genuinely absent here, leave it
6229
- // absent rather than poison the routing.
6230
- const launchResult = await this.deps.cliManager.handleCliCommand(cmd, args);
6231
- // Bug C fix (part 1): when launching a mesh node worker session, surface
6232
- // bootstrapPending:true if the node's worktree bootstrap is still running.
6233
- // This is informational — the launch is NOT blocked here (blocking is done
6234
- // upstream by getWorktreeBootstrapLaunchBlock in the MCP layer).
6235
- const meshNodeId = readStringValue((args?.settings as any)?.meshNodeId);
6236
- const meshId = readStringValue((args?.settings as any)?.meshNodeFor);
6237
- if (meshNodeId && meshId && launchResult?.success !== false) {
6238
- try {
6239
- const { getMesh } = await import('../config/mesh-config.js');
6240
- const meshObj = getMesh(meshId) ?? this.getCachedInlineMesh(meshId);
6241
- const nodeObj = Array.isArray(meshObj?.nodes)
6242
- ? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, meshNodeId))
6243
- : undefined;
6244
- const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
6245
- if (bootstrapStatus === 'running') {
6246
- return { success: true, ...launchResult, bootstrapPending: true };
6247
- }
6248
- } catch { /* best-effort — do not fail launch for bootstrap probe errors */ }
6249
- }
6250
- return launchResult;
6251
- }
6252
- case 'stop_cli':
6253
- case 'set_cli_view_mode':
6254
- case 'record_provider_pty': {
6255
- return this.deps.cliManager.handleCliCommand(cmd, args);
6256
- }
6257
- case 'agent_command': {
6258
- // Relay-safety stamp: a dispatch carrying meshContext.coordinatorDaemonId
6259
- // (mesh_send_task / queue assignment over P2P) is the worker daemon's chance
6260
- // to persist the coordinator routing anchor onto the target session BEFORE the
6261
- // turn runs. Without meshCoordinatorDaemonId on the session, the core forwarder
6262
- // (injectMeshSystemMessage) cannot resolve a remote coordinator target, so the
6263
- // completion event sits in the pending queue until a read_chat reconcile drains
6264
- // it. Stamping here makes a reused/relaunched remote session relay-safe at
6265
- // dispatch time even when it was not launched via mesh_launch_session.
6266
- {
6267
- const dispatchSessionId = readStringValue(args?.targetSessionId, (args as any)?.sessionId, (args as any)?.instanceId);
6268
- const dispatchMeshContext = args?.meshContext as Record<string, unknown> | undefined;
6269
- if (dispatchSessionId && dispatchMeshContext) {
6270
- try {
6271
- const inst = this.deps.instanceManager.getInstance(dispatchSessionId);
6272
- if (inst && typeof inst.updateSettings === 'function') {
6273
- const stamp = buildMeshWorkerRelayStamp(
6274
- inst.getState?.()?.settings as Record<string, unknown> | undefined,
6275
- {
6276
- meshId: dispatchMeshContext.meshId,
6277
- nodeId: dispatchMeshContext.nodeId,
6278
- coordinatorDaemonId: dispatchMeshContext.coordinatorDaemonId,
6279
- // Session-level anchor: preserved across the P2P dispatch to a
6280
- // remote worker so its completion echoes back to the right session.
6281
- coordinatorSessionId: dispatchMeshContext.coordinatorSessionId,
6282
- },
6283
- );
6284
- if (stamp) inst.updateSettings(stamp);
6285
- }
6286
- } catch { /* best-effort — dispatch still proceeds without the stamp */ }
6287
- }
6288
- }
6289
- const agentResult = await this.deps.cliManager.handleCliCommand(cmd, args);
6290
- // Bug C fix (part 2): when dispatching a task to a mesh node session, override
6291
- // the dispatch acknowledgement risk reason to 'bootstrap_still_running' when
6292
- // the target node's worktree bootstrap is still running. Informational only —
6293
- // dispatch is NOT blocked.
6294
- const meshCtx = args?.meshContext as Record<string, unknown> | undefined;
6295
- const dispatchNodeId = readStringValue(meshCtx?.nodeId);
6296
- const dispatchMeshId = readStringValue(meshCtx?.meshId);
6297
- if (dispatchNodeId && dispatchMeshId && agentResult?.success !== false) {
6298
- try {
6299
- const { getMesh } = await import('../config/mesh-config.js');
6300
- const meshObj = getMesh(dispatchMeshId) ?? this.getCachedInlineMesh(dispatchMeshId);
6301
- const nodeObj = Array.isArray(meshObj?.nodes)
6302
- ? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, dispatchNodeId))
6303
- : undefined;
6304
- const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
6305
- if (bootstrapStatus === 'running') {
6306
- return {
6307
- success: true,
6308
- ...agentResult,
6309
- dispatchAcknowledgementRisk: true,
6310
- dispatchAcknowledgementRiskReason: 'bootstrap_still_running',
6311
- nextAction: 'Wait for worktree_bootstrap_complete event before dispatching work to this node.',
6312
- };
6313
- }
6314
- } catch { /* best-effort */ }
6315
- }
6316
- return agentResult;
6317
- }
6318
-
6319
- // ─── Logs ───
6320
- case 'list_saved_sessions': {
6321
- const providerType = typeof args?.providerType === 'string'
6322
- ? args.providerType.trim()
6323
- : typeof args?.agentType === 'string'
6324
- ? args.agentType.trim()
6325
- : '';
6326
- const kind = args?.kind === 'acp' ? 'acp' : 'cli';
6327
- if (!providerType) {
6328
- return { success: false, error: 'providerType required' };
6329
- }
6330
-
6331
- const wantsAll = args?.all === true;
6332
- const offset = wantsAll ? 0 : Math.max(0, Number(args?.offset) || 0);
6333
- const limit = wantsAll ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.min(100, Number(args?.limit) || 30));
6334
- const requestedWorkspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
6335
- const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
6336
- ? args.providerSessionId.trim()
6337
- : typeof args?.activeProviderSessionId === 'string'
6338
- ? args.activeProviderSessionId.trim()
6339
- : '';
6340
- const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
6341
- const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
6342
- canonicalHistory: providerMeta?.nativeHistory,
6343
- offset,
6344
- limit,
6345
- historyBehavior: providerMeta?.historyBehavior,
6346
- scripts: providerMeta?.scripts as any,
6347
- });
6348
- const state = loadState();
6349
- const savedSessions = getSavedProviderSessions(state, { providerType, kind });
6350
- const recentSessions = getRecentActivity(state, 200)
6351
- .filter(entry => entry.providerType === providerType && entry.kind === kind && entry.providerSessionId);
6352
- const savedSessionById = new Map(savedSessions.map(entry => [entry.providerSessionId, entry]));
6353
- const recentSessionById = new Map(recentSessions.map(entry => [entry.providerSessionId!, entry]));
6354
- const canResumeById = supportsExplicitSessionResume(providerMeta?.resume);
6355
-
6356
- return {
6357
- success: true,
6358
- sessions: historySessions.map(session => {
6359
- const saved = savedSessionById.get(session.historySessionId);
6360
- const recent = recentSessionById.get(session.historySessionId);
6361
- const workspace = saved?.workspace
6362
- || recent?.workspace
6363
- || session.workspace
6364
- || (requestedWorkspace && requestedProviderSessionId === session.historySessionId ? requestedWorkspace : undefined);
6365
- return {
6366
- id: session.historySessionId,
6367
- providerSessionId: session.historySessionId,
6368
- providerType,
6369
- providerName: saved?.providerName || recent?.providerName || providerType,
6370
- kind: saved?.kind || recent?.kind || kind,
6371
- title: saved?.title || recent?.title || session.sessionTitle || session.preview || providerType,
6372
- workspace,
6373
- summaryMetadata: saved?.summaryMetadata || recent?.summaryMetadata,
6374
- preview: session.preview,
6375
- messageCount: session.messageCount,
6376
- firstMessageAt: session.firstMessageAt,
6377
- lastMessageAt: session.lastMessageAt,
6378
- canResume: !!workspace && canResumeById,
6379
- historySource: session.source,
6380
- sourcePath: session.sourcePath,
6381
- sourceMtimeMs: session.sourceMtimeMs,
6382
- };
6383
- }),
6384
- hasMore,
6385
- source,
6386
- };
6387
- }
6388
-
6389
- // ─── restart_session: IDE / CLI / ACP unified ───
6390
- case 'restart_session': {
6391
- const targetType = args?.cliType || args?.agentType || args?.ideType;
6392
- if (!targetType) throw new Error('cliType or ideType required');
6393
-
6394
- // Check if IDE (in cdpManagers or provider category is ide)
6395
- const isIde = this.deps.cdpManagers.has(targetType) ||
6396
- this.deps.providerLoader.getMeta(targetType)?.category === 'ide';
6397
-
6398
- if (isIde) {
6399
- // IDE restart: stop (with process kill) → launch
6400
- await this.stopIde(targetType, true);
6401
- const launchResult = await this.executeDaemonCommand('launch_ide', { ideType: targetType, enableCdp: true, workspace: args?.workspace });
6402
- return { success: true, restarted: true, ideType: targetType, launch: launchResult };
6403
- }
6404
-
6405
- // CLI/ACP restart: delegate to CliManager
6406
- return this.deps.cliManager.handleCliCommand(cmd, args);
6407
- }
6408
-
6409
- // ─── IDE stop ───
6410
- case 'stop_ide': {
6411
- const ideType = args?.ideType;
6412
- if (!ideType) throw new Error('ideType required');
6413
- const killProcess = args?.killProcess !== false; // default true
6414
- await this.stopIde(ideType, killProcess);
6415
- try {
6416
- const results = await detectIDEs(this.deps.providerLoader);
6417
- this.deps.detectedIdes.value = results;
6418
- this.deps.providerLoader.setIdeDetectionResults(results, true);
6419
- } catch { /* ignore detection refresh errors */ }
6420
- return { success: true, ideType, stopped: true, processKilled: killProcess };
6421
- }
6422
-
6423
- // ─── IDE restart ───
6424
- case 'restart_ide': {
6425
- const ideType = args?.ideType;
6426
- if (!ideType) throw new Error('ideType required');
6427
- await this.stopIde(ideType, true); // always kill process on restart
6428
- const launchResult = await this.executeDaemonCommand('launch_ide', { ideType, enableCdp: true, workspace: args?.workspace });
6429
- return { success: true, ideType, restarted: true, launch: launchResult };
6430
- }
6431
-
6432
- // ─── IDE launch + CDP connect ───
6433
- case 'launch_ide': {
6434
- const ideKey = args?.ideId || args?.ideType;
6435
- const resolvedWorkspace = resolveIdeLaunchWorkspace(
6436
- {
6437
- workspace: args?.workspace,
6438
- workspaceId: args?.workspaceId,
6439
- useDefaultWorkspace: args?.useDefaultWorkspace,
6440
- },
6441
- loadConfig(),
6442
- );
6443
- const launchArgs = {
6444
- ideId: ideKey,
6445
- workspace: resolvedWorkspace,
6446
- newWindow: args?.newWindow,
6447
- };
6448
- LOG.info('LaunchIDE', `target=${ideKey || 'auto'}`);
6449
- const result = await launchWithCdp(launchArgs);
6450
-
6451
- if (result.success && result.port && result.ideId && !this.deps.cdpManagers.has(result.ideId)) {
6452
- const logFn = this.deps.getCdpLogFn
6453
- ? this.deps.getCdpLogFn(result.ideId)
6454
- : LOG.forComponent(`CDP:${result.ideId}`).asLogFn();
6455
- const provider = this.deps.providerLoader.getMeta(result.ideId);
6456
- const manager = new DaemonCdpManager(result.port, logFn, undefined, provider?.targetFilter);
6457
- const connected = await manager.connect();
6458
- if (connected) {
6459
- // Register active extension providers for this IDE in CDP manager
6460
- registerExtensionProviders(this.deps.providerLoader, manager, result.ideId);
6461
- this.deps.cdpManagers.set(result.ideId, manager);
6462
- LOG.info('CDP', `Connected: ${result.ideId} (port ${result.port})`);
6463
- LOG.info('CDP', `${this.deps.cdpManagers.size} IDE(s) connected`);
6464
-
6465
- // Notify consumer (e.g. setupIdeInstance)
6466
- this.deps.onCdpManagerCreated?.(result.ideId, manager);
6467
- }
6468
- }
6469
- this.deps.onIdeConnected?.();
6470
- try {
6471
- const results = await detectIDEs(this.deps.providerLoader);
6472
- this.deps.detectedIdes.value = results;
6473
- this.deps.providerLoader.setIdeDetectionResults(results, true);
6474
- } catch { /* ignore detection refresh errors */ }
6475
- if (result.success && resolvedWorkspace) {
6476
- try {
6477
- const next = appendRecentActivity(loadState(), {
6478
- kind: 'ide',
6479
- providerType: result.ideId || ideKey,
6480
- providerName: result.ideId || ideKey,
6481
- workspace: resolvedWorkspace,
6482
- title: result.ideId || ideKey,
6483
- });
6484
- saveState(next);
6485
- } catch { /* ignore activity persist errors */ }
6486
- } else if (result.success && (result.ideId || ideKey)) {
6487
- try {
6488
- saveState(appendRecentActivity(loadState(), {
6489
- kind: 'ide',
6490
- providerType: result.ideId || ideKey,
6491
- providerName: result.ideId || ideKey,
6492
- title: result.ideId || ideKey,
6493
- }));
6494
- } catch { /* ignore activity persist errors */ }
6495
- }
6496
- return { ...result };
6497
- }
6498
-
6499
- // ─── Detect providers ───
6500
- case 'detect_provider': {
6501
- const providerType = typeof args?.providerType === 'string' ? args.providerType.trim() : '';
6502
- if (!providerType) return { success: false, error: 'providerType is required' };
6503
- const normalizedType = this.deps.providerLoader.resolveAlias(providerType);
6504
- const provider = this.deps.providerLoader.getByAlias(providerType);
6505
- if (!provider) return { success: false, error: `Provider not found: ${providerType}` };
6506
- if (provider.category !== 'cli' && provider.category !== 'acp') {
6507
- return { success: false, error: `Provider detection is only supported for CLI/ACP providers: ${providerType}` };
6508
- }
6509
- if (!this.deps.providerLoader.isMachineProviderEnabled(normalizedType)) {
6510
- return { success: false, error: `Provider is disabled on this machine: ${providerType}` };
6511
- }
6512
- const detected = await detectCLI(normalizedType, this.deps.providerLoader, { includeVersion: false });
6513
- this.deps.providerLoader.setCliDetectionResults([{
6514
- id: normalizedType,
6515
- installed: !!detected,
6516
- path: detected?.path,
6517
- }], false);
6518
- this.deps.onStatusChange?.();
6519
- return {
6520
- success: true,
6521
- providerType: normalizedType,
6522
- detected: !!detected,
6523
- path: detected?.path || null,
6524
- };
6525
- }
6526
-
6527
- // ─── Detect IDEs ───
6528
- case 'detect_ides': {
6529
- const results = await detectIDEs(this.deps.providerLoader);
6530
- this.deps.detectedIdes.value = results;
6531
- this.deps.providerLoader.setIdeDetectionResults(results, true);
6532
- return { success: true, detectedInfo: results };
6533
- }
6534
-
6535
- // ─── Mesh CRUD (local meshes.json) ───
6536
- case 'list_meshes': {
6537
- try {
6538
- const { listMeshes } = await import('../config/mesh-config.js');
6539
- return { success: true, meshes: listMeshes() };
6540
- } catch (e: any) {
6541
- return { success: false, error: e.message };
6542
- }
6543
- }
6544
-
6545
- case 'get_mesh': {
6546
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6547
- if (!meshId) return { success: false, error: 'meshId required' };
6548
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
6549
- if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
6550
-
6551
- const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
6552
- // Only an explicit refresh fans out a blocking peer probe.
6553
- // Default loads are satisfied from held standing-state git truth.
6554
- const probeRemotePeers = args?.refresh === true || args?.forceRefresh === true;
6555
- const directTruth = await hydrateInlineMeshDirectTruth({
6556
- mesh: meshRecord.mesh,
6557
- meshSource: meshRecord.source,
6558
- dispatchMeshCommand: this.deps.dispatchMeshCommand,
6559
- getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
6560
- statusInstanceId: this.deps.statusInstanceId,
6561
- localMachineId: loadConfig().machineId || '',
6562
- probeRemotePeers,
6563
- probeCache: this.meshGitProbeCache,
6564
- });
6565
- const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
6566
- const sourceOfTruth = {
6567
- membership: meshRecord.source === 'inline_cache'
6568
- ? 'coordinator_inline_mesh_cache'
6569
- : meshRecord.source === 'local_config'
6570
- ? 'local_mesh_config'
6571
- : 'inline_bootstrap_snapshot',
6572
- coordinatorOwnsLiveTruth: directTruthSatisfied,
6573
- directPeerTruth: {
6574
- required: requireDirectPeerTruth,
6575
- satisfied: directTruthSatisfied,
6576
- directEvidenceCount: directTruth.directEvidenceCount,
6577
- localConfirmedCount: directTruth.localConfirmedCount,
6578
- peerAttemptedCount: directTruth.peerAttemptedCount,
6579
- peerConfirmedCount: directTruth.peerConfirmedCount,
6580
- unavailableNodeIds: directTruth.unavailableNodeIds,
6581
- },
6582
- };
6583
- if (requireDirectPeerTruth && !directTruthSatisfied) {
6584
- return {
6585
- success: false,
6586
- code: 'mesh_direct_peer_truth_unavailable',
6587
- error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
6588
- sourceOfTruth,
6589
- };
6590
- }
6591
- return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
6592
- }
6593
-
6594
- case 'create_mesh': {
6595
- const name = typeof args?.name === 'string' ? args.name.trim() : '';
6596
- const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
6597
- const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
6598
- const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
6599
- if (!name) return { success: false, error: 'name required' };
6600
- try {
6601
- const { createMesh } = await import('../config/mesh-config.js');
6602
- const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
6603
- ? args.meshHost
6604
- : undefined;
6605
- const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
6606
- return { success: true, mesh };
6607
- } catch (e: any) {
6608
- return { success: false, error: e.message };
6609
- }
6610
- }
6611
-
6612
- case 'update_mesh': {
6613
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6614
- if (!meshId) return { success: false, error: 'meshId required' };
6615
- try {
6616
- const { updateMesh } = await import('../config/mesh-config.js');
6617
- const patch: Record<string, unknown> = {};
6618
- if (typeof args?.name === 'string') patch.name = args.name;
6619
- if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
6620
- if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
6621
- if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
6622
- if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
6623
- if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
6624
- const mesh = updateMesh(meshId, patch as any);
6625
- if (!mesh) return { success: false, error: 'Mesh not found' };
6626
- this.inlineMeshCache.set(meshId, mesh);
6627
- this.invalidateAggregateMeshStatus(meshId);
6628
- return { success: true, mesh };
6629
- } catch (e: any) {
6630
- return { success: false, error: e.message };
6631
- }
6632
- }
6633
-
6634
- case 'get_mesh_host_pairing': {
6635
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6636
- if (!meshId) return { success: false, error: 'meshId required' };
6637
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
6638
- const mesh = meshRecord?.mesh;
6639
- if (!mesh) return { success: false, error: 'Mesh not found' };
6640
- const meshHost = resolveMeshHostStatus(mesh);
6641
- const pairingStatus = meshHost.pairing?.status || 'not_configured';
6642
- return {
6643
- success: true,
6644
- code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
6645
- meshId,
6646
- hostAddress: meshHost.hostAddress,
6647
- meshHost,
6648
- manualPairing: {
6649
- status: pairingStatus,
6650
- joinImplemented: true,
6651
- protocol: 'standalone_command_direct_v1',
6652
- description: 'Standalone manual pairing can save address/token metadata, apply a host join over direct standalone command HTTP or injected mesh command dispatch, and check persisted status. P2P signaling remains outside this slice.',
6653
- },
6654
- };
6655
- }
6656
-
6657
- case 'configure_mesh_host_pairing': {
6658
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6659
- const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
6660
- const token = typeof args?.token === 'string' ? args.token.trim() : '';
6661
- if (!meshId) return { success: false, error: 'meshId required' };
6662
- if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
6663
- try {
6664
- const { configureMeshHostPairing } = await import('../config/mesh-config.js');
6665
- const configured = configureMeshHostPairing(meshId, { hostAddress, token });
6666
- if (!configured) return { success: false, error: 'Mesh not found' };
6667
- this.inlineMeshCache.set(meshId, configured.mesh);
6668
- const meshHost = resolveMeshHostStatus(configured.mesh);
6669
- return {
6670
- success: true,
6671
- code: 'mesh_host_pairing_pending',
6672
- meshId,
6673
- hostAddress: configured.hostAddress,
6674
- meshHost,
6675
- manualPairing: {
6676
- status: meshHost.pairing?.status || 'pairing',
6677
- joinImplemented: true,
6678
- protocol: 'standalone_command_direct_v1',
6679
- description: 'Manual Mesh Host pairing config was saved locally. Use join_mesh_host_pairing to apply it to the host. Raw token was not persisted.',
6680
- },
6681
- };
6682
- } catch (e: any) {
6683
- return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
6684
- }
6685
- }
6686
-
6687
- case 'create_mesh_host_pairing_token': {
6688
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6689
- if (!meshId) return { success: false, error: 'meshId required' };
6690
- try {
6691
- const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
6692
- const created = createMeshHostPairingToken(meshId, {
6693
- token: typeof args?.token === 'string' ? args.token : undefined,
6694
- expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
6695
- });
6696
- if (!created) return { success: false, error: 'Mesh not found' };
6697
- this.inlineMeshCache.set(meshId, created.mesh);
6698
- this.invalidateAggregateMeshStatus(meshId);
6699
- return {
6700
- success: true,
6701
- code: 'mesh_host_pairing_token_created',
6702
- meshId,
6703
- token: created.token,
6704
- tokenId: created.tokenId,
6705
- expiresAt: created.expiresAt,
6706
- meshHost: resolveMeshHostStatus(created.mesh),
6707
- warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
6708
- };
6709
- } catch (e: any) {
6710
- return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
6711
- }
6712
- }
6713
-
6714
- case 'apply_mesh_host_join': {
6715
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6716
- const token = typeof args?.token === 'string' ? args.token.trim() : '';
6717
- const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
6718
- ? args.memberNode
6719
- : null;
6720
- if (!meshId) return { success: false, error: 'meshId required' };
6721
- if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
6722
- try {
6723
- const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
6724
- const applied = applyMeshHostJoinRequest(meshId, {
6725
- token,
6726
- memberNode: memberNode as any,
6727
- memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
6728
- });
6729
- if (!applied) return { success: false, error: 'Mesh not found' };
6730
- if (!applied.accepted) {
6731
- return {
6732
- success: false,
6733
- code: 'mesh_host_join_rejected',
6734
- meshId,
6735
- tokenId: applied.tokenId,
6736
- meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
6737
- error: applied.reason,
6738
- };
6739
- }
6740
- this.inlineMeshCache.set(meshId, applied.mesh);
6741
- this.invalidateAggregateMeshStatus(meshId);
6742
- try {
6743
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
6744
- appendLedgerEntry(meshId, {
6745
- kind: 'node_joined',
6746
- nodeId: applied.node.id,
6747
- payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
6748
- });
6749
- } catch { /* ledger append is best-effort */ }
6750
- return {
6751
- success: true,
6752
- code: 'mesh_host_join_accepted',
6753
- meshId,
6754
- node: applied.node,
6755
- tokenId: applied.tokenId,
6756
- meshHost: resolveMeshHostStatus(applied.mesh),
6757
- };
6758
- } catch (e: any) {
6759
- return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
6760
- }
6761
- }
6762
-
6763
- case 'join_mesh_host_pairing': {
6764
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6765
- const token = typeof args?.token === 'string' ? args.token.trim() : '';
6766
- if (!meshId) return { success: false, error: 'meshId required' };
6767
- if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
6768
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
6769
- const mesh = meshRecord?.mesh;
6770
- if (!mesh) return { success: false, error: 'Mesh not found' };
6771
- const meshHost = resolveMeshHostStatus(mesh);
6772
- if (meshHost.role !== 'member') {
6773
- return { success: false, code: 'mesh_host_join_not_member', meshId, meshHost, error: 'join_mesh_host_pairing must run from a member daemon configured with a Mesh Host address/token.' };
6774
- }
6775
- try {
6776
- const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
6777
- const tokenId = tokenIdForManualPairing(token);
6778
- if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
6779
- return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
6780
- }
6781
- const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
6782
- if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
6783
- const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
6784
- const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
6785
- ? args.hostDaemonId.trim()
6786
- : meshHost.hostDaemonId;
6787
- let hostResult: any;
6788
- let transport: string;
6789
- if (hostDaemonId && this.deps.dispatchMeshCommand) {
6790
- transport = 'mesh_command_dispatch';
6791
- hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
6792
- meshId: hostMeshId,
6793
- token,
6794
- memberMeshId: meshId,
6795
- memberNode,
6796
- });
6797
- } else if (meshHost.hostAddress) {
6798
- transport = 'standalone_http_command';
6799
- const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
6800
- const response = await fetch(commandUrl, {
6801
- method: 'POST',
6802
- headers: { 'Content-Type': 'application/json' },
6803
- body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
6804
- });
6805
- hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
6806
- if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
6807
- } else {
6808
- return {
6809
- success: false,
6810
- code: 'mesh_host_join_transport_unavailable',
6811
- meshId,
6812
- meshHost,
6813
- error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
6814
- };
6815
- }
6816
- if (!hostResult?.success) {
6817
- return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
6818
- }
6819
- const joined = meshRecord.inline
6820
- ? null
6821
- : markMeshHostPairingJoined(meshId, {
6822
- tokenId: hostResult.tokenId || tokenId,
6823
- hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
6824
- hostNodeId: hostResult.meshHost?.hostNodeId,
6825
- joinedAt: hostResult.meshHost?.pairing?.joinedAt,
6826
- });
6827
- if (joined) {
6828
- this.inlineMeshCache.set(meshId, joined.mesh);
6829
- this.invalidateAggregateMeshStatus(meshId);
6830
- }
6831
- return {
6832
- success: true,
6833
- code: 'mesh_host_join_applied',
6834
- meshId,
6835
- hostMeshId,
6836
- transport,
6837
- node: hostResult.node,
6838
- tokenId: hostResult.tokenId || tokenId,
6839
- meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
6840
- hostResult,
6841
- manualPairing: {
6842
- status: 'paired',
6843
- joinImplemented: true,
6844
- protocol: 'standalone_command_direct_v1',
6845
- description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
6846
- },
6847
- };
6848
- } catch (e: any) {
6849
- return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
6850
- }
6851
- }
6852
-
6853
- case 'delete_mesh': {
6854
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6855
- if (!meshId) return { success: false, error: 'meshId required' };
6856
- try {
6857
- const { deleteMesh } = await import('../config/mesh-config.js');
6858
- const deleted = deleteMesh(meshId);
6859
- return { success: true, deleted };
6860
- } catch (e: any) {
6861
- return { success: false, error: e.message };
6862
- }
6863
- }
6864
-
6865
- case 'get_mesh_queue': {
6866
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6867
- if (!meshId) return { success: false, error: 'meshId required' };
6868
- try {
6869
- const { getMeshQueueStats, getQueue, describeTaskDependencyState } = await import('../mesh/mesh-work-queue.js');
6870
- const status = Array.isArray(args?.status)
6871
- ? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
6872
- : undefined;
6873
- const rawQueue = getQueue(meshId, { status: status as any });
6874
- // M1: annotate dependency state at view time (waitingOn / dependenciesSatisfied).
6875
- const statusById = new Map(getQueue(meshId).map(task => [task.id, task.status]));
6876
- const queue = rawQueue.map(task =>
6877
- Array.isArray(task.dependsOn) && task.dependsOn.length > 0
6878
- ? { ...task, ...describeTaskDependencyState(task, statusById) }
6879
- : task);
6880
- const summary = getMeshQueueStats(meshId);
6881
- return {
6882
- success: true,
6883
- queue,
6884
- summary,
6885
- sourceOfTruth: {
6886
- kind: 'mesh_work_queue_file',
6887
- activeStatuses: ['pending', 'assigned'],
6888
- historicalStatuses: ['completed', 'failed', 'cancelled'],
6889
- notes: 'pending/assigned are active work; completed/failed/cancelled are historical records.',
6890
- },
6891
- };
6892
- } catch (e: any) {
6893
- return { success: false, error: e.message };
6894
- }
6895
- }
6896
-
6897
- case 'cancel_mesh_queue_task': {
6898
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6899
- const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
6900
- if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
6901
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
6902
- if (ownerFailure) return ownerFailure;
6903
- try {
6904
- const { cancelTask } = await import('../mesh/mesh-work-queue.js');
6905
- const reason = typeof args?.reason === 'string' ? args.reason : undefined;
6906
- const task = cancelTask(meshId, taskId, { reason });
6907
- if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
6908
- return { success: true, task };
6909
- } catch (e: any) {
6910
- return { success: false, error: e.message };
6911
- }
6912
- }
6913
-
6914
- case 'requeue_mesh_queue_task': {
6915
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6916
- const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
6917
- if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
6918
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
6919
- if (ownerFailure) return ownerFailure;
6920
- try {
6921
- const { requeueTask } = await import('../mesh/mesh-work-queue.js');
6922
- const task = requeueTask(meshId, taskId, {
6923
- reason: typeof args?.reason === 'string' ? args.reason : undefined,
6924
- targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
6925
- targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
6926
- clearTargetNode: args?.clearTargetNode === true,
6927
- clearTargetSession: args?.clearTargetSession !== false,
6928
- });
6929
- if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
6930
- return { success: true, task };
6931
- } catch (e: any) {
6932
- return { success: false, error: e.message };
6933
- }
6934
- }
6935
-
6936
- case 'add_mesh_node': {
6937
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6938
- const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
6939
- if (!meshId) return { success: false, error: 'meshId required' };
6940
- if (!workspace) return { success: false, error: 'workspace required' };
6941
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
6942
- if (ownerFailure) return ownerFailure;
6943
- try {
6944
- const { addNode } = await import('../config/mesh-config.js');
6945
- const providerPriority = Array.isArray(args?.providerPriority)
6946
- ? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
6947
- : [];
6948
- const readOnly = args?.readOnly === true;
6949
- const providerRoles = normalizeProviderRoles(args?.providerRoles);
6950
- const policy = {
6951
- ...(readOnly ? { readOnly: true } : {}),
6952
- ...(providerPriority.length ? { providerPriority } : {}),
6953
- ...(providerRoles.length ? { providerRoles } : {}),
6954
- };
6955
- const role = normalizeMeshDaemonRole(args?.role);
6956
- const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
6957
- const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
6958
- const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
6959
- const node = addNode(meshId, {
6960
- workspace,
6961
- ...(repoRoot ? { repoRoot } : {}),
6962
- ...(daemonId ? { daemonId } : {}),
6963
- ...(machineId ? { machineId } : {}),
6964
- ...(policy ? { policy } : {}),
6965
- ...(role ? { role } : {}),
6966
- });
6967
- if (!node) return { success: false, error: 'Mesh not found' };
6968
- // mesh_status hands back a coordinator-memory aggregate
6969
- // snapshot keyed on (meshId, queueRevision). Adding a
6970
- // node touches neither, so without an explicit cache
6971
- // bust the dashboard graph keeps rendering the pre-add
6972
- // node list (empty for a fresh mesh) even after the
6973
- // user clicks Refresh.
6974
- this.invalidateAggregateMeshStatus(meshId);
6975
- return { success: true, node };
6976
- } catch (e: any) {
6977
- return { success: false, error: e.message };
6978
- }
6979
- }
6980
-
6981
- case 'update_mesh_node': {
6982
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6983
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
6984
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
6985
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
6986
- if (ownerFailure) return ownerFailure;
6987
- try {
6988
- const { updateNode } = await import('../config/mesh-config.js');
6989
- const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
6990
- ? { ...(args.policy as Record<string, unknown>) }
6991
- : {};
6992
- if (Array.isArray(args?.providerPriority)) {
6993
- const providerPriority = args.providerPriority
6994
- .map((type: any) => typeof type === 'string' ? type.trim() : '')
6995
- .filter(Boolean);
6996
- delete (policy as any).provider_priority;
6997
- if (providerPriority.length) {
6998
- (policy as any).providerPriority = providerPriority;
6999
- } else {
7000
- delete (policy as any).providerPriority;
7001
- }
7002
- }
7003
- // providerRoles: per-(node, provider) role label + maxParallel cap.
7004
- // Passing an explicit (possibly empty) array clears/replaces the
7005
- // declarations; omitting the arg leaves any value already on policy
7006
- // untouched (a full policy object passed by the caller still carries it).
7007
- if (Array.isArray(args?.providerRoles)) {
7008
- const providerRoles = normalizeProviderRoles(args.providerRoles);
7009
- if (providerRoles.length) {
7010
- (policy as any).providerRoles = providerRoles;
7011
- } else {
7012
- delete (policy as any).providerRoles;
7013
- }
7014
- }
7015
- const patch: Record<string, unknown> = { policy: policy as any };
7016
- if (typeof args?.systemPrompt === 'string') {
7017
- const trimmed = (args.systemPrompt as string).trim();
7018
- patch.systemPrompt = trimmed || undefined;
7019
- } else if (args?.systemPrompt === null) {
7020
- patch.systemPrompt = undefined;
7021
- }
7022
- const node = updateNode(meshId, nodeId, patch as any);
7023
- if (!node) return { success: false, error: 'Mesh node not found' };
7024
- // Provider priority / systemPrompt changes don't touch
7025
- // the queue revision, so without a manual bust the
7026
- // cached aggregate keeps surfacing pre-update values
7027
- // (priority chip, coordinator prompt preview, etc.).
7028
- this.invalidateAggregateMeshStatus(meshId);
7029
- return { success: true, node };
7030
- } catch (e: any) {
7031
- return { success: false, error: e.message };
7032
- }
7033
- }
7034
-
7035
- case 'cleanup_mesh_sessions': {
7036
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7037
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7038
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
7039
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
7040
- if (ownerFailure) return ownerFailure;
7041
- try {
7042
- // preferInline so inline-cache-only clone nodes resolve (matches owner check above).
7043
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7044
- const mesh = meshRecord?.mesh;
7045
- if (!mesh) return { success: false, error: 'Mesh not found' };
7046
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7047
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
7048
- const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
7049
- const sessionIds = Array.isArray(args?.sessionIds)
7050
- ? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
7051
- : undefined;
7052
- const result = await this.cleanupMeshSessions({
7053
- meshId,
7054
- nodeId,
7055
- node,
7056
- mode,
7057
- sessionIds,
7058
- dryRun: args?.dryRun === true,
7059
- source: 'mesh_cleanup_sessions',
7060
- });
7061
- return result;
7062
- } catch (e: any) {
7063
- return { success: false, error: e.message };
7064
- }
7065
- }
7066
-
7067
- case 'mesh_init': {
7068
- const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
7069
- const mesh = args?.inlineMesh || {};
7070
- try {
7071
- const detected = await detectCLIs(this.deps.providerLoader, { includeVersion: true });
7072
- return { ...runMeshInit(mesh, workspace, detected, {
7073
- write: args?.write === true,
7074
- overwrite: args?.overwrite === true,
7075
- }) };
7076
- } catch (e: any) {
7077
- return { success: false, error: e?.message || String(e) };
7078
- }
7079
- }
7080
-
7081
- case 'plan_mesh_refine_node': {
7082
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7083
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7084
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
7085
- // preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
7086
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7087
- const mesh = meshRecord?.mesh;
7088
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7089
- if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
7090
- return {
7091
- success: true,
7092
- dryRun: true,
7093
- nodeId,
7094
- workspace: node.workspace,
7095
- validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
7096
- mergeWillRun: false,
7097
- cleanupWillRun: false,
7098
- };
7099
- }
7100
-
7101
- case 'fast_forward_mesh_node': {
7102
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7103
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7104
- let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
7105
- let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
7106
- ? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
7107
- : undefined;
7108
- let nodeDaemonId: string | undefined;
7109
- let allowAutoPublishSubmoduleMainCommits = false;
7110
- if (meshId && nodeId) {
7111
- // preferInline so fast-forward can resolve inline-cache-only clone worktree nodes.
7112
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7113
- const mesh = meshRecord?.mesh;
7114
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7115
- if (!workspace) {
7116
- workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
7117
- }
7118
- if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
7119
- submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
7120
- }
7121
- allowAutoPublishSubmoduleMainCommits = mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true;
7122
- nodeDaemonId = typeof node?.daemonId === 'string' ? node.daemonId.trim() : undefined;
7123
- }
7124
- // If the target node belongs to a remote daemon, forward the command there.
7125
- // _meshDirectDispatch prevents re-forwarding (and P2P self-dial) when the stored
7126
- // daemonId uses a legacy format that doesn't match the receiving daemon's identity.
7127
- const selfDaemonId = this.deps.statusInstanceId;
7128
- // daemonIdsEquivalent: a legacy-form stored daemonId that resolves to THIS
7129
- // machine's core must be treated as local (not remote) so it is not forwarded /
7130
- // P2P self-dialed. Equivalent → local.
7131
- const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
7132
- if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
7133
- const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'fast_forward_mesh_node', {
7134
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7135
- workspace,
7136
- _meshDirectDispatch: true,
7137
- });
7138
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7139
- }
7140
- const result = await (fastForwardMeshNode({
7141
- meshId: meshId || undefined,
7142
- nodeId: nodeId || undefined,
7143
- workspace,
7144
- branch: typeof args?.branch === 'string' ? args.branch : undefined,
7145
- execute: args?.execute === true,
7146
- dryRun: args?.dryRun === true,
7147
- updateSubmodules: args?.updateSubmodules === true,
7148
- submoduleIgnorePaths,
7149
- mode: args?.mode === 'push' ? 'push' : 'merge',
7150
- pushSubmodules: args?.pushSubmodules === true,
7151
- allowAutoPublishSubmoduleMainCommits,
7152
- }) as Promise<unknown>);
7153
- return result as CommandRouterResult;
7154
- }
7155
-
7156
- case 'refine_mesh_node': {
7157
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7158
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7159
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
7160
-
7161
- // Remote forward: a worktree node lives on its OWN daemon's machine, so the
7162
- // refine (cd into node.workspace, merge → push → cleanup) must run on THAT
7163
- // daemon — not the coordinator, whose filesystem has no such path. The sibling
7164
- // fast_forward_mesh_node / clone_mesh_node handlers already forward to the
7165
- // node's daemon; refine_mesh_node was the gap (the coordinator would cd into a
7166
- // non-existent local path and fail), so remote-machine worktrees could not be
7167
- // converged at all. Forward both dry-run (plan reads the worktree git state)
7168
- // and execute (async merge job) so the same machine that owns the worktree
7169
- // resolves it.
7170
- //
7171
- // coordinatorDaemonId: refine is ASYNC — the completed/failed event is queued
7172
- // on the executing daemon's pending-events queue scoped to a coordinator id and
7173
- // recovered by the coordinator's reconcile loop (pullRemoteNodeQueues →
7174
- // get_pending_mesh_events). Without stamping our own status id, the remote
7175
- // daemon would fall back to ITS OWN statusInstanceId as the coordinator
7176
- // (startMeshRefineJob), scoping the terminal event to the wrong inbox where the
7177
- // real coordinator never pulls it. Stamp the canonical status id (which is in
7178
- // the coordinator's self-identity set used to scope the remote drain) so the
7179
- // event routes back here. Preserve any caller-supplied coordinatorDaemonId.
7180
- //
7181
- // _meshDirectDispatch prevents re-forwarding (and P2P self-dial) once the call
7182
- // has landed on the owning daemon — that daemon then executes locally even if
7183
- // the stored daemonId uses a legacy form that doesn't match its own identity.
7184
- {
7185
- const meshRecordForForward = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7186
- const forwardNode = meshRecordForForward?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7187
- const nodeDaemonId = typeof forwardNode?.daemonId === 'string' ? forwardNode.daemonId.trim() : undefined;
7188
- const selfDaemonId = this.deps.statusInstanceId;
7189
- // daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core
7190
- // is local — execute locally instead of forwarding. Equivalent → local.
7191
- const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
7192
- if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
7193
- const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
7194
- ? args.coordinatorDaemonId.trim()
7195
- : undefined;
7196
- const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'refine_mesh_node', {
7197
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7198
- coordinatorDaemonId: callerCoordinatorDaemonId || selfDaemonId,
7199
- _meshDirectDispatch: true,
7200
- });
7201
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7202
- }
7203
- }
7204
-
7205
- // Dry-run (plan-only) is the default and stays synchronous: it does no
7206
- // validation/merge/push and returns the plan instantly. Only execute=true
7207
- // (and not dry_run) goes through the async refine job that actually
7208
- // validates → merges → pushes → cleans up. Mirrors the
7209
- // batch_refine_mesh_nodes / fast_forward_mesh_node dry_run/execute contract.
7210
- const isDryRun = args?.dryRun !== false && args?.execute !== true;
7211
- if (isDryRun) {
7212
- // preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
7213
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7214
- const mesh = meshRecord?.mesh;
7215
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7216
- if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
7217
- return {
7218
- success: true,
7219
- dryRun: true,
7220
- nodeId,
7221
- workspace: node.workspace,
7222
- validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
7223
- mergeWillRun: false,
7224
- cleanupWillRun: false,
7225
- hint: 'Dry-run only — no merge/push/cleanup performed. Re-invoke with execute:true to converge this node.',
7226
- };
7227
- }
7228
- return this.startMeshRefineJob(meshId, nodeId, args);
7229
- }
7230
-
7231
- case 'batch_refine_mesh_nodes': {
7232
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7233
- if (!meshId) return { success: false, error: 'meshId required' };
7234
- const requestedNodeIds = Array.isArray(args?.nodeIds)
7235
- ? (args.nodeIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
7236
- : undefined;
7237
- // Dry-run (plan-only) stays synchronous: it does no validation/merge and
7238
- // returns instantly. Execute goes through the async batch job — immediate
7239
- // {async:true, status:'accepted'} + background convergence + terminal event,
7240
- // matching the single-node refine_mesh_node contract so long validation
7241
- // suites can't time out the IPC and strand the coordinator.
7242
- const isDryRun = args?.dryRun !== false && args?.execute !== true;
7243
- if (isDryRun) return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
7244
- return this.startMeshRefineBatchJob(meshId, requestedNodeIds, args);
7245
- }
7246
-
7247
- case 'remove_mesh_node': {
7248
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7249
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7250
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
7251
- try {
7252
- // preferInline so removal can resolve inline-cache-only clone worktree nodes.
7253
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7254
- const mesh = meshRecord?.mesh;
7255
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7256
-
7257
- // Guard: refuse to remove the coordinator's OWN local base node
7258
- // (same machine, NOT a worktree). Removing it breaks live mesh
7259
- // membership — the coordinator can no longer be reached and has
7260
- // to be restarted. Worktree clones are always safe to remove;
7261
- // only the non-worktree node bound to this daemon is protected.
7262
- // An explicit force:true overrides for intentional mesh teardown.
7263
- if (node && !args?._meshDirectDispatch && node.isLocalWorktree !== true && args?.force !== true) {
7264
- const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : '';
7265
- const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>) || '';
7266
- const selfDaemonId = this.deps.statusInstanceId || '';
7267
- const selfMachineId = (() => { try { return loadConfig().machineId || ''; } catch { return ''; } })();
7268
- const isCoordinatorBaseNode =
7269
- (!!selfDaemonId && (nodeDaemonId === selfDaemonId || nodeMachineId === selfDaemonId))
7270
- || (!!selfMachineId && (nodeDaemonId === selfMachineId || nodeMachineId === selfMachineId));
7271
- if (isCoordinatorBaseNode) {
7272
- return {
7273
- success: false,
7274
- removed: false,
7275
- code: 'mesh_remove_coordinator_base_node_protected',
7276
- error: `Refusing to remove the coordinator's own base node '${typeof node.workspace === 'string' ? node.workspace : nodeId}'. `
7277
- + `It is the local non-worktree node bound to this coordinator daemon; removing it breaks live mesh membership and forces a restart.`,
7278
- recoveryHint: 'Remove worktree clone nodes instead, or pass force:true only if you are intentionally tearing down this mesh and accept that the coordinator must be re-registered/restarted.',
7279
- };
7280
- }
7281
- }
7282
-
7283
- const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
7284
- args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
7285
- );
7286
- // Explicit sessionIds (e.g. supplied by refine auto-cleanup) bypass the
7287
- // workspace-only-match guard so a delegate session that lacks a
7288
- // meta.meshNodeId binding can still be stopped/deleted.
7289
- const explicitSessionIds = Array.isArray(args?.sessionIds)
7290
- ? (args.sessionIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
7291
- : undefined;
7292
- let sessionCleanup: Record<string, unknown> | undefined;
7293
- if (node && sessionCleanupMode !== 'preserve') {
7294
- sessionCleanup = await this.cleanupMeshSessions({
7295
- meshId,
7296
- nodeId,
7297
- node,
7298
- mode: sessionCleanupMode,
7299
- ...(explicitSessionIds && explicitSessionIds.length > 0 ? { sessionIds: explicitSessionIds } : {}),
7300
- source: 'mesh_remove_node',
7301
- });
7302
- if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
7303
- }
7304
-
7305
- let worktreeCleanup: Record<string, unknown> | undefined;
7306
- if (node?.isLocalWorktree) {
7307
- const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
7308
- // daemonIdsEquivalent: an equivalent-form daemonId is this machine —
7309
- // clean up locally, do not forward. Equivalent → local.
7310
- const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
7311
- && !args?._meshDirectDispatch;
7312
- if (isRemoteWorktree) {
7313
- // Worktree lives on a different machine — ask that daemon to clean it up.
7314
- // _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
7315
- const forwarded = await this.deps.dispatchMeshCommand!(nodeDaemonId!, 'remove_mesh_node', {
7316
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7317
- _meshDirectDispatch: true,
7318
- });
7319
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7320
- }
7321
- const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId, force: args?.force === true });
7322
- // De-gating: membership removal is NOT gated on the worktree
7323
- // directory actually being deleted. cleanupLocalWorktreeNode now
7324
- // returns success:true (with a residue flag) whenever the path is
7325
- // proven managed and the only remaining problem is leftover
7326
- // directory bytes (e.g. Windows EINVAL). A success:false here means
7327
- // a genuinely-unsafe condition — missing metadata, a non-managed /
7328
- // unexpected path, a branch mismatch, a dirty worktree, or an
7329
- // unverified force fallback — and those still block removal.
7330
- if (cleanupResult.success === false) {
7331
- return {
7332
- success: false,
7333
- removed: false,
7334
- code: cleanupResult.code,
7335
- error: cleanupResult.error,
7336
- recoveryHint: cleanupResult.recoveryHint,
7337
- ...(sessionCleanup ? { sessionCleanup } : {}),
7338
- worktreeCleanup: cleanupResult,
7339
- };
7340
- }
7341
- worktreeCleanup = cleanupResult;
7342
- }
7343
-
7344
- let removed = false;
7345
- if (meshRecord?.inline) {
7346
- removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
7347
- // Inline meshes share the same aggregate snapshot cache as
7348
- // local-config meshes; without this bust the removed node
7349
- // keeps showing up in the dashboard graph until the cache
7350
- // ages out on its own.
7351
- if (removed) this.invalidateAggregateMeshStatus(meshId);
7352
- // Node was already absent from the inline mesh (e.g. removed by a
7353
- // prior refine cleanup). Treat as removed so caller gets removed:true.
7354
- if (!removed && !node) removed = true;
7355
- } else {
7356
- const { removeNode } = await import('../config/mesh-config.js');
7357
- removed = removeNode(meshId, nodeId);
7358
- // Node already absent from config (e.g. removed by a prior refine
7359
- // cleanup after a successful Refinery merge). Treat as removed so
7360
- // the response is accurate.
7361
- if (!removed && !node) removed = true;
7362
- if (removed) this.invalidateAggregateMeshStatus(meshId);
7363
- }
7364
-
7365
- // Record in task ledger
7366
- if (removed) {
7367
- try {
7368
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
7369
- appendLedgerEntry(meshId, {
7370
- kind: 'node_removed',
7371
- nodeId,
7372
- payload: {
7373
- worktree: !!node?.isLocalWorktree,
7374
- sessionCleanupMode,
7375
- workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
7376
- daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
7377
- worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
7378
- worktreeCleanupFallback: typeof worktreeCleanup?.fallback === 'string' ? worktreeCleanup.fallback : undefined,
7379
- forced: worktreeCleanup?.forced === true ? true : undefined,
7380
- forceFallbackReason: typeof worktreeCleanup?.reason === 'string' ? worktreeCleanup.reason : undefined,
7381
- },
7382
- });
7383
- } catch { /* ledger append is best-effort */ }
7384
- }
7385
-
7386
- // Surface leftover-directory residue at the top level so callers
7387
- // see the node was dropped from the mesh even though the worktree
7388
- // directory could not be fully removed (best-effort, non-gating).
7389
- const residueWarning = worktreeCleanup?.residue === true && typeof worktreeCleanup?.residueWarning === 'string'
7390
- ? worktreeCleanup.residueWarning
7391
- : undefined;
7392
- return {
7393
- success: true,
7394
- removed,
7395
- ...(residueWarning ? { residueWarning } : {}),
7396
- ...(sessionCleanup ? { sessionCleanup } : {}),
7397
- ...(worktreeCleanup ? { worktreeCleanup } : {}),
7398
- };
7399
- } catch (e: any) {
7400
- return { success: false, error: e.message };
7401
- }
7402
- }
7403
-
7404
- case 'clone_mesh_node': {
7405
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7406
- const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
7407
- const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
7408
- const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
7409
- if (!meshId) return { success: false, error: 'meshId required' };
7410
- if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
7411
- if (!branch) return { success: false, error: 'branch required' };
7412
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
7413
- if (ownerFailure) return ownerFailure;
7414
-
7415
- try {
7416
- // Resolve with preferInline so the clone writes the new node into the
7417
- // same representation that get_mesh reads back. The MCP coordinator
7418
- // passes inlineMesh on every mesh command, so when it owns an inline
7419
- // mesh the membership read path (get_mesh, preferInline: true) returns
7420
- // the inline cache. Without preferInline here, clone could resolve to a
7421
- // local-config mesh and write the node only to config — leaving the
7422
- // inline cache (and therefore get_mesh / refreshMeshFromDaemon) without
7423
- // the node, so the new worktree node is never visible in live mesh
7424
- // membership even though worktree_bootstrap_complete fires.
7425
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7426
- const mesh = meshRecord?.mesh;
7427
- if (!mesh) return { success: false, error: 'Mesh not found' };
7428
-
7429
- const sourceNode = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, sourceNodeId));
7430
- if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
7431
-
7432
- // Forward to the source node's daemon if it's on a different machine.
7433
- // _meshDirectDispatch prevents infinite re-forwarding when the stored daemonId
7434
- // uses a legacy format that doesn't match the receiving daemon's statusInstanceId.
7435
- const sourceDaemonId = typeof sourceNode.daemonId === 'string' ? sourceNode.daemonId.trim() : undefined;
7436
- // daemonIdsEquivalent: an equivalent-form source daemonId is this machine —
7437
- // clone locally, do not forward. Equivalent → local.
7438
- if (sourceDaemonId && !daemonIdsEquivalent(sourceDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
7439
- && !args?._meshDirectDispatch) {
7440
- const forwarded = await this.deps.dispatchMeshCommand(sourceDaemonId, 'clone_mesh_node', {
7441
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7442
- _meshDirectDispatch: true,
7443
- });
7444
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7445
- }
7446
-
7447
- const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
7448
- const { createWorktree } = await import('../git/git-worktree.js');
7449
- const result = await createWorktree({
7450
- repoRoot,
7451
- branch,
7452
- baseBranch,
7453
- meshName: mesh.name,
7454
- });
7455
-
7456
- let node: any;
7457
- if (meshRecord.inline) {
7458
- const { randomUUID } = await import('crypto');
7459
- node = {
7460
- id: `node_${randomUUID().replace(/-/g, '')}`,
7461
- workspace: result.worktreePath,
7462
- repoRoot: result.worktreePath,
7463
- daemonId: sourceNode.daemonId,
7464
- machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
7465
- userOverrides: { ...(sourceNode.userOverrides || {}) },
7466
- policy: { ...(sourceNode.policy || {}) },
7467
- isLocalWorktree: true,
7468
- worktreeBranch: result.branch,
7469
- clonedFromNodeId: sourceNodeId,
7470
- };
7471
- this.updateInlineMeshNode(meshId, mesh, node);
7472
- } else {
7473
- const { addNode } = await import('../config/mesh-config.js');
7474
- node = addNode(meshId, {
7475
- workspace: result.worktreePath,
7476
- repoRoot: result.worktreePath,
7477
- daemonId: sourceNode.daemonId,
7478
- machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
7479
- userOverrides: { ...(sourceNode.userOverrides || {}) },
7480
- isLocalWorktree: true,
7481
- worktreeBranch: result.branch,
7482
- clonedFromNodeId: sourceNodeId,
7483
- policy: { ...(sourceNode.policy || {}) },
7484
- });
7485
- if (!node) return { success: false, error: 'Failed to register worktree node' };
7486
- // Also reconcile the freshly-registered node into any warmed inline
7487
- // cache for this mesh. get_mesh (preferInline: true) reads the inline
7488
- // cache first when one exists; if we only wrote to local config the
7489
- // node would be invisible to membership reads. updateInlineMeshNode is
7490
- // a no-op when no inline cache is present.
7491
- const inlineForReconcile = this.getCachedInlineMesh(meshId);
7492
- if (inlineForReconcile) this.updateInlineMeshNode(meshId, inlineForReconcile, node);
7493
- this.invalidateAggregateMeshStatus(meshId);
7494
- }
7495
-
7496
- const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
7497
- node.worktreeBootstrap = bootstrapState;
7498
- if (meshRecord.inline) {
7499
- this.updateInlineMeshNode(meshId, mesh, node);
7500
- return;
7501
- }
7502
- try {
7503
- const { updateNode } = await import('../config/mesh-config.js');
7504
- updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
7505
- this.invalidateAggregateMeshStatus(meshId);
7506
- } catch { /* bootstrap status persistence is best-effort */ }
7507
- };
7508
-
7509
- const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
7510
- try {
7511
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
7512
- appendLedgerEntry(meshId, {
7513
- kind: 'node_cloned',
7514
- nodeId: node.id,
7515
- payload: {
7516
- sourceNodeId,
7517
- branch: result.branch,
7518
- worktreePath: result.worktreePath,
7519
- submodulesInitialized: initSubmodules,
7520
- worktreeBootstrap: {
7521
- status: bootstrapState.status,
7522
- required: bootstrapState.required,
7523
- configSource: bootstrapState.configSource,
7524
- configSourceType: bootstrapState.configSourceType,
7525
- lastCommand: bootstrapState.lastCommand,
7526
- exitCode: bootstrapState.exitCode,
7527
- },
7528
- },
7529
- });
7530
- } catch { /* ledger append is best-effort */ }
7531
- };
7532
-
7533
- const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
7534
- const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
7535
- const runningBootstrapState: WorktreeBootstrapState = {
7536
- status: 'running',
7537
- required: loadedBootstrap.config?.required !== false,
7538
- configSource: loadedBootstrap.path || loadedBootstrap.source,
7539
- configSourceType: loadedBootstrap.sourceType,
7540
- startedAt: new Date().toISOString(),
7541
- };
7542
- await persistWorktreeSetupState(runningBootstrapState);
7543
-
7544
- const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
7545
- let submodulesInitialized = false;
7546
- if (initSubmodules) {
7547
- try {
7548
- const { runGit } = await import('../git/git-executor.js');
7549
- await runGit(
7550
- { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
7551
- ['submodule', 'update', '--init', '--recursive'],
7552
- { timeoutMs: 120000 },
7553
- );
7554
- submodulesInitialized = true;
7555
-
7556
- // Sync oss submodule to source node HEAD (best-effort)
7557
- const sourceWorkspace = sourceNode.repoRoot || sourceNode.workspace;
7558
- if (sourceWorkspace) {
7559
- try {
7560
- const { runGit: rg } = await import('../git/git-executor.js');
7561
- const sourceCtx = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
7562
- const worktreeCtx = { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true };
7563
-
7564
- // Read source node's oss submodule SHA
7565
- const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', 'oss'], { timeoutMs: 10000 });
7566
- const sourceStatusLine = (typeof sourceStatusOut === 'string' ? sourceStatusOut : (sourceStatusOut as any)?.stdout ?? '').trim();
7567
- const sourceShaMatch = sourceStatusLine.match(/^[+\- ]?([0-9a-f]{40})/);
7568
- const sourceSha = sourceShaMatch?.[1];
7569
-
7570
- if (sourceSha) {
7571
- // Read worktree's current oss HEAD
7572
- const ossCtx = { workspace: `${result.worktreePath}/oss`, repoRoot: `${result.worktreePath}/oss`, isGitRepo: true };
7573
- const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
7574
- const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
7575
-
7576
- if (worktreeOssSha !== sourceSha) {
7577
- // Fetch target SHA from source node's oss directory
7578
- await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
7579
- await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
7580
- await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
7581
- await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
7582
- console.log(`[mesh] Synced oss submodule to source HEAD ${sourceSha.slice(0, 8)} in worktree`);
7583
- }
7584
- }
7585
- } catch (ossErr: any) {
7586
- console.warn('[mesh] oss submodule sync to source HEAD failed (best-effort):', ossErr.message);
7587
- }
7588
- }
7589
- } catch (subErr: any) {
7590
- // Submodule init is best-effort; don't fail the clone
7591
- console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
7592
- }
7593
- }
7594
- const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
7595
- await persistWorktreeSetupState(bootstrapState);
7596
- await appendCloneLedger(submodulesInitialized, bootstrapState);
7597
- return { submodulesInitialized, bootstrapState };
7598
- };
7599
-
7600
- const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
7601
- const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
7602
- ? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
7603
- : 8000;
7604
- const setupPromise = finishWorktreeSetup();
7605
- const setupResult = await Promise.race([
7606
- setupPromise.then((value) => ({ completed: true as const, value })),
7607
- new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
7608
- ]);
7609
-
7610
- const emitBootstrapEvent = (eventStatus: 'bootstrap_complete' | 'bootstrap_failed', bootstrapState: WorktreeBootstrapState, startedAtMs: number, extraPayload?: Record<string, unknown>): void => {
7611
- try {
7612
- const durationMs = Date.now() - startedAtMs;
7613
- const event = `worktree_${eventStatus}` as const;
7614
- const metadataEvent = {
7615
- source: 'clone_mesh_node_bootstrap',
7616
- nodeId: node.id,
7617
- status: eventStatus,
7618
- worktreePath: result.worktreePath,
7619
- durationMs,
7620
- bootstrapStatus: bootstrapState.status,
7621
- ...(bootstrapState.error ? { error: bootstrapState.error } : {}),
7622
- ...(bootstrapState.exitCode !== undefined ? { exitCode: bootstrapState.exitCode } : {}),
7623
- ...(extraPayload || {}),
7624
- };
7625
- if (typeof this.deps.instanceManager?.getByCategory === 'function') {
7626
- const forwarded = handleMeshForwardEvent(
7627
- { instanceManager: this.deps.instanceManager } as any,
7628
- { event, meshId, nodeId: node.id, workspace: result.worktreePath, metadataEvent },
7629
- );
7630
- if (forwarded?.success === true) return;
7631
- }
7632
- queuePendingMeshCoordinatorEvent({
7633
- event,
7634
- meshId,
7635
- nodeLabel: node.id,
7636
- nodeId: node.id,
7637
- workspace: result.worktreePath,
7638
- metadataEvent,
7639
- queuedAt: Date.now(),
7640
- });
7641
- } catch { /* event emission is best-effort */ }
7642
- };
7643
-
7644
- const bootstrapStartedMs = Date.now();
7645
-
7646
- if (!setupResult.completed) {
7647
- setupPromise
7648
- .then(({ bootstrapState }) => {
7649
- emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
7650
- })
7651
- .catch((error: any) => {
7652
- const failedState: WorktreeBootstrapState = {
7653
- ...runningBootstrapState,
7654
- status: 'failed',
7655
- completedAt: new Date().toISOString(),
7656
- error: error?.message || String(error),
7657
- };
7658
- void persistWorktreeSetupState(failedState);
7659
- void appendCloneLedger(false, failedState);
7660
- emitBootstrapEvent('bootstrap_failed', failedState, bootstrapStartedMs, { error: error?.message || String(error) });
7661
- });
7662
- return {
7663
- success: true,
7664
- async: true,
7665
- status: 'accepted',
7666
- node,
7667
- worktreePath: result.worktreePath,
7668
- branch: result.branch,
7669
- worktreeBootstrap: runningBootstrapState,
7670
- worktreeSetup: {
7671
- status: 'running',
7672
- setupWaitMs,
7673
- message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
7674
- },
7675
- };
7676
- }
7677
-
7678
- const { submodulesInitialized, bootstrapState } = setupResult.value;
7679
- emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
7680
- return {
7681
- success: true,
7682
- node,
7683
- worktreePath: result.worktreePath,
7684
- branch: result.branch,
7685
- submodulesInitialized,
7686
- worktreeBootstrap: bootstrapState,
7687
- };
7688
- } catch (e: any) {
7689
- return { success: false, error: e.message };
7690
- }
7691
- }
7692
- case 'retry_mesh_node_bootstrap': {
7693
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7694
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7695
- if (!meshId) return { success: false, error: 'meshId required' };
7696
- if (!nodeId) return { success: false, error: 'nodeId required' };
7697
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'bootstrap retry');
7698
- if (ownerFailure) return ownerFailure;
7699
-
7700
- try {
7701
- // preferInline so bootstrap-retry can resolve inline-cache-only clone worktree nodes.
7702
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7703
- const mesh = meshRecord?.mesh;
7704
- if (!mesh) return { success: false, error: 'Mesh not found' };
7705
-
7706
- const node = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7707
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
7708
- if (!node.isLocalWorktree) return { success: false, error: 'Node is not a local worktree node' };
7709
-
7710
- // Bootstrap runs scripts in the worktree path — forward to the node's daemon if remote.
7711
- // _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
7712
- const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
7713
- // daemonIdsEquivalent: an equivalent-form daemonId is this machine —
7714
- // bootstrap locally, do not forward. Equivalent → local.
7715
- if (nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
7716
- && !args?._meshDirectDispatch) {
7717
- const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId, 'retry_mesh_node_bootstrap', {
7718
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7719
- _meshDirectDispatch: true,
7720
- });
7721
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7722
- }
7723
-
7724
- const currentBootstrap = node.worktreeBootstrap as WorktreeBootstrapState | undefined;
7725
- if (currentBootstrap?.status === 'running') {
7726
- return { success: false, error: 'Bootstrap is already running for this node' };
7727
- }
7728
-
7729
- const worktreePath: string = node.workspace || node.repoRoot;
7730
- if (!worktreePath) return { success: false, error: 'Node has no workspace path' };
7731
-
7732
- const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, worktreePath);
7733
- const runningState: WorktreeBootstrapState = {
7734
- status: 'running',
7735
- required: loadedBootstrap.config?.required !== false,
7736
- configSource: loadedBootstrap.path || loadedBootstrap.source,
7737
- configSourceType: loadedBootstrap.sourceType,
7738
- startedAt: new Date().toISOString(),
7739
- };
7740
-
7741
- const persistState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
7742
- node.worktreeBootstrap = bootstrapState;
7743
- if (meshRecord.inline) {
7744
- this.updateInlineMeshNode(meshId, mesh, node);
7745
- return;
7746
- }
7747
- try {
7748
- const { updateNode } = await import('../config/mesh-config.js');
7749
- updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
7750
- this.invalidateAggregateMeshStatus(meshId);
7751
- } catch { /* best-effort */ }
7752
- };
7753
-
7754
- await persistState(runningState);
7755
- const bootstrapState = await runMeshWorktreeBootstrap(mesh, worktreePath);
7756
- await persistState(bootstrapState);
7757
-
7758
- return { success: true, bootstrapState };
7759
- } catch (e: any) {
7760
- return { success: false, error: e.message };
7761
- }
7762
- }
7763
-
7764
- case 'trigger_mesh_queue': {
7765
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7766
- if (!meshId) return { success: false, error: 'meshId required' };
7767
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
7768
- if (ownerFailure) return ownerFailure;
7769
- try {
7770
- const { triggerMeshQueue, tryAssignQueueTask } = await import('../mesh/mesh-events.js');
7771
-
7772
- // Bug A fix: when preferredNodeId is provided, attempt to claim a pending
7773
- // task for the preferred node's idle session first, before the general
7774
- // round-robin trigger picks a different node.
7775
- const preferredNodeId = typeof args?.preferredNodeId === 'string' ? args.preferredNodeId.trim() : '';
7776
- if (preferredNodeId) {
7777
- const cliInstances = this.deps.instanceManager.getByCategory('cli');
7778
- // Sort: preferred node's sessions first, others after
7779
- const sorted = [...cliInstances].sort((a, b) => {
7780
- const aSettings = a.getState().settings as Record<string, unknown> || {};
7781
- const bSettings = b.getState().settings as Record<string, unknown> || {};
7782
- const aNode = readStringValue(aSettings.meshNodeId, aSettings.nodeId);
7783
- const bNode = readStringValue(bSettings.meshNodeId, bSettings.nodeId);
7784
- return (aNode === preferredNodeId ? -1 : 0) - (bNode === preferredNodeId ? -1 : 0);
7785
- });
7786
- for (const inst of sorted) {
7787
- const state = inst.getState();
7788
- const settings = state.settings as Record<string, unknown> || {};
7789
- const nodeId = readStringValue(settings.meshNodeId, settings.nodeId);
7790
- if (!nodeId || nodeId !== preferredNodeId) continue;
7791
- const meshNodeFor = readStringValue(settings.meshNodeFor);
7792
- if (meshNodeFor !== meshId) continue;
7793
- const status = (readStringValue(state.status) || '').toLowerCase();
7794
- if (status !== 'idle') continue;
7795
- const sessionId = typeof state.instanceId === 'string' ? state.instanceId : '';
7796
- const providerType = readStringValue(state.type, settings.providerType) || '';
7797
- if (sessionId && providerType) {
7798
- tryAssignQueueTask(this.deps as any, meshId, nodeId, sessionId, providerType);
7799
- break;
7800
- }
7801
- }
7802
- }
7803
-
7804
- const trigger = await triggerMeshQueue(this.deps as any, meshId);
7805
- return { success: true, trigger };
7806
- } catch (e: any) {
7807
- return { success: false, error: e.message };
7808
- }
7809
- }
7810
-
7811
6266
  // ─── Mesh Coordinator Launch ───
7812
6267
  case 'launch_mesh_coordinator': {
7813
6268
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';