@adhdev/daemon-core 0.9.82-rc.361 → 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.
@@ -10,8 +10,28 @@
10
10
  */
11
11
  import type { CommandRouterDeps, CommandRouterResult } from '../router.js';
12
12
 
13
+ /** Mesh record resolved from the router's inline-mesh cache + local config. */
14
+ export type ResolvedMeshForCommand = {
15
+ mesh: any;
16
+ inline: boolean;
17
+ source: 'inline_cache' | 'inline_bootstrap' | 'local_config';
18
+ } | null;
19
+
13
20
  export interface LowFamilyContext {
14
21
  deps: CommandRouterDeps;
22
+ /**
23
+ * Bound `DaemonCommandRouter.getMeshForCommand`. A handful of LOW handlers
24
+ * (mesh-node-logs) must resolve a mesh node's owning daemonId from the
25
+ * router's inline-mesh cache, which is router instance state not present in
26
+ * `deps`. The router injects it at dispatch; handlers that don't need it
27
+ * ignore it. Optional so unit tests can omit it (and assert the guarded
28
+ * fallback) without constructing a full router.
29
+ */
30
+ getMeshForCommand?: (
31
+ meshId: string,
32
+ inlineMesh?: unknown,
33
+ options?: { preferInline?: boolean },
34
+ ) => Promise<ResolvedMeshForCommand>;
15
35
  }
16
36
 
17
37
  export type LowFamilyHandler = (ctx: LowFamilyContext, args: any) => Promise<CommandRouterResult>;
@@ -19,10 +19,10 @@ import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
19
19
  import type { ProviderLoader } from '../providers/provider-loader.js';
20
20
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
21
21
  import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
22
- import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
22
+ import { loadConfig, saveConfig } from '../config/config.js';
23
23
  import { loadState, saveState } from '../config/state-store.js';
24
24
  import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
25
- import { appendRecentActivity, getRecentActivity, markSessionSeen, dismissSessionNotification, markSessionNotificationUnread } from '../config/recent-activity.js';
25
+ import { appendRecentActivity, getRecentActivity } from '../config/recent-activity.js';
26
26
  import { getSavedProviderSessions } from '../config/saved-sessions.js';
27
27
  import { listProviderHistorySessions } from '../config/chat-history.js';
28
28
  import { detectIDEs } from '../detection/ide-detector.js';
@@ -48,14 +48,10 @@ import { LOG } from '../logging/logger.js';
48
48
  import { logCommand } from '../logging/command-log.js';
49
49
  import type { CommandLogEntry } from '../logging/command-log.js';
50
50
  import * as yaml from 'js-yaml';
51
- import { getRecentLogs, LOG_PATH } from '../logging/logger.js';
52
- import { readDaemonLogTail, MAX_TAIL_BYTES } from '../logging/log-tail-reader.js';
53
- import { redactLogLines } from '../logging/log-redactor.js';
54
- import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
51
+ import { createInteractionId, recordDebugTrace } from '../logging/debug-trace.js';
55
52
  import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
56
53
  import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
57
- import { buildSessionEntries } from '../status/builders.js';
58
- import { registerMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
54
+ import { registerMeshCoordinator } from '../mesh/coordinator-registry.js';
59
55
  import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, type PendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
60
56
  import { getRecentUnroutableDeliveries } from '../mesh/mesh-routing.js';
61
57
  import { buildMeshWorkerRelayStamp } from '../mesh/mesh-events-utils.js';
@@ -82,10 +78,6 @@ import {
82
78
  type WorktreeBootstrapState,
83
79
  } from '../mesh/worktree-bootstrap-config.js';
84
80
  import { runMeshInit } from '../mesh/mesh-init.js';
85
- import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
86
- import { getDaemonBuildInfo } from '../build-info.js';
87
- import { getSessionCompletionMarker } from '../status/snapshot.js';
88
- import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
89
81
  import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
90
82
  import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
91
83
  import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
@@ -97,29 +89,6 @@ import { normalizeInteractivePromptResponse } from '../providers/types/interacti
97
89
  import { workingDirBasename } from '../providers/working-dir.js';
98
90
  import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
99
91
 
100
- type ReleaseChannel = 'stable' | 'preview';
101
- const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
102
- const CHANNEL_SERVER_URL: Record<ReleaseChannel, string> = {
103
- stable: 'https://api.adhf.dev',
104
- preview: 'https://api-preview.adhf.dev',
105
- };
106
-
107
- function normalizeReleaseChannel(value: unknown): ReleaseChannel | null {
108
- if (typeof value !== 'string') return null;
109
- const normalized = value.trim().toLowerCase();
110
- if (normalized === 'stable' || normalized === 'latest') return 'stable';
111
- if (normalized === 'preview' || normalized === 'next') return 'preview';
112
- return null;
113
- }
114
-
115
- function resolveUpgradeChannel(args: any): ReleaseChannel {
116
- return normalizeReleaseChannel(args?.channel)
117
- || normalizeReleaseChannel(args?.updatePolicy?.channel)
118
- || normalizeReleaseChannel(args?.npmTag)
119
- || normalizeReleaseChannel(loadConfig().updateChannel)
120
- || 'stable';
121
- }
122
-
123
92
  function readProviderPriorityFromPolicy(policy: unknown): string[] {
124
93
  const record = policy && typeof policy === 'object' && !Array.isArray(policy)
125
94
  ? policy as Record<string, unknown>
@@ -3472,7 +3441,6 @@ const MESH_FORWARDABLE_SESSION_COMMANDS = new Set([
3472
3441
  // delivers it to the real worker instead. (findAdapter is also fail-closed as the backstop.)
3473
3442
  'agent_command',
3474
3443
  ]);
3475
- const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
3476
3444
 
3477
3445
  function normalizeCommandSource(source: string): CommandLogEntry['source'] {
3478
3446
  switch (source) {
@@ -6209,7 +6177,10 @@ export class DaemonCommandRouter {
6209
6177
  // case used to; a miss falls through to the switch unchanged.
6210
6178
  const lowFamilyHandler = lowFamilyRegistry.get(cmd);
6211
6179
  if (lowFamilyHandler) {
6212
- return await lowFamilyHandler({ deps: this.deps }, args);
6180
+ return await lowFamilyHandler({
6181
+ deps: this.deps,
6182
+ getMeshForCommand: this.getMeshForCommand.bind(this),
6183
+ }, args);
6213
6184
  }
6214
6185
 
6215
6186
  switch (cmd) {
@@ -6346,49 +6317,6 @@ export class DaemonCommandRouter {
6346
6317
  }
6347
6318
 
6348
6319
  // ─── Logs ───
6349
- case 'get_logs': {
6350
- const count = parseInt(args?.count) || parseInt(args?.lines) || 100;
6351
- const minLevel = args?.minLevel || 'info';
6352
- const sinceTs = args?.since || 0;
6353
-
6354
- try {
6355
- // Priority 1: ring buffer (fast and structured)
6356
- let logs = getRecentLogs(count, minLevel);
6357
- if (sinceTs > 0) {
6358
- logs = logs.filter((l: any) => l.ts > sinceTs);
6359
- }
6360
- if (logs.length > 0) {
6361
- return { success: true, logs, totalBuffered: logs.length };
6362
- }
6363
- // Incremental polling must not fall back to unfiltered file text: the file
6364
- // format is not timestamp-filterable, and returning its tail makes the UI
6365
- // replace structured logs with old raw fallback lines when nothing new exists.
6366
- if (sinceTs > 0) {
6367
- return { success: true, logs: [], totalBuffered: 0 };
6368
- }
6369
- // Priority 2: file fallback
6370
- if (fs.existsSync(LOG_PATH)) {
6371
- const content = fs.readFileSync(LOG_PATH, 'utf-8');
6372
- const allLines = content.split('\n');
6373
- const recent = allLines.slice(-count).join('\n');
6374
- return { success: true, logs: recent, totalLines: allLines.length };
6375
- }
6376
- return { success: true, logs: [], totalBuffered: 0 };
6377
- } catch (e: any) {
6378
- return { success: false, error: e.message };
6379
- }
6380
- }
6381
-
6382
- case 'get_debug_trace': {
6383
- const count = parseInt(args?.count) || parseInt(args?.limit) || 100;
6384
- const sinceTs = Number(args?.since) || 0;
6385
- const interactionId = typeof args?.interactionId === 'string' ? args.interactionId : undefined;
6386
- const category = typeof args?.category === 'string' ? args.category : undefined;
6387
- const trace = getRecentDebugTrace({ interactionId, category, limit: count })
6388
- .filter((entry) => !sinceTs || entry.ts > sinceTs);
6389
- return { success: true, trace, count: trace.length };
6390
- }
6391
-
6392
6320
  case 'list_saved_sessions': {
6393
6321
  const providerType = typeof args?.providerType === 'string'
6394
6322
  ? args.providerType.trim()
@@ -6604,333 +6532,6 @@ export class DaemonCommandRouter {
6604
6532
  return { success: true, detectedInfo: results };
6605
6533
  }
6606
6534
 
6607
- // ─── Set User Name ───
6608
- case 'set_user_name': {
6609
- const name = args?.userName;
6610
- if (!name || typeof name !== 'string') throw new Error('userName required');
6611
- updateConfig({ userName: name });
6612
- return { success: true, userName: name };
6613
- }
6614
-
6615
- case 'get_status_metadata': {
6616
- const snapshot = buildStatusSnapshot({
6617
- allStates: this.deps.instanceManager.collectAllStates(),
6618
- cdpManagers: this.deps.cdpManagers,
6619
- providerLoader: this.deps.providerLoader,
6620
- detectedIdes: this.deps.detectedIdes.value,
6621
- instanceId: this.deps.statusInstanceId || loadConfig().machineId || 'daemon',
6622
- version: this.deps.statusVersion || 'unknown',
6623
- profile: 'metadata',
6624
- });
6625
- // Surface the daemon's build stamp so coordinators (mesh_status)
6626
- // can detect a running daemon that predates a just-merged fix and
6627
- // is awaiting deploy/restart. Sibling of `status` to avoid
6628
- // perturbing the dashboard status snapshot shape.
6629
- return { success: true, status: snapshot, daemonBuild: getDaemonBuildInfo() };
6630
- }
6631
-
6632
- case 'get_machine_runtime_stats': {
6633
- return {
6634
- success: true,
6635
- machine: buildMachineInfo('full'),
6636
- timestamp: Date.now(),
6637
- };
6638
- }
6639
-
6640
- // Session-info popup data. Aggregates whatever the daemon knows
6641
- // about a single live session into one envelope so the dashboard
6642
- // doesn't need to stitch together status + coordinator registry +
6643
- // session registry on the client. Includes the actual system
6644
- // prompt that was injected at launch when the session is a mesh
6645
- // coordinator — that's the "what prompt did the agent see?"
6646
- // question the info-icon dialog is meant to answer.
6647
- case 'get_session_info': {
6648
- const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
6649
- : typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
6650
- if (!sessionId) return { success: false, error: 'targetSessionId required' };
6651
- // Fetch both lookups up front. We used to bail with "Session not
6652
- // found" when sessionRegistry forgot the SID (auto-cleanup,
6653
- // daemon restart with the session not yet restored, etc), which
6654
- // hid the coordinator-side metadata even though the
6655
- // coordinator-registry still has it. Now we return whichever
6656
- // side we have. The dashboard renders "no coordinator-specific
6657
- // prompt" only when *neither* side knows the session.
6658
- const target = this.deps.sessionRegistry.get(sessionId);
6659
- const coord = getCoordinatorForSession(sessionId);
6660
- if (!target && !coord) return { success: false, error: 'Session not found', sessionId };
6661
- const adapter = target
6662
- ? this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter
6663
- : undefined;
6664
- const runtimeMeta = (adapter && typeof (adapter as any).getRuntimeMetadata === 'function')
6665
- ? (adapter as any).getRuntimeMetadata()
6666
- : undefined;
6667
- // Launch metadata (args / cwd / extra-env keys / providerSessionId) is
6668
- // derived from the live adapter's spawn plan; only available while the
6669
- // adapter is alive (resumed-from-history sessions report nothing here).
6670
- const launchInfo = (adapter && typeof (adapter as any).getLaunchInfo === 'function')
6671
- ? (adapter as any).getLaunchInfo()
6672
- : undefined;
6673
- const providerType = target?.providerType || coord?.cliType || '';
6674
- const providerMetaForSession = providerType
6675
- ? this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType)
6676
- : undefined;
6677
- return {
6678
- success: true,
6679
- session: {
6680
- sessionId,
6681
- providerType,
6682
- providerName: providerMetaForSession?.name,
6683
- transport: target?.transport,
6684
- workspace: (target as any)?.workspace || coord?.workspace,
6685
- spawnedAtMs: (target as any)?.spawnedAtMs || coord?.startedAt,
6686
- // providerSessionId now comes from the live adapter's launch info
6687
- // (the registry target never carried it — it was always undefined).
6688
- providerSessionId: launchInfo?.providerSessionId || (target as any)?.providerSessionId,
6689
- runtimeMetadata: runtimeMeta,
6690
- launch: launchInfo,
6691
- },
6692
- coordinator: coord ? {
6693
- meshId: coord.meshId,
6694
- startedAt: coord.startedAt,
6695
- cliType: coord.cliType,
6696
- systemPrompt: coord.systemPrompt,
6697
- extraSystemPrompt: coord.extraSystemPrompt,
6698
- injection: coord.injection,
6699
- mcpConfigPath: coord.mcpConfigPath,
6700
- } : null,
6701
- };
6702
- }
6703
-
6704
- case 'list_coordinator_prompts': {
6705
- const fs = await import('node:fs');
6706
- const path = await import('node:path');
6707
- const os = await import('node:os');
6708
- const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
6709
- const entries: Record<string, { override: string; append: string }> = {};
6710
- try {
6711
- if (fs.existsSync(dir)) {
6712
- for (const name of fs.readdirSync(dir)) {
6713
- // Bucket files into <key>.{md|append.md}; ignore others
6714
- // so a stray README or .DS_Store doesn't show up.
6715
- const matchOverride = name.match(/^([a-zA-Z0-9_.-]+)\.md$/);
6716
- const matchAppend = name.match(/^([a-zA-Z0-9_.-]+)\.append\.md$/);
6717
- // append-pattern wins when both match (file is `.append.md`).
6718
- const m = matchAppend || matchOverride;
6719
- if (!m) continue;
6720
- const isAppend = !!matchAppend;
6721
- const key = m[1];
6722
- const full = path.join(dir, name);
6723
- let content = '';
6724
- try { content = fs.readFileSync(full, 'utf8'); } catch { /* skip */ }
6725
- if (!entries[key]) entries[key] = { override: '', append: '' };
6726
- if (isAppend) entries[key].append = content;
6727
- else entries[key].override = content;
6728
- }
6729
- }
6730
- } catch (error: any) {
6731
- return { success: false, error: error?.message || String(error) };
6732
- }
6733
- return { success: true, dir, entries };
6734
- }
6735
-
6736
- case 'write_coordinator_prompt': {
6737
- const fs = await import('node:fs');
6738
- const path = await import('node:path');
6739
- const os = await import('node:os');
6740
- const key = typeof args?.key === 'string' ? args.key.trim() : '';
6741
- const kind = args?.kind === 'append' ? 'append' : 'override';
6742
- const content = typeof args?.content === 'string' ? args.content : '';
6743
- // Whitelist key chars so a malicious caller can't write
6744
- // ../../etc/passwd. Same charset readUserPromptFile accepts.
6745
- if (!key || !/^[a-zA-Z0-9_.-]+$/.test(key)) {
6746
- return { success: false, error: 'key must match [a-zA-Z0-9_.-]+' };
6747
- }
6748
- const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
6749
- const filename = kind === 'append' ? `${key}.append.md` : `${key}.md`;
6750
- const full = path.join(dir, filename);
6751
- try {
6752
- fs.mkdirSync(dir, { recursive: true });
6753
- if (content.trim()) {
6754
- fs.writeFileSync(full, content, { encoding: 'utf8', mode: 0o600 });
6755
- } else if (fs.existsSync(full)) {
6756
- // Empty content = "reset to default" — delete the file
6757
- // so the daemon's readUserPromptFile path falls through.
6758
- fs.unlinkSync(full);
6759
- }
6760
- return { success: true, path: full, kind, key };
6761
- } catch (error: any) {
6762
- return { success: false, error: error?.message || String(error) };
6763
- }
6764
- }
6765
-
6766
- case 'mark_session_seen': {
6767
- const sessionId = args?.sessionId;
6768
- if (!sessionId || typeof sessionId !== 'string') {
6769
- return { success: false, error: 'sessionId is required' };
6770
- }
6771
- const currentState = loadState();
6772
- const prevSeenAt = currentState.sessionReads?.[sessionId] || 0;
6773
- const sessionEntries = buildSessionEntries(
6774
- this.deps.instanceManager.collectAllStates(),
6775
- this.deps.cdpManagers,
6776
- );
6777
- const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
6778
- const requestedCompletionMarker = typeof args?.completionMarker === 'string'
6779
- ? args.completionMarker.trim()
6780
- : '';
6781
- const completionMarker = requestedCompletionMarker || (targetSession ? getSessionCompletionMarker(targetSession) : '');
6782
- const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
6783
- ? args.providerSessionId.trim()
6784
- : '';
6785
- const providerSessionId = requestedProviderSessionId || targetSession?.providerSessionId;
6786
- const next = markSessionSeen(
6787
- currentState,
6788
- sessionId,
6789
- typeof args?.seenAt === 'number' ? args.seenAt : Date.now(),
6790
- completionMarker,
6791
- providerSessionId,
6792
- );
6793
- if (READ_DEBUG_ENABLED) {
6794
- LOG.info('RecentRead', `mark_session_seen sessionId=${sessionId} seenAt=${String(args?.seenAt || '')} prevSeenAt=${String(prevSeenAt)} nextSeenAt=${String(next.sessionReads?.[sessionId] || 0)} marker=${completionMarker || '-'}`);
6795
- }
6796
- saveState(next);
6797
- this.deps.onStatusChange?.();
6798
- return {
6799
- success: true,
6800
- sessionId,
6801
- seenAt: next.sessionReads?.[sessionId] || Date.now(),
6802
- completionMarker,
6803
- };
6804
- }
6805
-
6806
- case 'delete_notification': {
6807
- const sessionId = args?.sessionId;
6808
- const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
6809
- if (!sessionId || typeof sessionId !== 'string') {
6810
- return { success: false, error: 'sessionId is required' };
6811
- }
6812
- if (!notificationId) {
6813
- return { success: false, error: 'notificationId is required' };
6814
- }
6815
- const sessionEntries = buildSessionEntries(
6816
- this.deps.instanceManager.collectAllStates(),
6817
- this.deps.cdpManagers,
6818
- );
6819
- const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
6820
- const next = dismissSessionNotification(
6821
- loadState(),
6822
- sessionId,
6823
- notificationId,
6824
- targetSession?.providerSessionId,
6825
- );
6826
- saveState(next);
6827
- this.deps.onStatusChange?.();
6828
- return {
6829
- success: true,
6830
- sessionId,
6831
- notificationId,
6832
- };
6833
- }
6834
-
6835
- case 'mark_notification_unread': {
6836
- const sessionId = args?.sessionId;
6837
- const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
6838
- if (!sessionId || typeof sessionId !== 'string') {
6839
- return { success: false, error: 'sessionId is required' };
6840
- }
6841
- if (!notificationId) {
6842
- return { success: false, error: 'notificationId is required' };
6843
- }
6844
- const sessionEntries = buildSessionEntries(
6845
- this.deps.instanceManager.collectAllStates(),
6846
- this.deps.cdpManagers,
6847
- );
6848
- const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
6849
- const next = markSessionNotificationUnread(
6850
- loadState(),
6851
- sessionId,
6852
- notificationId,
6853
- targetSession?.providerSessionId,
6854
- );
6855
- saveState(next);
6856
- this.deps.onStatusChange?.();
6857
- return {
6858
- success: true,
6859
- sessionId,
6860
- notificationId,
6861
- };
6862
- }
6863
-
6864
- // ─── Daemon Self-Upgrade ───
6865
- case 'daemon_upgrade': {
6866
- LOG.info('Upgrade', 'Remote upgrade requested from dashboard');
6867
- try {
6868
- // Detect package name for upgrade
6869
- const isStandalone = this.deps.packageName === '@adhdev/daemon-standalone'
6870
- || process.argv[1]?.includes('daemon-standalone');
6871
- const pkgName = isStandalone ? '@adhdev/daemon-standalone' : 'adhdev';
6872
- const npmSurface = resolveCurrentGlobalInstallSurface({ packageName: pkgName });
6873
- const channel = resolveUpgradeChannel(args);
6874
- const npmTag = CHANNEL_NPM_TAG[channel];
6875
-
6876
- // Check channel-pinned dist-tag and resolve it to a concrete install version.
6877
- const latest = String(execNpmCommandSync(['view', `${pkgName}@${npmTag}`, 'version'], { encoding: 'utf-8', timeout: 10000 }, npmSurface)).trim();
6878
- LOG.info('Upgrade', `Latest ${pkgName}@${npmTag}: v${latest}`);
6879
- updateConfig({ updateChannel: channel, serverUrl: CHANNEL_SERVER_URL[channel] } as any);
6880
- let currentInstalled: string | null = null;
6881
- try {
6882
- const currentJson = String(execNpmCommandSync(['ls', '-g', pkgName, '--depth=0', '--json'], {
6883
- encoding: 'utf-8',
6884
- timeout: 10000,
6885
- stdio: ['pipe', 'pipe', 'pipe'],
6886
- }, npmSurface)).trim();
6887
- const parsed = JSON.parse(currentJson);
6888
- currentInstalled = parsed?.dependencies?.[pkgName]?.version || null;
6889
- } catch {
6890
- // ignore ls failures; upgrade can still proceed
6891
- }
6892
-
6893
- const runningVersion = typeof this.deps.statusVersion === 'string'
6894
- ? this.deps.statusVersion.trim().replace(/^v/, '')
6895
- : null;
6896
- if (currentInstalled === latest && runningVersion === latest) {
6897
- LOG.info('Upgrade', `Already on ${channel} channel version v${latest}; skipping install`);
6898
- return { success: true, upgraded: false, alreadyLatest: true, version: latest, channel, npmTag };
6899
- }
6900
- if (currentInstalled === latest && runningVersion && runningVersion !== latest) {
6901
- LOG.info('Upgrade', `Installed package is v${latest}, but running daemon is v${runningVersion}; scheduling restart`);
6902
- }
6903
-
6904
- spawnDetachedDaemonUpgradeHelper({
6905
- packageName: pkgName,
6906
- targetVersion: latest,
6907
- parentPid: process.pid,
6908
- restartArgv: process.argv.slice(1),
6909
- cwd: process.cwd(),
6910
- sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || 'adhdev',
6911
- });
6912
- LOG.info('Upgrade', `Scheduled detached ${channel} upgrade to v${latest}`);
6913
-
6914
- // Exit after the command response has been sent so the helper can replace the package cleanly.
6915
- setTimeout(() => {
6916
- LOG.info('Upgrade', 'Exiting daemon so detached upgrader can continue...');
6917
- process.exit(0);
6918
- }, 3000);
6919
-
6920
- return { success: true, upgraded: true, version: latest, restarting: true, channel, npmTag };
6921
- } catch (e: any) {
6922
- LOG.error('Upgrade', `Failed: ${e.message}`);
6923
- return { success: false, error: e.message };
6924
- }
6925
- }
6926
-
6927
- // ─── Machine Settings ───
6928
- case 'set_machine_nickname': {
6929
- const nickname = args?.nickname;
6930
- updateConfig({ machineNickname: nickname || null });
6931
- return { success: true };
6932
- }
6933
-
6934
6535
  // ─── Mesh CRUD (local meshes.json) ───
6935
6536
  case 'list_meshes': {
6936
6537
  try {
@@ -7261,57 +6862,6 @@ export class DaemonCommandRouter {
7261
6862
  }
7262
6863
  }
7263
6864
 
7264
- case 'get_mesh_ledger': {
7265
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7266
- if (!meshId) return { success: false, error: 'meshId required' };
7267
- try {
7268
- const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
7269
- const tail = typeof args?.tail === 'number' ? args.tail : 20;
7270
- const since = typeof args?.since === 'string' ? args.since : undefined;
7271
- const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
7272
- const entries = readLedgerEntries(meshId, { tail, since, kind });
7273
- const summary = getLedgerSummary(meshId);
7274
- return { success: true, entries, summary };
7275
- } catch (e: any) {
7276
- return { success: false, error: e.message };
7277
- }
7278
- }
7279
-
7280
- case 'get_mesh_ledger_slice': {
7281
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7282
- if (!meshId) return { success: false, error: 'meshId required' };
7283
- try {
7284
- const { readLedgerSlice } = await import('../mesh/mesh-ledger.js');
7285
- const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
7286
- const slice = readLedgerSlice(meshId, {
7287
- afterId: typeof args?.afterId === 'string' ? args.afterId : undefined,
7288
- since: typeof args?.since === 'string' ? args.since : undefined,
7289
- kind,
7290
- limit: typeof args?.limit === 'number' ? args.limit : undefined,
7291
- });
7292
- return { success: true, slice };
7293
- } catch (e: any) {
7294
- return { success: false, error: e.message };
7295
- }
7296
- }
7297
-
7298
- case 'import_mesh_ledger_slice': {
7299
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7300
- if (!meshId) return { success: false, error: 'meshId required' };
7301
- try {
7302
- const { appendRemoteLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
7303
- const entries = Array.isArray(args?.entries)
7304
- ? args.entries as any[]
7305
- : Array.isArray(args?.slice?.entries)
7306
- ? args.slice.entries as any[]
7307
- : [];
7308
- const result = appendRemoteLedgerEntries(meshId, entries as any);
7309
- return { success: true, result, summary: getLedgerSummary(meshId) };
7310
- } catch (e: any) {
7311
- return { success: false, error: e.message };
7312
- }
7313
- }
7314
-
7315
6865
  case 'get_mesh_queue': {
7316
6866
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7317
6867
  if (!meshId) return { success: false, error: 'meshId required' };
@@ -7603,70 +7153,6 @@ export class DaemonCommandRouter {
7603
7153
  return result as CommandRouterResult;
7604
7154
  }
7605
7155
 
7606
- case 'get_mesh_node_logs': {
7607
- // Coordinator-driven remote log fetch: read a (possibly remote)
7608
- // daemon's recent log tail over P2P instead of opening a session
7609
- // and grepping the file by hand. Mirrors fast_forward_mesh_node's
7610
- // forward pattern — resolve the node, forward to its owning daemon
7611
- // when remote, otherwise read locally. The reply tail is HARD
7612
- // byte-bounded and secret-redacted before it leaves the machine.
7613
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7614
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7615
- let nodeDaemonId: string | undefined;
7616
- if (meshId && nodeId) {
7617
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7618
- const node = meshRecord?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7619
- nodeDaemonId = typeof node?.daemonId === 'string' ? node.daemonId.trim() : undefined;
7620
- }
7621
- // _meshDirectDispatch prevents re-forwarding (and P2P self-dial)
7622
- // once the call lands on the owning daemon — that daemon then reads
7623
- // its own logs even if the stored daemonId uses a legacy form.
7624
- const selfDaemonId = this.deps.statusInstanceId;
7625
- // daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core is
7626
- // local — read locally instead of forwarding. Equivalent → local.
7627
- const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
7628
- if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
7629
- const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'get_mesh_node_logs', {
7630
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7631
- _meshDirectDispatch: true,
7632
- });
7633
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7634
- }
7635
-
7636
- // Local read on the owning daemon.
7637
- const rawTailBytes = Number(args?.tailBytes);
7638
- const tail = readDaemonLogTail({
7639
- date: typeof args?.date === 'string' ? args.date : undefined,
7640
- tailBytes: Number.isFinite(rawTailBytes) ? Math.min(rawTailBytes, MAX_TAIL_BYTES) : undefined,
7641
- grep: typeof args?.grep === 'string' ? args.grep : undefined,
7642
- sinceMs: Number.isFinite(Number(args?.sinceMs)) ? Number(args?.sinceMs) : undefined,
7643
- });
7644
- if (!tail.success) {
7645
- return {
7646
- success: false,
7647
- error: tail.error || 'failed to read daemon log tail',
7648
- nodeId,
7649
- logPath: tail.logPath,
7650
- platform: tail.platform,
7651
- } as CommandRouterResult;
7652
- }
7653
- // SECURITY: redact secrets from every line before returning over P2P.
7654
- const redactedLines = redactLogLines(tail.lines);
7655
- return {
7656
- success: true,
7657
- nodeId,
7658
- daemonId: selfDaemonId,
7659
- logPath: tail.logPath,
7660
- platform: tail.platform,
7661
- lines: redactedLines,
7662
- lineCount: redactedLines.length,
7663
- truncated: tail.truncated,
7664
- filtered: tail.filtered,
7665
- bytesReturned: tail.bytesReturned,
7666
- ...(tail.grep ? { grep: tail.grep } : {}),
7667
- } as CommandRouterResult;
7668
- }
7669
-
7670
7156
  case 'refine_mesh_node': {
7671
7157
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7672
7158
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';