@adhdev/daemon-core 0.9.82-rc.361 → 0.9.82-rc.363
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/cli-adapters/cli-state-engine.d.ts +1 -1
- package/dist/cli-adapters/provider-cli-shared.d.ts +10 -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/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/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +17 -0
- package/dist/index.js +2290 -2177
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2296 -2183
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-utils.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/package.json +2 -2
- package/src/cli-adapters/cli-state-engine.ts +7 -1
- package/src/cli-adapters/provider-cli-adapter.ts +1 -0
- package/src/cli-adapters/provider-cli-shared.ts +10 -0
- 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 +14 -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/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +20 -0
- package/src/commands/router.ts +8 -522
- package/src/mesh/mesh-events-coordinator.ts +37 -0
- package/src/mesh/mesh-events-utils.ts +28 -2
- package/src/providers/cli-provider-instance.ts +46 -0
package/src/commands/router.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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 {
|
|
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({
|
|
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() : '';
|
|
@@ -457,6 +457,17 @@ function deliverTaskToSession(dispatchThunk: () => Promise<unknown>, ctx: Delive
|
|
|
457
457
|
});
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// WTCLAIM: normalize a workspace path for base-vs-worktree comparison. Mirrors
|
|
461
|
+
// cli-manager.ts normalizeDirForCompare (fix-B) — folds separator style, trailing
|
|
462
|
+
// slashes, and case (Windows paths are case-insensitive) so a base node and a worktree
|
|
463
|
+
// clone, whose only structural difference is their distinct workspace roots, are still
|
|
464
|
+
// told apart. Kept local (the cli-manager copy is module-private) so the comparison rule
|
|
465
|
+
// stays identical to the one fix-B already uses on the worker side.
|
|
466
|
+
function normalizeMeshWorkspaceForCompare(dir?: string): string {
|
|
467
|
+
if (typeof dir !== 'string') return '';
|
|
468
|
+
return dir.trim().replace(/[\\/]+/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
469
|
+
}
|
|
470
|
+
|
|
460
471
|
export function tryAssignQueueTask(
|
|
461
472
|
components: DaemonComponents,
|
|
462
473
|
meshId: string,
|
|
@@ -466,6 +477,32 @@ export function tryAssignQueueTask(
|
|
|
466
477
|
): boolean {
|
|
467
478
|
const mesh = getMeshWithCache(components, meshId);
|
|
468
479
|
const node = mesh?.nodes.find((n: any) => readMeshNodeId(n) === nodeId);
|
|
480
|
+
|
|
481
|
+
// WTCLAIM (fix-B extended to the enqueue→claim path): a base-targeted task must never be
|
|
482
|
+
// claimed by — and dispatched into — a co-located worktree-clone session, nor vice versa.
|
|
483
|
+
// The drain candidate's nodeId is derived from settings.meshNodeId || settings.nodeId
|
|
484
|
+
// (triggerMeshQueue), so a worktree session whose meshNodeId is empty/stale falls back to
|
|
485
|
+
// settings.nodeId = the BASE node id and impersonates the base node here. fix-B's worker-side
|
|
486
|
+
// workspace scope only ran for sessionless dispatch (meshScopeNodeId && !targetSessionId); the
|
|
487
|
+
// claim path ALWAYS carries a targetSessionId, so it never engaged. Apply the same scope here:
|
|
488
|
+
// for a LOCAL claiming session (adapter resolvable on this daemon), require its actual
|
|
489
|
+
// workingDir to match the target node's declared workspace. On a confirmed mismatch, refuse the
|
|
490
|
+
// claim so the task returns to pending for the correctly-scoped session/node to pull. Scoped to
|
|
491
|
+
// local sessions where the workspace is verifiable — a remote session lives on another daemon
|
|
492
|
+
// whose paths we cannot compare here (and remote candidates are already nodeId-matched from
|
|
493
|
+
// getRemoteIdleSessions). Conservative by design: when either workspace is unknown we do NOT
|
|
494
|
+
// skip, so a node with no declared workspace keeps its prior behavior and no legitimate claim
|
|
495
|
+
// is starved.
|
|
496
|
+
const localClaimAdapter = components.cliManager?.adapters?.get(sessionId) as { workingDir?: string } | undefined;
|
|
497
|
+
if (localClaimAdapter) {
|
|
498
|
+
const sessionWorkspace = normalizeMeshWorkspaceForCompare(localClaimAdapter.workingDir);
|
|
499
|
+
const nodeWorkspace = normalizeMeshWorkspaceForCompare(readNonEmptyString(node?.workspace));
|
|
500
|
+
if (sessionWorkspace && nodeWorkspace && sessionWorkspace !== nodeWorkspace) {
|
|
501
|
+
LOG.info('MeshQueue', `WTCLAIM: refusing claim for node ${nodeId} (${sessionId}) — session workspace "${sessionWorkspace}" ≠ node workspace "${nodeWorkspace}" (cross-workspace dispatch blocked)`);
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
469
506
|
const capabilityTags = buildMeshNodeCapabilityTags(node, providerType);
|
|
470
507
|
// Per-(node, provider) maxParallel cap (RepoMeshNodePolicy.providerRoles) layers
|
|
471
508
|
// on top of the global/taskMode caps — stricter wins. Resolved here where the
|