@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.
- package/dist/commands/cli-manager.d.ts +13 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/router.d.ts +0 -1
- package/dist/index.js +5314 -5204
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4833 -4723
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-utils.d.ts +0 -2
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +69 -4
- package/src/commands/low-family/coordinator-prompt.ts +72 -0
- package/src/commands/low-family/daemon-lifecycle.ts +107 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +81 -0
- package/src/commands/low-family/notification.ts +116 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +274 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +39 -0
- package/src/commands/router.ts +22 -1076
- package/src/mesh/contracts.ts +12 -3
- package/src/mesh/mesh-events-coordinator.ts +1 -1
- package/src/mesh/mesh-events-utils.ts +0 -20
- package/src/providers/cli-provider-instance.ts +16 -3
package/src/commands/router.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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>, {
|