@adhdev/daemon-core 0.9.82-rc.360 → 0.9.82-rc.362

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.
Files changed (38) hide show
  1. package/dist/commands/cli-manager.d.ts +13 -0
  2. package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
  3. package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
  4. package/dist/commands/low-family/diagnostics.d.ts +2 -0
  5. package/dist/commands/low-family/index.d.ts +3 -0
  6. package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
  7. package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
  8. package/dist/commands/low-family/notification.d.ts +2 -0
  9. package/dist/commands/low-family/refine-config.d.ts +2 -0
  10. package/dist/commands/low-family/session-host.d.ts +2 -0
  11. package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
  12. package/dist/commands/low-family/status-meta.d.ts +2 -0
  13. package/dist/commands/low-family/types.d.ts +33 -0
  14. package/dist/commands/router.d.ts +0 -1
  15. package/dist/index.js +5314 -5204
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +4833 -4723
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/mesh/mesh-events-utils.d.ts +0 -2
  20. package/package.json +2 -2
  21. package/src/commands/cli-manager.ts +69 -4
  22. package/src/commands/low-family/coordinator-prompt.ts +72 -0
  23. package/src/commands/low-family/daemon-lifecycle.ts +107 -0
  24. package/src/commands/low-family/diagnostics.ts +57 -0
  25. package/src/commands/low-family/index.ts +37 -0
  26. package/src/commands/low-family/mesh-ledger.ts +62 -0
  27. package/src/commands/low-family/mesh-node-logs.ts +81 -0
  28. package/src/commands/low-family/notification.ts +116 -0
  29. package/src/commands/low-family/refine-config.ts +106 -0
  30. package/src/commands/low-family/session-host.ts +274 -0
  31. package/src/commands/low-family/spec-providerdev.ts +217 -0
  32. package/src/commands/low-family/status-meta.ts +112 -0
  33. package/src/commands/low-family/types.ts +39 -0
  34. package/src/commands/router.ts +22 -1076
  35. package/src/mesh/contracts.ts +12 -3
  36. package/src/mesh/mesh-events-coordinator.ts +1 -1
  37. package/src/mesh/mesh-events-utils.ts +0 -20
  38. package/src/providers/cli-provider-instance.ts +16 -3
@@ -12,16 +12,17 @@
12
12
  import { DaemonCdpManager } from '../cdp/manager.js';
13
13
  import { registerExtensionProviders } from '../cdp/setup.js';
14
14
  import { DaemonCommandHandler } from './handler.js';
15
+ import { lowFamilyRegistry } from './low-family/index.js';
15
16
  import { DaemonCliManager } from './cli-manager.js';
16
17
  import { supportsExplicitSessionResume } from './cli-manager.js';
17
18
  import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
18
19
  import type { ProviderLoader } from '../providers/provider-loader.js';
19
20
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
20
21
  import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
21
- import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
22
+ import { loadConfig, saveConfig } from '../config/config.js';
22
23
  import { loadState, saveState } from '../config/state-store.js';
23
24
  import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
24
- import { appendRecentActivity, getRecentActivity, markSessionSeen, dismissSessionNotification, markSessionNotificationUnread } from '../config/recent-activity.js';
25
+ import { appendRecentActivity, getRecentActivity } from '../config/recent-activity.js';
25
26
  import { getSavedProviderSessions } from '../config/saved-sessions.js';
26
27
  import { listProviderHistorySessions } from '../config/chat-history.js';
27
28
  import { detectIDEs } from '../detection/ide-detector.js';
@@ -47,14 +48,10 @@ import { LOG } from '../logging/logger.js';
47
48
  import { logCommand } from '../logging/command-log.js';
48
49
  import type { CommandLogEntry } from '../logging/command-log.js';
49
50
  import * as yaml from 'js-yaml';
50
- import { getRecentLogs, LOG_PATH } from '../logging/logger.js';
51
- import { readDaemonLogTail, MAX_TAIL_BYTES } from '../logging/log-tail-reader.js';
52
- import { redactLogLines } from '../logging/log-redactor.js';
53
- import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
51
+ import { createInteractionId, recordDebugTrace } from '../logging/debug-trace.js';
54
52
  import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
55
53
  import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
56
- import { buildSessionEntries } from '../status/builders.js';
57
- import { registerMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
54
+ import { registerMeshCoordinator } from '../mesh/coordinator-registry.js';
58
55
  import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, type PendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
59
56
  import { getRecentUnroutableDeliveries } from '../mesh/mesh-routing.js';
60
57
  import { buildMeshWorkerRelayStamp } from '../mesh/mesh-events-utils.js';
@@ -81,10 +78,6 @@ import {
81
78
  type WorktreeBootstrapState,
82
79
  } from '../mesh/worktree-bootstrap-config.js';
83
80
  import { runMeshInit } from '../mesh/mesh-init.js';
84
- import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
85
- import { getDaemonBuildInfo } from '../build-info.js';
86
- import { getSessionCompletionMarker } from '../status/snapshot.js';
87
- import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
88
81
  import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
89
82
  import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
90
83
  import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
@@ -96,29 +89,6 @@ import { normalizeInteractivePromptResponse } from '../providers/types/interacti
96
89
  import { workingDirBasename } from '../providers/working-dir.js';
97
90
  import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
98
91
 
99
- type ReleaseChannel = 'stable' | 'preview';
100
- const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
101
- const CHANNEL_SERVER_URL: Record<ReleaseChannel, string> = {
102
- stable: 'https://api.adhf.dev',
103
- preview: 'https://api-preview.adhf.dev',
104
- };
105
-
106
- function normalizeReleaseChannel(value: unknown): ReleaseChannel | null {
107
- if (typeof value !== 'string') return null;
108
- const normalized = value.trim().toLowerCase();
109
- if (normalized === 'stable' || normalized === 'latest') return 'stable';
110
- if (normalized === 'preview' || normalized === 'next') return 'preview';
111
- return null;
112
- }
113
-
114
- function resolveUpgradeChannel(args: any): ReleaseChannel {
115
- return normalizeReleaseChannel(args?.channel)
116
- || normalizeReleaseChannel(args?.updatePolicy?.channel)
117
- || normalizeReleaseChannel(args?.npmTag)
118
- || normalizeReleaseChannel(loadConfig().updateChannel)
119
- || 'stable';
120
- }
121
-
122
92
  function readProviderPriorityFromPolicy(policy: unknown): string[] {
123
93
  const record = policy && typeof policy === 'object' && !Array.isArray(policy)
124
94
  ? policy as Record<string, unknown>
@@ -336,7 +306,7 @@ function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
336
306
  const machineName = readMeshNodeDisplayMachineName(node);
337
307
  const coordinatorHostname = readStringValue(opts.coordinatorHostname);
338
308
  const machineIdMatches = Boolean(opts.localMachineId && machineId && opts.localMachineId === machineId);
339
- const daemonIdMatches = Boolean(opts.localDaemonId && daemonId && opts.localDaemonId === daemonId);
309
+ const daemonIdMatches = Boolean(opts.localDaemonId && daemonId && daemonIdsEquivalent(opts.localDaemonId, daemonId));
340
310
  const hostnameMatches = Boolean(
341
311
  normalizeMeshHostname(hostname)
342
312
  && normalizeMeshHostname(coordinatorHostname)
@@ -1403,7 +1373,7 @@ async function hydrateInlineMeshDirectTruth(args: {
1403
1373
  const isSelfNode = Boolean(
1404
1374
  nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
1405
1375
  ) || Boolean(
1406
- daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
1376
+ daemonId && (daemonIdsEquivalent(daemonId, args.localMachineId) || daemonIdsEquivalent(daemonId, args.statusInstanceId)),
1407
1377
  ) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
1408
1378
 
1409
1379
  // A dead local worktree owned by this coordinator (isLocalWorktree, the
@@ -1415,7 +1385,7 @@ async function hydrateInlineMeshDirectTruth(args: {
1415
1385
  // strictly self + isLocalWorktree + absent-path; remote peers and nodes
1416
1386
  // whose workspace still exists are unaffected and stay classifiable.
1417
1387
  const isSelfDaemonNode = Boolean(
1418
- daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
1388
+ daemonId && (daemonIdsEquivalent(daemonId, args.localMachineId) || daemonIdsEquivalent(daemonId, args.statusInstanceId)),
1419
1389
  );
1420
1390
  if ((isSelfNode || isSelfDaemonNode) && isDeadLocalWorktreeNode(node)) {
1421
1391
  deadNodeIds.push(nodeId);
@@ -3471,7 +3441,6 @@ const MESH_FORWARDABLE_SESSION_COMMANDS = new Set([
3471
3441
  // delivers it to the real worker instead. (findAdapter is also fail-closed as the backstop.)
3472
3442
  'agent_command',
3473
3443
  ]);
3474
- const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
3475
3444
 
3476
3445
  function normalizeCommandSource(source: string): CommandLogEntry['source'] {
3477
3446
  switch (source) {
@@ -3501,117 +3470,6 @@ function normalizeCommandArgsWithInteractionId(args: any): Record<string, unknow
3501
3470
  * basename to be a literal `*.json`, and re-joins under the verified parent so
3502
3471
  * the returned path can't point outside the tree. Used by get/write_spec_source.
3503
3472
  */
3504
- function resolveSpecPathInProviders(
3505
- specPath: string,
3506
- fsm: typeof import('node:fs'),
3507
- pathm: typeof import('node:path'),
3508
- osm: typeof import('node:os'),
3509
- ): { ok: true; path: string } | { ok: false; error: string } {
3510
- let rootReal: string;
3511
- try {
3512
- rootReal = fsm.realpathSync(pathm.join(osm.homedir(), '.adhdev', 'providers'));
3513
- } catch (e) {
3514
- return { ok: false, error: `providers root unavailable: ${(e as Error).message}` };
3515
- }
3516
- const resolved = pathm.resolve(specPath);
3517
- const base = pathm.basename(resolved);
3518
- if (!/^[\w.-]+\.json$/.test(base)) {
3519
- return { ok: false, error: 'refused: spec file must be a *.json basename' };
3520
- }
3521
- let parentReal: string;
3522
- try {
3523
- parentReal = fsm.realpathSync(pathm.dirname(resolved));
3524
- } catch (e) {
3525
- return { ok: false, error: `spec directory not found: ${(e as Error).message}` };
3526
- }
3527
- if (parentReal !== rootReal && !parentReal.startsWith(rootReal + pathm.sep)) {
3528
- return { ok: false, error: 'refused: spec path must be under the providers root' };
3529
- }
3530
- const safe = pathm.join(parentReal, base);
3531
- // Reject if the final file itself is a symlink pointing elsewhere.
3532
- try {
3533
- const st = fsm.lstatSync(safe);
3534
- if (st.isSymbolicLink()) return { ok: false, error: 'refused: spec path is a symlink' };
3535
- } catch { /* file may not exist yet (write case) — fine */ }
3536
- return { ok: true, path: safe };
3537
- }
3538
-
3539
- function toHostedCliRuntimeDescriptor(record: any): HostedCliRuntimeDescriptor | null {
3540
- if (!record || typeof record !== 'object') return null;
3541
- const runtimeId = typeof record.sessionId === 'string' ? record.sessionId : '';
3542
- const cliType = typeof record.providerType === 'string' ? record.providerType : '';
3543
- const workspace = typeof record.workspace === 'string' ? record.workspace : '';
3544
- if (!runtimeId || !cliType || !workspace) return null;
3545
- return {
3546
- runtimeId,
3547
- runtimeKey: typeof record.runtimeKey === 'string' ? record.runtimeKey : undefined,
3548
- displayName: typeof record.displayName === 'string' ? record.displayName : undefined,
3549
- workspaceLabel: typeof record.workspaceLabel === 'string' ? record.workspaceLabel : undefined,
3550
- lifecycle: typeof record.lifecycle === 'string' ? record.lifecycle as HostedCliRuntimeDescriptor['lifecycle'] : undefined,
3551
- recoveryState: typeof record.meta?.runtimeRecoveryState === 'string'
3552
- ? String(record.meta.runtimeRecoveryState)
3553
- : null,
3554
- cliType,
3555
- workspace,
3556
- cliArgs: Array.isArray(record.meta?.cliArgs) ? record.meta.cliArgs as string[] : [],
3557
- providerSessionId: typeof record.meta?.providerSessionId === 'string'
3558
- ? String(record.meta.providerSessionId)
3559
- : undefined,
3560
- };
3561
- }
3562
-
3563
- function getWriteConflictOwnerClientId(error: unknown): string | undefined {
3564
- const message = typeof error === 'string'
3565
- ? error
3566
- : error instanceof Error
3567
- ? error.message
3568
- : '';
3569
- const match = /^Write owned by\s+(.+)$/.exec(message.trim());
3570
- return match?.[1]?.trim() || undefined;
3571
- }
3572
-
3573
- function summarizeSessionHostRecord(result: unknown): Record<string, unknown> {
3574
- if (!result || typeof result !== 'object') return {};
3575
- const record = result as Record<string, any>;
3576
- return {
3577
- runtimeKey: typeof record.runtimeKey === 'string' ? record.runtimeKey : undefined,
3578
- lifecycle: typeof record.lifecycle === 'string' ? record.lifecycle : undefined,
3579
- surfaceKind: getSessionHostSurfaceKind(record as any),
3580
- attachedClientCount: Array.isArray(record.attachedClients) ? record.attachedClients.length : undefined,
3581
- hasWriteOwner: !!record.writeOwner,
3582
- writeOwnerClientId: typeof record.writeOwner?.clientId === 'string' ? record.writeOwner.clientId : undefined,
3583
- };
3584
- }
3585
-
3586
- function summarizeSessionHostRecords(result: unknown): Record<string, unknown> {
3587
- const records = Array.isArray(result) ? result : [];
3588
- const groups = partitionSessionHostRecords(records as any[]);
3589
- return {
3590
- sessionCount: records.length,
3591
- liveRuntimeCount: groups.liveRuntimes.length,
3592
- recoverySnapshotCount: groups.recoverySnapshots.length,
3593
- inactiveRecordCount: groups.inactiveRecords.length,
3594
- };
3595
- }
3596
-
3597
- function summarizeSessionHostDiagnostics(result: unknown): Record<string, unknown> {
3598
- const diagnostics = result && typeof result === 'object' ? result as Record<string, any> : {};
3599
- const sessions = Array.isArray(diagnostics.sessions) ? diagnostics.sessions : [];
3600
- return {
3601
- runtimeCount: typeof diagnostics.runtimeCount === 'number' ? diagnostics.runtimeCount : undefined,
3602
- ...summarizeSessionHostRecords(sessions),
3603
- };
3604
- }
3605
-
3606
- function summarizeSessionHostPruneResult(result: unknown): Record<string, unknown> {
3607
- const value = result && typeof result === 'object' ? result as Record<string, any> : {};
3608
- return {
3609
- duplicateGroupCount: typeof value.duplicateGroupCount === 'number' ? value.duplicateGroupCount : undefined,
3610
- prunedCount: Array.isArray(value.prunedSessionIds) ? value.prunedSessionIds.length : undefined,
3611
- keptCount: Array.isArray(value.keptSessionIds) ? value.keptSessionIds.length : undefined,
3612
- };
3613
- }
3614
-
3615
3473
  function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
3616
3474
  const raw = hostAddress.trim();
3617
3475
  if (!raw) throw new Error('hostAddress required');
@@ -4671,65 +4529,6 @@ export class DaemonCommandRouter {
4671
4529
  ...(errors.length ? { errors } : {}),
4672
4530
  };
4673
4531
  }
4674
-
4675
- private async traceSessionHostAction<T>(
4676
- action: string,
4677
- args: any,
4678
- run: () => Promise<T>,
4679
- summarizeResult?: (result: T) => Record<string, unknown>,
4680
- ): Promise<T> {
4681
- const interactionId = typeof args?._interactionId === 'string' ? args._interactionId : undefined;
4682
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : undefined;
4683
- const requestedPayload: Record<string, unknown> = { action };
4684
- if (sessionId) requestedPayload.sessionId = sessionId;
4685
- if (typeof args?.clientId === 'string') requestedPayload.clientId = args.clientId;
4686
- if (typeof args?.signal === 'string') requestedPayload.signal = args.signal;
4687
- if (typeof args?.providerType === 'string') requestedPayload.providerType = args.providerType;
4688
- if (typeof args?.workspace === 'string') requestedPayload.workspace = args.workspace;
4689
- if (typeof args?.dryRun === 'boolean') requestedPayload.dryRun = args.dryRun;
4690
-
4691
- recordDebugTrace({
4692
- interactionId,
4693
- category: 'session_host',
4694
- stage: 'action_requested',
4695
- level: 'info',
4696
- sessionId,
4697
- payload: requestedPayload,
4698
- });
4699
-
4700
- try {
4701
- const result = await run();
4702
- recordDebugTrace({
4703
- interactionId,
4704
- category: 'session_host',
4705
- stage: 'action_result',
4706
- level: 'info',
4707
- sessionId,
4708
- payload: {
4709
- ...requestedPayload,
4710
- success: true,
4711
- ...(summarizeResult ? summarizeResult(result) : {}),
4712
- },
4713
- });
4714
- return result;
4715
- } catch (error: any) {
4716
- recordDebugTrace({
4717
- interactionId,
4718
- category: 'session_host',
4719
- stage: 'action_failed',
4720
- level: 'error',
4721
- sessionId,
4722
- payload: {
4723
- ...requestedPayload,
4724
- error: error?.message || String(error),
4725
- failureKind: getWriteConflictOwnerClientId(error) ? 'write_conflict' : 'request_failed',
4726
- conflictOwnerClientId: getWriteConflictOwnerClientId(error),
4727
- },
4728
- });
4729
- throw error;
4730
- }
4731
- }
4732
-
4733
4532
  /**
4734
4533
  * Unified command routing.
4735
4534
  * Returns result for all commands:
@@ -6372,6 +6171,18 @@ export class DaemonCommandRouter {
6372
6171
  }
6373
6172
  }
6374
6173
 
6174
+ // RF-ROUTER LOW family: low-coupling commands (session-host control, spec
6175
+ // provider-dev, refine/change-impact config) are handled by the registry
6176
+ // before the switch. A hit returns the same CommandRouterResult the inlined
6177
+ // case used to; a miss falls through to the switch unchanged.
6178
+ const lowFamilyHandler = lowFamilyRegistry.get(cmd);
6179
+ if (lowFamilyHandler) {
6180
+ return await lowFamilyHandler({
6181
+ deps: this.deps,
6182
+ getMeshForCommand: this.getMeshForCommand.bind(this),
6183
+ }, args);
6184
+ }
6185
+
6375
6186
  switch (cmd) {
6376
6187
  // ─── CLI / ACP commands ───
6377
6188
  case 'mesh_forward_event': {
@@ -6411,7 +6222,7 @@ export class DaemonCommandRouter {
6411
6222
  // fail-closes for a remote node when none resolve. We deliberately do NOT
6412
6223
  // self-stamp this daemon's own id when the field is missing: for a
6413
6224
  // P2P-relayed remote worker launch, stamping the worker's own id would make
6414
- // the self-forward gate (mesh-events-coordinator: sameDaemonId) treat the
6225
+ // the self-forward gate (mesh-events-coordinator: daemonIdsEquivalent) treat the
6415
6226
  // worker as its own coordinator, suppressing the spontaneous completion-event
6416
6227
  // forward and leaving the event in the pending inbox until a read_chat
6417
6228
  // reconcile drains it. If the anchor is genuinely absent here, leave it
@@ -6506,174 +6317,6 @@ export class DaemonCommandRouter {
6506
6317
  }
6507
6318
 
6508
6319
  // ─── Logs ───
6509
- case 'get_logs': {
6510
- const count = parseInt(args?.count) || parseInt(args?.lines) || 100;
6511
- const minLevel = args?.minLevel || 'info';
6512
- const sinceTs = args?.since || 0;
6513
-
6514
- try {
6515
- // Priority 1: ring buffer (fast and structured)
6516
- let logs = getRecentLogs(count, minLevel);
6517
- if (sinceTs > 0) {
6518
- logs = logs.filter((l: any) => l.ts > sinceTs);
6519
- }
6520
- if (logs.length > 0) {
6521
- return { success: true, logs, totalBuffered: logs.length };
6522
- }
6523
- // Incremental polling must not fall back to unfiltered file text: the file
6524
- // format is not timestamp-filterable, and returning its tail makes the UI
6525
- // replace structured logs with old raw fallback lines when nothing new exists.
6526
- if (sinceTs > 0) {
6527
- return { success: true, logs: [], totalBuffered: 0 };
6528
- }
6529
- // Priority 2: file fallback
6530
- if (fs.existsSync(LOG_PATH)) {
6531
- const content = fs.readFileSync(LOG_PATH, 'utf-8');
6532
- const allLines = content.split('\n');
6533
- const recent = allLines.slice(-count).join('\n');
6534
- return { success: true, logs: recent, totalLines: allLines.length };
6535
- }
6536
- return { success: true, logs: [], totalBuffered: 0 };
6537
- } catch (e: any) {
6538
- return { success: false, error: e.message };
6539
- }
6540
- }
6541
-
6542
- case 'get_debug_trace': {
6543
- const count = parseInt(args?.count) || parseInt(args?.limit) || 100;
6544
- const sinceTs = Number(args?.since) || 0;
6545
- const interactionId = typeof args?.interactionId === 'string' ? args.interactionId : undefined;
6546
- const category = typeof args?.category === 'string' ? args.category : undefined;
6547
- const trace = getRecentDebugTrace({ interactionId, category, limit: count })
6548
- .filter((entry) => !sinceTs || entry.ts > sinceTs);
6549
- return { success: true, trace, count: trace.length };
6550
- }
6551
-
6552
- case 'session_host_get_diagnostics': {
6553
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6554
- const diagnostics = await this.traceSessionHostAction('session_host_get_diagnostics', args, () => this.deps.sessionHostControl!.getDiagnostics({
6555
- includeSessions: args?.includeSessions !== false,
6556
- limit: Number(args?.limit) || undefined,
6557
- }), (result) => ({
6558
- includeSessions: args?.includeSessions !== false,
6559
- limit: Number(args?.limit) || undefined,
6560
- ...summarizeSessionHostDiagnostics(result),
6561
- }));
6562
- return { success: true, diagnostics };
6563
- }
6564
-
6565
- case 'session_host_list_sessions': {
6566
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6567
- const sessions = await this.traceSessionHostAction('session_host_list_sessions', args, () => this.deps.sessionHostControl!.listSessions(), (records) => summarizeSessionHostRecords(records));
6568
- return { success: true, sessions };
6569
- }
6570
-
6571
- case 'session_host_stop_session': {
6572
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6573
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
6574
- if (!sessionId) return { success: false, error: 'sessionId required' };
6575
- const record = await this.traceSessionHostAction('session_host_stop_session', args, () => this.deps.sessionHostControl!.stopSession(sessionId), (result) => summarizeSessionHostRecord(result));
6576
- return { success: true, record };
6577
- }
6578
-
6579
- case 'session_host_resume_session': {
6580
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6581
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
6582
- if (!sessionId) return { success: false, error: 'sessionId required' };
6583
- const record = await this.traceSessionHostAction('session_host_resume_session', args, async () => {
6584
- const nextRecord = await this.deps.sessionHostControl!.resumeSession(sessionId);
6585
- const hosted = toHostedCliRuntimeDescriptor(nextRecord);
6586
- if (hosted) {
6587
- await this.deps.cliManager.restoreHostedSessions([hosted]);
6588
- }
6589
- return nextRecord;
6590
- }, (result) => ({
6591
- ...summarizeSessionHostRecord(result),
6592
- restoredHostedSession: !!toHostedCliRuntimeDescriptor(result),
6593
- }));
6594
- return { success: true, record };
6595
- }
6596
-
6597
- case 'session_host_restart_session': {
6598
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6599
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
6600
- if (!sessionId) return { success: false, error: 'sessionId required' };
6601
- const record = await this.traceSessionHostAction('session_host_restart_session', args, async () => {
6602
- const nextRecord = await this.deps.sessionHostControl!.restartSession(sessionId);
6603
- const hosted = toHostedCliRuntimeDescriptor(nextRecord);
6604
- if (hosted) {
6605
- await this.deps.cliManager.restoreHostedSessions([hosted]);
6606
- }
6607
- return nextRecord;
6608
- }, (result) => ({
6609
- ...summarizeSessionHostRecord(result),
6610
- restoredHostedSession: !!toHostedCliRuntimeDescriptor(result),
6611
- }));
6612
- return { success: true, record };
6613
- }
6614
-
6615
- case 'session_host_send_signal': {
6616
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6617
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
6618
- const signal = typeof args?.signal === 'string' ? args.signal : '';
6619
- if (!sessionId) return { success: false, error: 'sessionId required' };
6620
- if (!signal) return { success: false, error: 'signal required' };
6621
- const record = await this.traceSessionHostAction('session_host_send_signal', args, () => this.deps.sessionHostControl!.sendSignal(sessionId, signal), (result) => summarizeSessionHostRecord(result));
6622
- return { success: true, record };
6623
- }
6624
-
6625
- case 'session_host_force_detach_client': {
6626
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6627
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
6628
- const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
6629
- if (!sessionId) return { success: false, error: 'sessionId required' };
6630
- if (!clientId) return { success: false, error: 'clientId required' };
6631
- const record = await this.traceSessionHostAction('session_host_force_detach_client', args, () => this.deps.sessionHostControl!.forceDetachClient(sessionId, clientId), (result) => summarizeSessionHostRecord(result));
6632
- return { success: true, record };
6633
- }
6634
-
6635
- case 'session_host_prune_duplicate_sessions': {
6636
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6637
- const result = await this.traceSessionHostAction('session_host_prune_duplicate_sessions', args, () => this.deps.sessionHostControl!.pruneDuplicateSessions({
6638
- providerType: typeof args?.providerType === 'string' ? args.providerType : undefined,
6639
- workspace: typeof args?.workspace === 'string' ? args.workspace : undefined,
6640
- dryRun: args?.dryRun === true,
6641
- }), (value) => summarizeSessionHostPruneResult(value));
6642
- return { success: true, result };
6643
- }
6644
-
6645
- case 'session_host_acquire_write': {
6646
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6647
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
6648
- const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
6649
- const ownerType = args?.ownerType === 'agent' ? 'agent' : 'user';
6650
- if (!sessionId) return { success: false, error: 'sessionId required' };
6651
- if (!clientId) return { success: false, error: 'clientId required' };
6652
- const record = await this.traceSessionHostAction('session_host_acquire_write', args, () => this.deps.sessionHostControl!.acquireWrite({
6653
- sessionId,
6654
- clientId,
6655
- ownerType,
6656
- force: args?.force !== false,
6657
- }), (result) => ({
6658
- ...summarizeSessionHostRecord(result),
6659
- ownerType,
6660
- }));
6661
- return { success: true, record };
6662
- }
6663
-
6664
- case 'session_host_release_write': {
6665
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
6666
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
6667
- const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
6668
- if (!sessionId) return { success: false, error: 'sessionId required' };
6669
- if (!clientId) return { success: false, error: 'clientId required' };
6670
- const record = await this.traceSessionHostAction('session_host_release_write', args, () => this.deps.sessionHostControl!.releaseWrite({
6671
- sessionId,
6672
- clientId,
6673
- }), (result) => summarizeSessionHostRecord(result));
6674
- return { success: true, record };
6675
- }
6676
-
6677
6320
  case 'list_saved_sessions': {
6678
6321
  const providerType = typeof args?.providerType === 'string'
6679
6322
  ? args.providerType.trim()
@@ -6889,511 +6532,6 @@ export class DaemonCommandRouter {
6889
6532
  return { success: true, detectedInfo: results };
6890
6533
  }
6891
6534
 
6892
- // ─── Set User Name ───
6893
- case 'set_user_name': {
6894
- const name = args?.userName;
6895
- if (!name || typeof name !== 'string') throw new Error('userName required');
6896
- updateConfig({ userName: name });
6897
- return { success: true, userName: name };
6898
- }
6899
-
6900
- case 'get_status_metadata': {
6901
- const snapshot = buildStatusSnapshot({
6902
- allStates: this.deps.instanceManager.collectAllStates(),
6903
- cdpManagers: this.deps.cdpManagers,
6904
- providerLoader: this.deps.providerLoader,
6905
- detectedIdes: this.deps.detectedIdes.value,
6906
- instanceId: this.deps.statusInstanceId || loadConfig().machineId || 'daemon',
6907
- version: this.deps.statusVersion || 'unknown',
6908
- profile: 'metadata',
6909
- });
6910
- // Surface the daemon's build stamp so coordinators (mesh_status)
6911
- // can detect a running daemon that predates a just-merged fix and
6912
- // is awaiting deploy/restart. Sibling of `status` to avoid
6913
- // perturbing the dashboard status snapshot shape.
6914
- return { success: true, status: snapshot, daemonBuild: getDaemonBuildInfo() };
6915
- }
6916
-
6917
- case 'get_machine_runtime_stats': {
6918
- return {
6919
- success: true,
6920
- machine: buildMachineInfo('full'),
6921
- timestamp: Date.now(),
6922
- };
6923
- }
6924
-
6925
- // Session-info popup data. Aggregates whatever the daemon knows
6926
- // about a single live session into one envelope so the dashboard
6927
- // doesn't need to stitch together status + coordinator registry +
6928
- // session registry on the client. Includes the actual system
6929
- // prompt that was injected at launch when the session is a mesh
6930
- // coordinator — that's the "what prompt did the agent see?"
6931
- // question the info-icon dialog is meant to answer.
6932
- case 'get_session_info': {
6933
- const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
6934
- : typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
6935
- if (!sessionId) return { success: false, error: 'targetSessionId required' };
6936
- // Fetch both lookups up front. We used to bail with "Session not
6937
- // found" when sessionRegistry forgot the SID (auto-cleanup,
6938
- // daemon restart with the session not yet restored, etc), which
6939
- // hid the coordinator-side metadata even though the
6940
- // coordinator-registry still has it. Now we return whichever
6941
- // side we have. The dashboard renders "no coordinator-specific
6942
- // prompt" only when *neither* side knows the session.
6943
- const target = this.deps.sessionRegistry.get(sessionId);
6944
- const coord = getCoordinatorForSession(sessionId);
6945
- if (!target && !coord) return { success: false, error: 'Session not found', sessionId };
6946
- const adapter = target
6947
- ? this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter
6948
- : undefined;
6949
- const runtimeMeta = (adapter && typeof (adapter as any).getRuntimeMetadata === 'function')
6950
- ? (adapter as any).getRuntimeMetadata()
6951
- : undefined;
6952
- // Launch metadata (args / cwd / extra-env keys / providerSessionId) is
6953
- // derived from the live adapter's spawn plan; only available while the
6954
- // adapter is alive (resumed-from-history sessions report nothing here).
6955
- const launchInfo = (adapter && typeof (adapter as any).getLaunchInfo === 'function')
6956
- ? (adapter as any).getLaunchInfo()
6957
- : undefined;
6958
- const providerType = target?.providerType || coord?.cliType || '';
6959
- const providerMetaForSession = providerType
6960
- ? this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType)
6961
- : undefined;
6962
- return {
6963
- success: true,
6964
- session: {
6965
- sessionId,
6966
- providerType,
6967
- providerName: providerMetaForSession?.name,
6968
- transport: target?.transport,
6969
- workspace: (target as any)?.workspace || coord?.workspace,
6970
- spawnedAtMs: (target as any)?.spawnedAtMs || coord?.startedAt,
6971
- // providerSessionId now comes from the live adapter's launch info
6972
- // (the registry target never carried it — it was always undefined).
6973
- providerSessionId: launchInfo?.providerSessionId || (target as any)?.providerSessionId,
6974
- runtimeMetadata: runtimeMeta,
6975
- launch: launchInfo,
6976
- },
6977
- coordinator: coord ? {
6978
- meshId: coord.meshId,
6979
- startedAt: coord.startedAt,
6980
- cliType: coord.cliType,
6981
- systemPrompt: coord.systemPrompt,
6982
- extraSystemPrompt: coord.extraSystemPrompt,
6983
- injection: coord.injection,
6984
- mcpConfigPath: coord.mcpConfigPath,
6985
- } : null,
6986
- };
6987
- }
6988
-
6989
- case 'get_spec_debug': {
6990
- const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
6991
- : typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
6992
- if (!sessionId) return { success: false, error: 'targetSessionId required' };
6993
- const target = this.deps.sessionRegistry.get(sessionId);
6994
- if (!target) return { success: false, error: 'Session not found', sessionId };
6995
- const adapterObj = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
6996
- const snapshot = adapterObj
6997
- ? (typeof (adapterObj as any).getDebugSnapshot === 'function'
6998
- ? (adapterObj as any).getDebugSnapshot()
6999
- : typeof (adapterObj as any).getDebugState === 'function'
7000
- ? (adapterObj as any).getDebugState()
7001
- : null)
7002
- : null;
7003
- return {
7004
- success: true,
7005
- sessionId,
7006
- providerType: target.providerType,
7007
- isSpecProvider: snapshot !== null,
7008
- snapshot,
7009
- };
7010
- }
7011
-
7012
- // ── Spec source read/write for the debug panel's live editor.
7013
- // Lets the dashboard load a session's spec.json, edit it, and
7014
- // save it back — the driver's fs.watch picks up the change and
7015
- // hot-reloads the FSM with no restart. Writes are confined to
7016
- // files under ~/.adhdev/providers to avoid arbitrary fs access.
7017
- case 'get_spec_source': {
7018
- const fsm = await import('node:fs');
7019
- const pathm = await import('node:path');
7020
- const osm = await import('node:os');
7021
- const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
7022
- : typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
7023
- let specPath = typeof args?.specPath === 'string' ? args.specPath : '';
7024
- if (!specPath && sessionId) {
7025
- const target = this.deps.sessionRegistry.get(sessionId);
7026
- const adapterObj = target ? this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter : null;
7027
- const snap = adapterObj && typeof (adapterObj as any).getDebugSnapshot === 'function' ? (adapterObj as any).getDebugSnapshot() : null;
7028
- specPath = snap?.specPath ?? '';
7029
- }
7030
- if (!specPath) return { success: false, error: 'specPath or resolvable targetSessionId required' };
7031
- // Confine reads to the providers tree, resolving symlinks so a
7032
- // crafted path can't escape via a symlinked spec file.
7033
- const safe = resolveSpecPathInProviders(specPath, fsm, pathm, osm);
7034
- if (!safe.ok) return { success: false, error: safe.error, specPath };
7035
- try {
7036
- const content = fsm.readFileSync(safe.path, 'utf8');
7037
- return { success: true, specPath: safe.path, content };
7038
- } catch (e) {
7039
- return { success: false, error: `read failed: ${(e as Error).message}`, specPath };
7040
- }
7041
- }
7042
-
7043
- case 'write_spec_source': {
7044
- const fsm = await import('node:fs');
7045
- const pathm = await import('node:path');
7046
- const osm = await import('node:os');
7047
- const specPath = typeof args?.specPath === 'string' ? args.specPath : '';
7048
- const content = typeof args?.content === 'string' ? args.content : '';
7049
- if (!specPath) return { success: false, error: 'specPath required' };
7050
- if (!content) return { success: false, error: 'content required' };
7051
- // Confine writes to the providers tree (symlink-safe — see helper).
7052
- const safe = resolveSpecPathInProviders(specPath, fsm, pathm, osm);
7053
- if (!safe.ok) return { success: false, error: safe.error };
7054
- // Validate JSON + (if v4) FSM structure before writing so a bad
7055
- // edit can't break the live session — return precise errors.
7056
- let parsed: unknown;
7057
- try { parsed = JSON.parse(content); }
7058
- catch (e) { return { success: false, error: `invalid JSON: ${(e as Error).message}` }; }
7059
- if ((parsed as any)?.$schema === 'adhdev:cli/spec@4') {
7060
- const { validateFsmSpec } = await import('../providers/spec/fsm-loader.js');
7061
- const errs = validateFsmSpec(parsed);
7062
- if (errs.length) return { success: false, error: 'spec invalid', validationErrors: errs };
7063
- }
7064
- try {
7065
- fsm.writeFileSync(safe.path, content, 'utf8');
7066
- return { success: true, specPath: safe.path };
7067
- } catch (e) {
7068
- return { success: false, error: `write failed: ${(e as Error).message}` };
7069
- }
7070
- }
7071
-
7072
- // ── Validate an in-progress spec (string or object) without writing.
7073
- // The form builder calls this on every change so Save can stay
7074
- // disabled while there are structural / reference / regex errors.
7075
- case 'validate_spec': {
7076
- let parsed: unknown = args?.spec;
7077
- if (typeof args?.content === 'string') {
7078
- try { parsed = JSON.parse(args.content); }
7079
- catch (e) { return { success: true, valid: false, errors: [`invalid JSON: ${(e as Error).message}`] }; }
7080
- }
7081
- if (!parsed || typeof parsed !== 'object') {
7082
- return { success: true, valid: false, errors: ['spec must be an object or content string'] };
7083
- }
7084
- const schema = (parsed as any).$schema;
7085
- if (schema === 'adhdev:cli/spec@4') {
7086
- const { validateFsmSpec } = await import('../providers/spec/fsm-loader.js');
7087
- const errors = validateFsmSpec(parsed);
7088
- return { success: true, valid: errors.length === 0, errors };
7089
- }
7090
- // v1/v3 left to the legacy loader path; the builder is v4-only.
7091
- return { success: true, valid: false, errors: [`unsupported $schema "${schema}" — form builder is v4-only`] };
7092
- }
7093
-
7094
- // ── Evaluate a single condition against a live session's current
7095
- // screen — powers the editor's "does this match right now?"
7096
- // preview. Returns the recursive match tree.
7097
- case 'eval_condition_preview': {
7098
- const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
7099
- : typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
7100
- if (!sessionId) return { success: false, error: 'targetSessionId required' };
7101
- if (!args?.condition || typeof args.condition !== 'object') return { success: false, error: 'condition required' };
7102
- const target = this.deps.sessionRegistry.get(sessionId);
7103
- if (!target) return { success: false, error: 'Session not found', sessionId };
7104
- const adapterObj = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter as any;
7105
- const snap = adapterObj && typeof adapterObj.getDebugSnapshot === 'function' ? adapterObj.getDebugSnapshot() : null;
7106
- if (!snap?.screen) return { success: false, error: 'no live screen for session' };
7107
- // Reconstruct the sections map the spec would resolve. We pass
7108
- // the spec's sections so section-scoped regexes resolve the same
7109
- // way they do at runtime; fall back to the on-disk spec.
7110
- let sectionsDef: Record<string, unknown> | undefined;
7111
- try {
7112
- const fsm2 = await import('node:fs');
7113
- if (snap.specPath) {
7114
- const raw = JSON.parse(fsm2.readFileSync(snap.specPath, 'utf8'));
7115
- sectionsDef = raw?.sections;
7116
- }
7117
- } catch { /* fall back to whole-screen matching */ }
7118
- const { evaluateConditionPreview } = await import('../providers/spec/fsm-evaluator.js');
7119
- try {
7120
- const result = evaluateConditionPreview(
7121
- args.condition,
7122
- sectionsDef as any,
7123
- snap.screen,
7124
- snap.cursorPosition ?? undefined,
7125
- );
7126
- return { success: true, result, sections: snap.sections ?? null };
7127
- } catch (e) {
7128
- return { success: false, error: `eval failed: ${(e as Error).message}` };
7129
- }
7130
- }
7131
-
7132
- // ── Resolve a sections map against a live session's screen — the
7133
- // section editor's "test" button. Returns, for each section id,
7134
- // the line range + the text it captures, so the author can SEE
7135
- // whether a from_top/until/anchor definition carves the screen
7136
- // the way they intend. Accepts an in-progress sections map so it
7137
- // previews unsaved edits.
7138
- case 'resolve_section_preview': {
7139
- const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
7140
- : typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
7141
- if (!sessionId) return { success: false, error: 'targetSessionId required' };
7142
- if (!args?.sections || typeof args.sections !== 'object') return { success: false, error: 'sections map required' };
7143
- const target = this.deps.sessionRegistry.get(sessionId);
7144
- if (!target) return { success: false, error: 'Session not found', sessionId };
7145
- const adapterObj = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter as any;
7146
- const snap = adapterObj && typeof adapterObj.getDebugSnapshot === 'function' ? adapterObj.getDebugSnapshot() : null;
7147
- if (!snap?.screen) return { success: false, error: 'no live screen for session' };
7148
- const { resolveSections } = await import('../providers/spec/evaluator.js');
7149
- try {
7150
- const lines = String(snap.screen).split('\n').map((l: string) => l.endsWith('\r') ? l.slice(0, -1) : l);
7151
- const resolved = resolveSections(args.sections as any, lines);
7152
- return {
7153
- success: true,
7154
- screenLineCount: lines.length,
7155
- sections: resolved.map(s => ({ id: s.id, fromLine: s.fromLine, toLine: s.toLine, text: s.text })),
7156
- };
7157
- } catch (e) {
7158
- return { success: false, error: `resolve failed: ${(e as Error).message}` };
7159
- }
7160
- }
7161
-
7162
- // ── User-level coordinator-prompt files (~/.adhdev/coordinator-prompts/).
7163
- // These live on this daemon's filesystem and never sync to the
7164
- // cloud / other daemons — they're per-machine config. The
7165
- // Settings page in the dashboard reads/writes via these two
7166
- // commands instead of going through fs from the browser.
7167
- case 'list_coordinator_prompts': {
7168
- const fs = await import('node:fs');
7169
- const path = await import('node:path');
7170
- const os = await import('node:os');
7171
- const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
7172
- const entries: Record<string, { override: string; append: string }> = {};
7173
- try {
7174
- if (fs.existsSync(dir)) {
7175
- for (const name of fs.readdirSync(dir)) {
7176
- // Bucket files into <key>.{md|append.md}; ignore others
7177
- // so a stray README or .DS_Store doesn't show up.
7178
- const matchOverride = name.match(/^([a-zA-Z0-9_.-]+)\.md$/);
7179
- const matchAppend = name.match(/^([a-zA-Z0-9_.-]+)\.append\.md$/);
7180
- // append-pattern wins when both match (file is `.append.md`).
7181
- const m = matchAppend || matchOverride;
7182
- if (!m) continue;
7183
- const isAppend = !!matchAppend;
7184
- const key = m[1];
7185
- const full = path.join(dir, name);
7186
- let content = '';
7187
- try { content = fs.readFileSync(full, 'utf8'); } catch { /* skip */ }
7188
- if (!entries[key]) entries[key] = { override: '', append: '' };
7189
- if (isAppend) entries[key].append = content;
7190
- else entries[key].override = content;
7191
- }
7192
- }
7193
- } catch (error: any) {
7194
- return { success: false, error: error?.message || String(error) };
7195
- }
7196
- return { success: true, dir, entries };
7197
- }
7198
-
7199
- case 'write_coordinator_prompt': {
7200
- const fs = await import('node:fs');
7201
- const path = await import('node:path');
7202
- const os = await import('node:os');
7203
- const key = typeof args?.key === 'string' ? args.key.trim() : '';
7204
- const kind = args?.kind === 'append' ? 'append' : 'override';
7205
- const content = typeof args?.content === 'string' ? args.content : '';
7206
- // Whitelist key chars so a malicious caller can't write
7207
- // ../../etc/passwd. Same charset readUserPromptFile accepts.
7208
- if (!key || !/^[a-zA-Z0-9_.-]+$/.test(key)) {
7209
- return { success: false, error: 'key must match [a-zA-Z0-9_.-]+' };
7210
- }
7211
- const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
7212
- const filename = kind === 'append' ? `${key}.append.md` : `${key}.md`;
7213
- const full = path.join(dir, filename);
7214
- try {
7215
- fs.mkdirSync(dir, { recursive: true });
7216
- if (content.trim()) {
7217
- fs.writeFileSync(full, content, { encoding: 'utf8', mode: 0o600 });
7218
- } else if (fs.existsSync(full)) {
7219
- // Empty content = "reset to default" — delete the file
7220
- // so the daemon's readUserPromptFile path falls through.
7221
- fs.unlinkSync(full);
7222
- }
7223
- return { success: true, path: full, kind, key };
7224
- } catch (error: any) {
7225
- return { success: false, error: error?.message || String(error) };
7226
- }
7227
- }
7228
-
7229
- case 'mark_session_seen': {
7230
- const sessionId = args?.sessionId;
7231
- if (!sessionId || typeof sessionId !== 'string') {
7232
- return { success: false, error: 'sessionId is required' };
7233
- }
7234
- const currentState = loadState();
7235
- const prevSeenAt = currentState.sessionReads?.[sessionId] || 0;
7236
- const sessionEntries = buildSessionEntries(
7237
- this.deps.instanceManager.collectAllStates(),
7238
- this.deps.cdpManagers,
7239
- );
7240
- const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
7241
- const requestedCompletionMarker = typeof args?.completionMarker === 'string'
7242
- ? args.completionMarker.trim()
7243
- : '';
7244
- const completionMarker = requestedCompletionMarker || (targetSession ? getSessionCompletionMarker(targetSession) : '');
7245
- const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
7246
- ? args.providerSessionId.trim()
7247
- : '';
7248
- const providerSessionId = requestedProviderSessionId || targetSession?.providerSessionId;
7249
- const next = markSessionSeen(
7250
- currentState,
7251
- sessionId,
7252
- typeof args?.seenAt === 'number' ? args.seenAt : Date.now(),
7253
- completionMarker,
7254
- providerSessionId,
7255
- );
7256
- if (READ_DEBUG_ENABLED) {
7257
- LOG.info('RecentRead', `mark_session_seen sessionId=${sessionId} seenAt=${String(args?.seenAt || '')} prevSeenAt=${String(prevSeenAt)} nextSeenAt=${String(next.sessionReads?.[sessionId] || 0)} marker=${completionMarker || '-'}`);
7258
- }
7259
- saveState(next);
7260
- this.deps.onStatusChange?.();
7261
- return {
7262
- success: true,
7263
- sessionId,
7264
- seenAt: next.sessionReads?.[sessionId] || Date.now(),
7265
- completionMarker,
7266
- };
7267
- }
7268
-
7269
- case 'delete_notification': {
7270
- const sessionId = args?.sessionId;
7271
- const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
7272
- if (!sessionId || typeof sessionId !== 'string') {
7273
- return { success: false, error: 'sessionId is required' };
7274
- }
7275
- if (!notificationId) {
7276
- return { success: false, error: 'notificationId is required' };
7277
- }
7278
- const sessionEntries = buildSessionEntries(
7279
- this.deps.instanceManager.collectAllStates(),
7280
- this.deps.cdpManagers,
7281
- );
7282
- const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
7283
- const next = dismissSessionNotification(
7284
- loadState(),
7285
- sessionId,
7286
- notificationId,
7287
- targetSession?.providerSessionId,
7288
- );
7289
- saveState(next);
7290
- this.deps.onStatusChange?.();
7291
- return {
7292
- success: true,
7293
- sessionId,
7294
- notificationId,
7295
- };
7296
- }
7297
-
7298
- case 'mark_notification_unread': {
7299
- const sessionId = args?.sessionId;
7300
- const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
7301
- if (!sessionId || typeof sessionId !== 'string') {
7302
- return { success: false, error: 'sessionId is required' };
7303
- }
7304
- if (!notificationId) {
7305
- return { success: false, error: 'notificationId is required' };
7306
- }
7307
- const sessionEntries = buildSessionEntries(
7308
- this.deps.instanceManager.collectAllStates(),
7309
- this.deps.cdpManagers,
7310
- );
7311
- const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
7312
- const next = markSessionNotificationUnread(
7313
- loadState(),
7314
- sessionId,
7315
- notificationId,
7316
- targetSession?.providerSessionId,
7317
- );
7318
- saveState(next);
7319
- this.deps.onStatusChange?.();
7320
- return {
7321
- success: true,
7322
- sessionId,
7323
- notificationId,
7324
- };
7325
- }
7326
-
7327
- // ─── Daemon Self-Upgrade ───
7328
- case 'daemon_upgrade': {
7329
- LOG.info('Upgrade', 'Remote upgrade requested from dashboard');
7330
- try {
7331
- // Detect package name for upgrade
7332
- const isStandalone = this.deps.packageName === '@adhdev/daemon-standalone'
7333
- || process.argv[1]?.includes('daemon-standalone');
7334
- const pkgName = isStandalone ? '@adhdev/daemon-standalone' : 'adhdev';
7335
- const npmSurface = resolveCurrentGlobalInstallSurface({ packageName: pkgName });
7336
- const channel = resolveUpgradeChannel(args);
7337
- const npmTag = CHANNEL_NPM_TAG[channel];
7338
-
7339
- // Check channel-pinned dist-tag and resolve it to a concrete install version.
7340
- const latest = String(execNpmCommandSync(['view', `${pkgName}@${npmTag}`, 'version'], { encoding: 'utf-8', timeout: 10000 }, npmSurface)).trim();
7341
- LOG.info('Upgrade', `Latest ${pkgName}@${npmTag}: v${latest}`);
7342
- updateConfig({ updateChannel: channel, serverUrl: CHANNEL_SERVER_URL[channel] } as any);
7343
- let currentInstalled: string | null = null;
7344
- try {
7345
- const currentJson = String(execNpmCommandSync(['ls', '-g', pkgName, '--depth=0', '--json'], {
7346
- encoding: 'utf-8',
7347
- timeout: 10000,
7348
- stdio: ['pipe', 'pipe', 'pipe'],
7349
- }, npmSurface)).trim();
7350
- const parsed = JSON.parse(currentJson);
7351
- currentInstalled = parsed?.dependencies?.[pkgName]?.version || null;
7352
- } catch {
7353
- // ignore ls failures; upgrade can still proceed
7354
- }
7355
-
7356
- const runningVersion = typeof this.deps.statusVersion === 'string'
7357
- ? this.deps.statusVersion.trim().replace(/^v/, '')
7358
- : null;
7359
- if (currentInstalled === latest && runningVersion === latest) {
7360
- LOG.info('Upgrade', `Already on ${channel} channel version v${latest}; skipping install`);
7361
- return { success: true, upgraded: false, alreadyLatest: true, version: latest, channel, npmTag };
7362
- }
7363
- if (currentInstalled === latest && runningVersion && runningVersion !== latest) {
7364
- LOG.info('Upgrade', `Installed package is v${latest}, but running daemon is v${runningVersion}; scheduling restart`);
7365
- }
7366
-
7367
- spawnDetachedDaemonUpgradeHelper({
7368
- packageName: pkgName,
7369
- targetVersion: latest,
7370
- parentPid: process.pid,
7371
- restartArgv: process.argv.slice(1),
7372
- cwd: process.cwd(),
7373
- sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || 'adhdev',
7374
- });
7375
- LOG.info('Upgrade', `Scheduled detached ${channel} upgrade to v${latest}`);
7376
-
7377
- // Exit after the command response has been sent so the helper can replace the package cleanly.
7378
- setTimeout(() => {
7379
- LOG.info('Upgrade', 'Exiting daemon so detached upgrader can continue...');
7380
- process.exit(0);
7381
- }, 3000);
7382
-
7383
- return { success: true, upgraded: true, version: latest, restarting: true, channel, npmTag };
7384
- } catch (e: any) {
7385
- LOG.error('Upgrade', `Failed: ${e.message}`);
7386
- return { success: false, error: e.message };
7387
- }
7388
- }
7389
-
7390
- // ─── Machine Settings ───
7391
- case 'set_machine_nickname': {
7392
- const nickname = args?.nickname;
7393
- updateConfig({ machineNickname: nickname || null });
7394
- return { success: true };
7395
- }
7396
-
7397
6535
  // ─── Mesh CRUD (local meshes.json) ───
7398
6536
  case 'list_meshes': {
7399
6537
  try {
@@ -7724,57 +6862,6 @@ export class DaemonCommandRouter {
7724
6862
  }
7725
6863
  }
7726
6864
 
7727
- case 'get_mesh_ledger': {
7728
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7729
- if (!meshId) return { success: false, error: 'meshId required' };
7730
- try {
7731
- const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
7732
- const tail = typeof args?.tail === 'number' ? args.tail : 20;
7733
- const since = typeof args?.since === 'string' ? args.since : undefined;
7734
- const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
7735
- const entries = readLedgerEntries(meshId, { tail, since, kind });
7736
- const summary = getLedgerSummary(meshId);
7737
- return { success: true, entries, summary };
7738
- } catch (e: any) {
7739
- return { success: false, error: e.message };
7740
- }
7741
- }
7742
-
7743
- case 'get_mesh_ledger_slice': {
7744
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7745
- if (!meshId) return { success: false, error: 'meshId required' };
7746
- try {
7747
- const { readLedgerSlice } = await import('../mesh/mesh-ledger.js');
7748
- const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
7749
- const slice = readLedgerSlice(meshId, {
7750
- afterId: typeof args?.afterId === 'string' ? args.afterId : undefined,
7751
- since: typeof args?.since === 'string' ? args.since : undefined,
7752
- kind,
7753
- limit: typeof args?.limit === 'number' ? args.limit : undefined,
7754
- });
7755
- return { success: true, slice };
7756
- } catch (e: any) {
7757
- return { success: false, error: e.message };
7758
- }
7759
- }
7760
-
7761
- case 'import_mesh_ledger_slice': {
7762
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7763
- if (!meshId) return { success: false, error: 'meshId required' };
7764
- try {
7765
- const { appendRemoteLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
7766
- const entries = Array.isArray(args?.entries)
7767
- ? args.entries as any[]
7768
- : Array.isArray(args?.slice?.entries)
7769
- ? args.slice.entries as any[]
7770
- : [];
7771
- const result = appendRemoteLedgerEntries(meshId, entries as any);
7772
- return { success: true, result, summary: getLedgerSummary(meshId) };
7773
- } catch (e: any) {
7774
- return { success: false, error: e.message };
7775
- }
7776
- }
7777
-
7778
6865
  case 'get_mesh_queue': {
7779
6866
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7780
6867
  if (!meshId) return { success: false, error: 'meshId required' };
@@ -7977,83 +7064,6 @@ export class DaemonCommandRouter {
7977
7064
  }
7978
7065
  }
7979
7066
 
7980
- case 'get_mesh_refine_config_schema': {
7981
- return {
7982
- success: true,
7983
- schema: MESH_REFINE_CONFIG_SCHEMA,
7984
- locations: MESH_REFINE_CONFIG_LOCATIONS,
7985
- worktreeBootstrap: {
7986
- schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
7987
- locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
7988
- sourceOfTruth: 'repo worktree bootstrap config',
7989
- runBehavior: 'When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node.',
7990
- },
7991
- sourceOfTruth: 'repo mesh/refine config',
7992
- heuristicRole: 'suggestions_only_not_execution_path',
7993
- };
7994
- }
7995
-
7996
- case 'validate_mesh_refine_config': {
7997
- const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
7998
- const mesh = args?.inlineMesh || {};
7999
- const loaded = args?.config !== undefined
8000
- ? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
8001
- : loadMeshRefineConfig(mesh, workspace);
8002
- const validation = loaded.config
8003
- ? validateMeshRefineConfig(loaded.config, loaded.source)
8004
- : { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
8005
- return { success: validation.valid, ...loaded, ...validation };
8006
- }
8007
-
8008
- case 'suggest_mesh_refine_config': {
8009
- const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
8010
- const mesh = args?.inlineMesh || {};
8011
- return {
8012
- success: true,
8013
- ...suggestMeshRefineConfig(mesh, workspace),
8014
- note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
8015
- };
8016
- }
8017
-
8018
- case 'get_mesh_change_impact_config_schema': {
8019
- return {
8020
- success: true,
8021
- schema: CHANGE_IMPACT_CONFIG_SCHEMA,
8022
- locations: CHANGE_IMPACT_CONFIG_LOCATIONS,
8023
- sourceOfTruth: 'repo change-impact config',
8024
- heuristicRole: 'suggestions_only_not_execution_path',
8025
- note: 'Declarative config only — JSON/YAML are parsed but never executed. Defines which package/file changes require a daemon rebuild/restart vs. a web-only redeploy vs. nothing.',
8026
- };
8027
- }
8028
-
8029
- case 'validate_mesh_change_impact_config': {
8030
- const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
8031
- if (args?.config !== undefined) {
8032
- const validation = validateChangeImpactConfig(args.config, 'inline');
8033
- return { success: validation.valid, source: 'inline', sourceType: 'mesh_policy', ...validation };
8034
- }
8035
- const loaded = loadChangeImpactConfig(workspace);
8036
- if (loaded.sourceType === 'repo_file') {
8037
- const validation = validateChangeImpactConfig(loaded.config, loaded.source);
8038
- return { success: validation.valid, ...loaded, ...validation };
8039
- }
8040
- return {
8041
- success: false,
8042
- ...loaded,
8043
- valid: false,
8044
- errors: [loaded.error || 'repo change-impact config unavailable'],
8045
- };
8046
- }
8047
-
8048
- case 'suggest_mesh_change_impact_config': {
8049
- const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
8050
- return {
8051
- success: true,
8052
- ...suggestChangeImpactConfig(workspace),
8053
- note: 'Suggestions are heuristic scaffold only; the draft must be reviewed and saved into repo change-impact config before it takes effect. Nothing is executed.',
8054
- };
8055
- }
8056
-
8057
7067
  case 'mesh_init': {
8058
7068
  const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
8059
7069
  const mesh = args?.inlineMesh || {};
@@ -8143,70 +7153,6 @@ export class DaemonCommandRouter {
8143
7153
  return result as CommandRouterResult;
8144
7154
  }
8145
7155
 
8146
- case 'get_mesh_node_logs': {
8147
- // Coordinator-driven remote log fetch: read a (possibly remote)
8148
- // daemon's recent log tail over P2P instead of opening a session
8149
- // and grepping the file by hand. Mirrors fast_forward_mesh_node's
8150
- // forward pattern — resolve the node, forward to its owning daemon
8151
- // when remote, otherwise read locally. The reply tail is HARD
8152
- // byte-bounded and secret-redacted before it leaves the machine.
8153
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
8154
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
8155
- let nodeDaemonId: string | undefined;
8156
- if (meshId && nodeId) {
8157
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
8158
- const node = meshRecord?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
8159
- nodeDaemonId = typeof node?.daemonId === 'string' ? node.daemonId.trim() : undefined;
8160
- }
8161
- // _meshDirectDispatch prevents re-forwarding (and P2P self-dial)
8162
- // once the call lands on the owning daemon — that daemon then reads
8163
- // its own logs even if the stored daemonId uses a legacy form.
8164
- const selfDaemonId = this.deps.statusInstanceId;
8165
- // daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core is
8166
- // local — read locally instead of forwarding. Equivalent → local.
8167
- const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
8168
- if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
8169
- const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'get_mesh_node_logs', {
8170
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
8171
- _meshDirectDispatch: true,
8172
- });
8173
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
8174
- }
8175
-
8176
- // Local read on the owning daemon.
8177
- const rawTailBytes = Number(args?.tailBytes);
8178
- const tail = readDaemonLogTail({
8179
- date: typeof args?.date === 'string' ? args.date : undefined,
8180
- tailBytes: Number.isFinite(rawTailBytes) ? Math.min(rawTailBytes, MAX_TAIL_BYTES) : undefined,
8181
- grep: typeof args?.grep === 'string' ? args.grep : undefined,
8182
- sinceMs: Number.isFinite(Number(args?.sinceMs)) ? Number(args?.sinceMs) : undefined,
8183
- });
8184
- if (!tail.success) {
8185
- return {
8186
- success: false,
8187
- error: tail.error || 'failed to read daemon log tail',
8188
- nodeId,
8189
- logPath: tail.logPath,
8190
- platform: tail.platform,
8191
- } as CommandRouterResult;
8192
- }
8193
- // SECURITY: redact secrets from every line before returning over P2P.
8194
- const redactedLines = redactLogLines(tail.lines);
8195
- return {
8196
- success: true,
8197
- nodeId,
8198
- daemonId: selfDaemonId,
8199
- logPath: tail.logPath,
8200
- platform: tail.platform,
8201
- lines: redactedLines,
8202
- lineCount: redactedLines.length,
8203
- truncated: tail.truncated,
8204
- filtered: tail.filtered,
8205
- bytesReturned: tail.bytesReturned,
8206
- ...(tail.grep ? { grep: tail.grep } : {}),
8207
- } as CommandRouterResult;
8208
- }
8209
-
8210
7156
  case 'refine_mesh_node': {
8211
7157
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
8212
7158
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
@@ -9592,7 +8538,7 @@ export class DaemonCommandRouter {
9592
8538
  const isSelfNode = Boolean(
9593
8539
  nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
9594
8540
  ) || Boolean(
9595
- daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
8541
+ daemonId && (daemonIdsEquivalent(daemonId, localMachineId) || daemonIdsEquivalent(daemonId, this.deps.statusInstanceId)),
9596
8542
  ) || Boolean(meshRecord?.inline && nodeIndex === 0)
9597
8543
  || sparseConfiguredCoordinatorNode;
9598
8544
  const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {