@adhdev/daemon-core 0.9.82-rc.185 → 0.9.82-rc.187

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3
3
  import * as os from 'node:os'
4
- import { isAbsolute, join, resolve } from 'node:path'
4
+ import { basename, isAbsolute, join, resolve } from 'node:path'
5
5
  import { LOG } from '../logging/logger.js'
6
6
  import type {
7
7
  MeshCoordinatorMcpConfigFormat,
@@ -39,6 +39,7 @@ export type MeshCoordinatorSetup =
39
39
  command: string
40
40
  requiresRestart: boolean
41
41
  instructions: string
42
+ mcpServer: MeshCoordinatorMcpServerLaunch
42
43
  }
43
44
  | {
44
45
  kind: 'unsupported'
@@ -71,6 +72,8 @@ function resolveHermesMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupO
71
72
  const mcpServer = resolveAdhdevMcpServerLaunch({
72
73
  meshId: options.meshId,
73
74
  adhdevMcpCommand: options.adhdevMcpCommand,
75
+ adhdevMcpEntryPath: options.adhdevMcpEntryPath,
76
+ nodeExecutable: options.nodeExecutable,
74
77
  adhdevMcpTransport: options.adhdevMcpTransport,
75
78
  adhdevMcpPort: options.adhdevMcpPort,
76
79
  })
@@ -144,6 +147,8 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
144
147
  const mcpServer = resolveAdhdevMcpServerLaunch({
145
148
  meshId,
146
149
  adhdevMcpCommand: options.adhdevMcpCommand,
150
+ adhdevMcpEntryPath: options.adhdevMcpEntryPath,
151
+ nodeExecutable: options.nodeExecutable,
147
152
  adhdevMcpTransport: options.adhdevMcpTransport,
148
153
  adhdevMcpPort: options.adhdevMcpPort,
149
154
  })
@@ -168,22 +173,41 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
168
173
  if (!instructions || !template?.trim()) {
169
174
  return { kind: 'unsupported', reason: 'Provider manual MCP setup is missing instructions or template' }
170
175
  }
171
- const renderedTemplate = renderMeshCoordinatorTemplate(template, {
176
+ const mcpServer = resolveAdhdevMcpServerLaunch({
177
+ meshId,
178
+ adhdevMcpCommand: options.adhdevMcpCommand,
179
+ adhdevMcpEntryPath: options.adhdevMcpEntryPath,
180
+ nodeExecutable: options.nodeExecutable,
181
+ adhdevMcpTransport: options.adhdevMcpTransport,
182
+ adhdevMcpPort: options.adhdevMcpPort,
183
+ })
184
+ if (!mcpServer) {
185
+ return {
186
+ kind: 'unsupported',
187
+ reason: 'Could not resolve the ADHDev MCP server entrypoint and transport arguments',
188
+ }
189
+ }
190
+ let renderedTemplate = renderMeshCoordinatorTemplate(template, {
172
191
  meshId,
173
192
  workspace,
174
193
  serverName,
175
- adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
194
+ adhdevMcpCommand: mcpServer.command,
195
+ adhdevMcpArgs: mcpServer.args.join(' '),
176
196
  })
177
197
  // Detect if the template is a runnable CLI command (single line, no YAML/JSON structure).
178
198
  // If so, use cli_command kind so the daemon can execute it automatically.
179
199
  const isCliCommand = !renderedTemplate.trim().includes('\n') && !renderedTemplate.trim().startsWith('{')
180
200
  if (isCliCommand) {
201
+ if (!/\{\{\s*adhdevMcpArgs\s*\}\}/.test(template)) {
202
+ renderedTemplate = replaceLegacyCliCommandMcpArgs(renderedTemplate, mcpServer.args)
203
+ }
181
204
  return {
182
205
  kind: 'cli_command',
183
206
  serverName,
184
207
  command: renderedTemplate.trim(),
185
208
  requiresRestart: mcpConfig.requiresRestart === true,
186
209
  instructions: instructions,
210
+ mcpServer,
187
211
  }
188
212
  }
189
213
  return {
@@ -204,7 +228,14 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
204
228
  }
205
229
 
206
230
  function renderMeshCoordinatorTemplate(template: string, values: Record<string, string>): string {
207
- return template.replace(/\{\{\s*(meshId|workspace|serverName|adhdevMcpCommand)\s*\}\}/g, (_, key: string) => values[key] || '')
231
+ return template.replace(/\{\{\s*(meshId|workspace|serverName|adhdevMcpCommand|adhdevMcpArgs)\s*\}\}/g, (_, key: string) => values[key] || '')
232
+ }
233
+
234
+ function replaceLegacyCliCommandMcpArgs(command: string, args: string[]): string {
235
+ return command.replace(
236
+ /\bmcp\s+--mode\s+(?:ipc|local)\s+--repo-mesh\s+\S+(?:\s+--port\s+\d+)?\s*$/,
237
+ args.join(' '),
238
+ )
208
239
  }
209
240
 
210
241
  function resolveHermesCoordinatorHome(meshId: string, workspace: string): string {
@@ -224,12 +255,29 @@ function resolveMcpConfigPath(configPath: string, workspace: string): string {
224
255
  function resolveAdhdevMcpServerLaunch(options: {
225
256
  meshId: string
226
257
  adhdevMcpCommand?: string
258
+ adhdevMcpEntryPath?: string
259
+ nodeExecutable?: string
227
260
  adhdevMcpTransport?: 'local' | 'ipc'
228
261
  adhdevMcpPort?: number
229
262
  }): MeshCoordinatorMcpServerLaunch | null {
263
+ const directEntryPath = resolveAdhdevMcpEntryPath(options.adhdevMcpEntryPath)
264
+ if (directEntryPath) {
265
+ const transport = resolveMcpTransport(options.adhdevMcpTransport)
266
+ const args = [directEntryPath, '--mode', transport, '--repo-mesh', options.meshId]
267
+ const port = resolveMcpPort(options.adhdevMcpPort)
268
+ if (port !== undefined) args.push('--port', String(port))
269
+ return {
270
+ command: resolveNodeExecutable(options.nodeExecutable),
271
+ args,
272
+ }
273
+ }
274
+
230
275
  const command = resolveAdhdevCommand(options.adhdevMcpCommand)
231
276
  const transport = resolveMcpTransport(options.adhdevMcpTransport)
232
- const args = ['mcp', '--mode', transport, '--repo-mesh', options.meshId]
277
+ const directMcpEntrypoint = basename(command).startsWith('adhdev-mcp')
278
+ || command.includes('/vendor/mcp-server/')
279
+ || command.includes('\\vendor\\mcp-server\\')
280
+ const args = [...(directMcpEntrypoint ? [] : ['mcp']), '--mode', transport, '--repo-mesh', options.meshId]
233
281
  const port = resolveMcpPort(options.adhdevMcpPort)
234
282
  if (port !== undefined) args.push('--port', String(port))
235
283
  return {
@@ -242,6 +290,17 @@ function resolveAdhdevCommand(explicitCommand?: string): string {
242
290
  return explicitCommand?.trim() || process.env.ADHDEV_COORDINATOR_MCP_COMMAND?.trim() || DEFAULT_ADHDEV_MCP_COMMAND
243
291
  }
244
292
 
293
+ function resolveAdhdevMcpEntryPath(explicitEntryPath?: string): string | null {
294
+ const entryPath = explicitEntryPath?.trim() || process.env.ADHDEV_COORDINATOR_MCP_ENTRY_PATH?.trim()
295
+ return entryPath || null
296
+ }
297
+
298
+ function resolveNodeExecutable(explicitNodeExecutable?: string): string {
299
+ return explicitNodeExecutable?.trim()
300
+ || process.env.ADHDEV_COORDINATOR_NODE_EXECUTABLE?.trim()
301
+ || process.execPath
302
+ }
303
+
245
304
  function resolveMcpTransport(explicitTransport?: 'local' | 'ipc'): 'local' | 'ipc' {
246
305
  if (explicitTransport === 'local' || explicitTransport === 'ipc') return explicitTransport
247
306
  const envTransport = process.env.ADHDEV_COORDINATOR_MCP_TRANSPORT?.trim()
@@ -418,6 +477,52 @@ export interface PtyExecResult {
418
477
  timedOut: boolean
419
478
  }
420
479
 
480
+ export interface MeshCoordinatorRegistrationStep {
481
+ command: string
482
+ args: string[]
483
+ required: boolean
484
+ label: 'remove_existing' | 'register'
485
+ }
486
+
487
+ /**
488
+ * Codex rejects `mcp add` when a server with the same name already exists.
489
+ * Replace that entry before registering so transport/mesh changes do not leave
490
+ * a fresh coordinator attached to stale MCP launch arguments.
491
+ */
492
+ export function buildMeshCoordinatorRegistrationPlan(
493
+ cliType: string,
494
+ serverName: string,
495
+ registrationCommand: string,
496
+ ): MeshCoordinatorRegistrationStep[] {
497
+ const commandParts = registrationCommand.trim().split(/\s+/).filter(Boolean)
498
+ const [command, ...args] = commandParts
499
+ if (!command) return []
500
+
501
+ const register: MeshCoordinatorRegistrationStep = {
502
+ command,
503
+ args,
504
+ required: true,
505
+ label: 'register',
506
+ }
507
+ if (
508
+ cliType === 'codex-cli'
509
+ && basename(command) === 'codex'
510
+ && args[0] === 'mcp'
511
+ && args[1] === 'add'
512
+ ) {
513
+ return [
514
+ {
515
+ command,
516
+ args: ['mcp', 'remove', serverName],
517
+ required: false,
518
+ label: 'remove_existing',
519
+ },
520
+ register,
521
+ ]
522
+ }
523
+ return [register]
524
+ }
525
+
421
526
  /**
422
527
  * Run a one-shot CLI command under a real PTY and collect its output.
423
528
  *
@@ -4852,6 +4852,13 @@ export class DaemonCommandRouter {
4852
4852
  ...(role ? { role } : {}),
4853
4853
  });
4854
4854
  if (!node) return { success: false, error: 'Mesh not found' };
4855
+ // mesh_status hands back a coordinator-memory aggregate
4856
+ // snapshot keyed on (meshId, queueRevision). Adding a
4857
+ // node touches neither, so without an explicit cache
4858
+ // bust the dashboard graph keeps rendering the pre-add
4859
+ // node list (empty for a fresh mesh) even after the
4860
+ // user clicks Refresh.
4861
+ this.invalidateAggregateMeshStatus(meshId);
4855
4862
  return { success: true, node };
4856
4863
  } catch (e: any) {
4857
4864
  return { success: false, error: e.message };
@@ -4889,6 +4896,11 @@ export class DaemonCommandRouter {
4889
4896
  }
4890
4897
  const node = updateNode(meshId, nodeId, patch as any);
4891
4898
  if (!node) return { success: false, error: 'Mesh node not found' };
4899
+ // Provider priority / systemPrompt changes don't touch
4900
+ // the queue revision, so without a manual bust the
4901
+ // cached aggregate keeps surfacing pre-update values
4902
+ // (priority chip, coordinator prompt preview, etc.).
4903
+ this.invalidateAggregateMeshStatus(meshId);
4892
4904
  return { success: true, node };
4893
4905
  } catch (e: any) {
4894
4906
  return { success: false, error: e.message };
@@ -5057,6 +5069,11 @@ export class DaemonCommandRouter {
5057
5069
  let removed = false;
5058
5070
  if (meshRecord?.inline) {
5059
5071
  removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
5072
+ // Inline meshes share the same aggregate snapshot cache as
5073
+ // local-config meshes; without this bust the removed node
5074
+ // keeps showing up in the dashboard graph until the cache
5075
+ // ages out on its own.
5076
+ if (removed) this.invalidateAggregateMeshStatus(meshId);
5060
5077
  } else {
5061
5078
  const { removeNode } = await import('../config/mesh-config.js');
5062
5079
  removed = removeNode(meshId, nodeId);
@@ -5278,10 +5295,8 @@ export class DaemonCommandRouter {
5278
5295
  if (ownerFailure) return ownerFailure;
5279
5296
  try {
5280
5297
  const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
5281
- if (meshId) {
5282
- triggerMeshQueue(this.deps as any, meshId);
5283
- }
5284
- return { success: true };
5298
+ const trigger = await triggerMeshQueue(this.deps as any, meshId);
5299
+ return { success: true, trigger };
5285
5300
  } catch (e: any) {
5286
5301
  return { success: false, error: e.message };
5287
5302
  }
@@ -5434,23 +5449,102 @@ export class DaemonCommandRouter {
5434
5449
  // a real PTY the registration goes through and the
5435
5450
  // exit code tells us whether it actually persisted.
5436
5451
  let mcpRegistrationOk = false;
5452
+ let mcpRegistrationFailure: {
5453
+ command: string;
5454
+ output: string;
5455
+ exitCode: number | null;
5456
+ signal: number | null;
5457
+ timedOut: boolean;
5458
+ } | null = null;
5437
5459
  try {
5438
- const { execUnderPty } = await import('./mesh-coordinator.js');
5439
- const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
5440
- const [regCmd, ...regArgs] = cmdParts;
5441
- LOG.info('MeshCoordinator', `Running MCP registration (pty): ${coordinatorSetup.command}`);
5442
- const ptyResult = await execUnderPty(regCmd, regArgs, { cwd: workspace, timeoutMs: 20_000 });
5443
- if (ptyResult.timedOut) {
5444
- LOG.warn('MeshCoordinator', `MCP registration timed out — last output:\n${ptyResult.output.slice(-2000)}`);
5445
- } else if (ptyResult.exitCode === 0) {
5446
- mcpRegistrationOk = true;
5447
- LOG.info('MeshCoordinator', `MCP registration succeeded (exit=0)`);
5448
- } else {
5449
- // Non-fatal many providers return non-zero on duplicate registration.
5450
- LOG.warn('MeshCoordinator', `MCP registration exit=${ptyResult.exitCode} signal=${ptyResult.signal} — output:\n${ptyResult.output.slice(-2000)}`);
5460
+ const { buildMeshCoordinatorRegistrationPlan, execUnderPty } = await import('./mesh-coordinator.js');
5461
+ const registrationPlan = buildMeshCoordinatorRegistrationPlan(
5462
+ cliType,
5463
+ coordinatorSetup.serverName,
5464
+ coordinatorSetup.command,
5465
+ );
5466
+ for (const step of registrationPlan) {
5467
+ const renderedCommand = [step.command, ...step.args].join(' ');
5468
+ LOG.info('MeshCoordinator', `Running MCP ${step.label} (pty): ${renderedCommand}`);
5469
+ const ptyResult = await execUnderPty(step.command, step.args, { cwd: workspace, timeoutMs: 20_000 });
5470
+ if (ptyResult.exitCode === 0 && !ptyResult.timedOut) {
5471
+ if (step.required) mcpRegistrationOk = true;
5472
+ continue;
5473
+ }
5474
+ LOG.warn('MeshCoordinator', `MCP ${step.label} failed exit=${ptyResult.exitCode} signal=${ptyResult.signal} timedOut=${ptyResult.timedOut} — output:\n${ptyResult.output.slice(-2000)}`);
5475
+ if (step.required) {
5476
+ mcpRegistrationFailure = {
5477
+ command: renderedCommand,
5478
+ output: ptyResult.output.slice(-2000),
5479
+ exitCode: ptyResult.exitCode,
5480
+ signal: ptyResult.signal,
5481
+ timedOut: ptyResult.timedOut,
5482
+ };
5483
+ break;
5484
+ }
5451
5485
  }
5452
5486
  } catch (error: any) {
5453
5487
  LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
5488
+ mcpRegistrationFailure = {
5489
+ command: coordinatorSetup.command,
5490
+ output: error?.message || String(error),
5491
+ exitCode: null,
5492
+ signal: null,
5493
+ timedOut: false,
5494
+ };
5495
+ }
5496
+
5497
+ if (!mcpRegistrationOk) {
5498
+ return {
5499
+ success: false,
5500
+ code: 'mesh_coordinator_mcp_registration_failed',
5501
+ error: `Could not register ${coordinatorSetup.serverName}; coordinator session was not launched`,
5502
+ meshId,
5503
+ cliType,
5504
+ workspace,
5505
+ registration: mcpRegistrationFailure,
5506
+ };
5507
+ }
5508
+
5509
+ // Codex gives repo-local .mcp.json precedence over its
5510
+ // global `codex mcp add` registration. Refresh an
5511
+ // existing ADHDev entry so a stale workspace command
5512
+ // cannot shadow the registration we just verified.
5513
+ if (cliType === 'codex-cli') {
5514
+ const repoMcpConfigPath = pathJoin(workspace, '.mcp.json');
5515
+ if (fs.existsSync(repoMcpConfigPath)) {
5516
+ try {
5517
+ const repoMcpConfig = parseMeshCoordinatorMcpConfig(
5518
+ fs.readFileSync(repoMcpConfigPath, 'utf-8'),
5519
+ 'claude_mcp_json',
5520
+ );
5521
+ const existingServers = repoMcpConfig.mcpServers;
5522
+ if (
5523
+ existingServers
5524
+ && typeof existingServers === 'object'
5525
+ && !Array.isArray(existingServers)
5526
+ && existingServers[coordinatorSetup.serverName]
5527
+ ) {
5528
+ fs.writeFileSync(repoMcpConfigPath, serializeMeshCoordinatorMcpConfig({
5529
+ ...repoMcpConfig,
5530
+ mcpServers: {
5531
+ ...existingServers,
5532
+ [coordinatorSetup.serverName]: coordinatorSetup.mcpServer,
5533
+ },
5534
+ }, 'claude_mcp_json'), 'utf-8');
5535
+ LOG.info('MeshCoordinator', `Refreshed repo-local ${repoMcpConfigPath} entry for ${coordinatorSetup.serverName}`);
5536
+ }
5537
+ } catch (error: any) {
5538
+ return {
5539
+ success: false,
5540
+ code: 'mesh_coordinator_config_write_failed',
5541
+ error: `Could not refresh repo-local MCP config: ${error?.message || error}`,
5542
+ meshId,
5543
+ cliType,
5544
+ workspace,
5545
+ };
5546
+ }
5547
+ }
5454
5548
  }
5455
5549
 
5456
5550
  // Inject system prompt declaratively from provider.v1.json.
@@ -1350,6 +1350,7 @@ type ProviderNativeHistoryReadResult = {
1350
1350
  sourcePath: string;
1351
1351
  sourceMtimeMs: number;
1352
1352
  providerSessionId?: string;
1353
+ workspace?: string;
1353
1354
  nativeHistoryCoverage?: string;
1354
1355
  partialReason?: string;
1355
1356
  unavailableReason?: string;
@@ -1449,6 +1450,7 @@ function callProviderNativeHistoryRead(
1449
1450
  sourcePath: typeof (result as any).sourcePath === 'string' ? (result as any).sourcePath : '',
1450
1451
  sourceMtimeMs: Number((result as any).sourceMtimeMs) || 0,
1451
1452
  providerSessionId: typeof (result as any).providerSessionId === 'string' ? (result as any).providerSessionId.trim() : undefined,
1453
+ workspace: typeof (result as any).workspace === 'string' ? (result as any).workspace.trim() : undefined,
1452
1454
  nativeHistoryCoverage: typeof (result as any).nativeHistoryCoverage === 'string' ? (result as any).nativeHistoryCoverage.trim() : undefined,
1453
1455
  partialReason: typeof (result as any).partialReason === 'string' ? (result as any).partialReason.trim() : undefined,
1454
1456
  unavailableReason: typeof (result as any).unavailableReason === 'string' ? (result as any).unavailableReason.trim() : undefined,
@@ -1537,6 +1539,7 @@ export function readProviderChatHistory(
1537
1539
  sourcePath?: string;
1538
1540
  sourceMtimeMs?: number;
1539
1541
  providerSessionId?: string;
1542
+ workspace?: string;
1540
1543
  nativeHistoryCoverage?: string;
1541
1544
  partialReason?: string;
1542
1545
  unavailableReason?: string;
@@ -1550,6 +1553,7 @@ export function readProviderChatHistory(
1550
1553
  sourcePath: nativeResult.sourcePath,
1551
1554
  sourceMtimeMs: nativeResult.sourceMtimeMs,
1552
1555
  providerSessionId: nativeResult.providerSessionId,
1556
+ workspace: nativeResult.workspace,
1553
1557
  nativeHistoryCoverage: nativeResult.nativeHistoryCoverage,
1554
1558
  partialReason: nativeResult.partialReason,
1555
1559
  unavailableReason: nativeResult.unavailableReason,
package/src/index.ts CHANGED
@@ -229,7 +229,7 @@ export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHost
229
229
  // export type { MeshGraph, MeshGraphNode, MeshGraphEdge, MeshGraphNodeType, MeshGraphEdgeType } from './mesh/mesh-visualization.js';
230
230
 
231
231
  // ── Mesh Events ──
232
- export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
232
+ export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, reconcileDirectDispatchCompletionFromTranscript } from './mesh/mesh-events.js';
233
233
  export type { PendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
234
234
 
235
235
  // ── Mesh P2P Relay Failure Classification ──
@@ -474,6 +474,10 @@ export class BeadsDB {
474
474
  `).run(cutoff);
475
475
  }
476
476
 
477
+ deleteDirectDispatches(meshId: string): void {
478
+ this.db.prepare(`DELETE FROM mesh_direct_dispatches WHERE mesh_id = ?`).run(meshId);
479
+ }
480
+
477
481
  markStaleDirectDispatches(meshId: string, olderThanMs: number): void {
478
482
  const cutoff = new Date(Date.now() - olderThanMs).toISOString();
479
483
  const now = new Date().toISOString();