@adhdev/daemon-core 0.9.82-rc.22 → 0.9.82-rc.24
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/router.d.ts +2 -0
- package/dist/index.js +309 -173
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +309 -173
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +5 -5
- package/package.json +1 -1
- package/src/commands/router.ts +68 -26
- package/src/mesh/mesh-events.ts +80 -29
- package/src/mesh/mesh-work-queue.ts +132 -119
|
@@ -9,12 +9,12 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
9
9
|
queuedAt: number;
|
|
10
10
|
}
|
|
11
11
|
export declare function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean;
|
|
12
|
-
/** Drain and return all pending coordinator events,
|
|
13
|
-
export declare function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[];
|
|
12
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
13
|
+
export declare function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[];
|
|
14
14
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
15
|
-
export declare function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[];
|
|
16
|
-
/** Explicitly clear all pending coordinator events. */
|
|
17
|
-
export declare function clearPendingMeshCoordinatorEvents(): void;
|
|
15
|
+
export declare function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[];
|
|
16
|
+
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
17
|
+
export declare function clearPendingMeshCoordinatorEvents(meshId?: string): void;
|
|
18
18
|
export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
|
|
19
19
|
/**
|
|
20
20
|
* Triggers a queue check for all nodes in the mesh.
|
package/package.json
CHANGED
package/src/commands/router.ts
CHANGED
|
@@ -447,6 +447,23 @@ function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
|
447
447
|
};
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
|
|
451
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
452
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
453
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
454
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
458
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
459
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
460
|
+
|
|
461
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
462
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
463
|
+
|
|
464
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
465
|
+
}
|
|
466
|
+
|
|
450
467
|
function readLiveMeshNodeWorkspace(args: {
|
|
451
468
|
meshId: string;
|
|
452
469
|
nodeId: string;
|
|
@@ -454,7 +471,7 @@ function readLiveMeshNodeWorkspace(args: {
|
|
|
454
471
|
allowCoordinatorSession?: boolean;
|
|
455
472
|
}): string {
|
|
456
473
|
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
457
|
-
|
|
474
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
458
475
|
&& readStringValue(record?.workspace)
|
|
459
476
|
));
|
|
460
477
|
if (directNodeWorkspace) {
|
|
@@ -482,10 +499,9 @@ function collectLiveMeshSessionRecords(args: {
|
|
|
482
499
|
allowCoordinatorSession?: boolean;
|
|
483
500
|
}): any[] {
|
|
484
501
|
const matches = args.liveSessionRecords.filter((record) => {
|
|
485
|
-
if (readStringValue(record?.meta?.meshNodeId) === args.nodeId) return true;
|
|
486
|
-
const recordWorkspace = readStringValue(record?.workspace);
|
|
487
502
|
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
488
|
-
|
|
503
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)) return true;
|
|
504
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
489
505
|
});
|
|
490
506
|
|
|
491
507
|
if (args.allowCoordinatorSession) {
|
|
@@ -948,6 +964,8 @@ export interface CommandRouterDeps {
|
|
|
948
964
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
949
965
|
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
950
966
|
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
967
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
968
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
951
969
|
}
|
|
952
970
|
|
|
953
971
|
export interface CommandRouterResult {
|
|
@@ -1672,7 +1690,8 @@ export class DaemonCommandRouter {
|
|
|
1672
1690
|
}
|
|
1673
1691
|
|
|
1674
1692
|
case 'get_pending_mesh_events': {
|
|
1675
|
-
const
|
|
1693
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1694
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1676
1695
|
return { success: true, events };
|
|
1677
1696
|
}
|
|
1678
1697
|
|
|
@@ -3328,29 +3347,52 @@ export class DaemonCommandRouter {
|
|
|
3328
3347
|
}
|
|
3329
3348
|
if (workspace) {
|
|
3330
3349
|
if (!fs.existsSync(workspace)) {
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3350
|
+
// Workspace not local — attempt a P2P git probe for remote nodes.
|
|
3351
|
+
let remoteProbeApplied = false;
|
|
3352
|
+
if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand) {
|
|
3353
|
+
try {
|
|
3354
|
+
const remoteResult = await Promise.race([
|
|
3355
|
+
this.deps.dispatchMeshCommand(daemonId, 'git_status', { workspace }),
|
|
3356
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000)),
|
|
3357
|
+
]) as any;
|
|
3358
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
3359
|
+
if (remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean') {
|
|
3360
|
+
status.git = remoteGit;
|
|
3361
|
+
status.health = remoteGit.isGitRepo
|
|
3362
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
3363
|
+
: 'degraded';
|
|
3364
|
+
remoteProbeApplied = true;
|
|
3365
|
+
}
|
|
3366
|
+
} catch {
|
|
3367
|
+
// Probe timed out or P2P unavailable — fall back to cached status
|
|
3368
|
+
}
|
|
3340
3369
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3370
|
+
if (!remoteProbeApplied) {
|
|
3371
|
+
if (applyCachedInlineMeshNodeStatus(status, node)) {
|
|
3372
|
+
status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
|
|
3373
|
+
nodeStatuses.push(status);
|
|
3374
|
+
continue;
|
|
3375
|
+
}
|
|
3376
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
3377
|
+
status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
|
|
3378
|
+
nodeStatuses.push(status);
|
|
3379
|
+
continue;
|
|
3380
|
+
}
|
|
3350
3381
|
}
|
|
3351
|
-
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3382
|
+
} else {
|
|
3383
|
+
try {
|
|
3384
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
3385
|
+
status.git = gitStatus;
|
|
3386
|
+
if (gitStatus.isGitRepo) {
|
|
3387
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
3388
|
+
} else {
|
|
3389
|
+
status.health = 'degraded';
|
|
3390
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
3391
|
+
}
|
|
3392
|
+
} catch {
|
|
3393
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
3394
|
+
status.health = 'degraded';
|
|
3395
|
+
}
|
|
3354
3396
|
}
|
|
3355
3397
|
}
|
|
3356
3398
|
} else {
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -1,31 +1,47 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
1
3
|
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
4
|
import { loadConfig } from '../config/config.js';
|
|
3
5
|
import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
|
|
4
6
|
import { detectCLI } from '../detection/cli-detector.js';
|
|
5
7
|
import { LOG } from '../logging/logger.js';
|
|
6
|
-
import { appendLedgerEntry, buildTaskCompletionEvidence, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
8
|
+
import { appendLedgerEntry, buildTaskCompletionEvidence, getLedgerDir, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
7
9
|
import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
|
|
8
10
|
import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch } from './mesh-work-queue.js';
|
|
9
11
|
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// Remote Node Idle Session Tracking
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
13
|
-
// Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
|
|
14
|
-
// can assign tasks to them.
|
|
15
|
+
// Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
|
|
16
|
+
// can assign tasks to them. Each entry carries an expiresAt timestamp;
|
|
17
|
+
// entries are swept on insertion to prevent unbounded growth.
|
|
15
18
|
// ---------------------------------------------------------------------------
|
|
16
19
|
interface RemoteIdleSession {
|
|
17
20
|
nodeId: string;
|
|
18
21
|
sessionId: string;
|
|
19
22
|
providerType: string;
|
|
23
|
+
expiresAt: number;
|
|
20
24
|
}
|
|
25
|
+
const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
21
26
|
const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
|
|
22
27
|
|
|
28
|
+
function sweepExpiredRemoteIdleSessions(): void {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
for (const [key, session] of remoteIdleSessions) {
|
|
31
|
+
if (session.expiresAt <= now) remoteIdleSessions.delete(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
// ---------------------------------------------------------------------------
|
|
24
|
-
// MCP coordinator pending-event queue
|
|
36
|
+
// MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
|
|
25
37
|
// ---------------------------------------------------------------------------
|
|
26
38
|
// When a mesh event fires but no CLI coordinator session is registered (e.g.
|
|
27
|
-
// the coordinator is Claude Code running via MCP), we
|
|
28
|
-
//
|
|
39
|
+
// the coordinator is Claude Code running via MCP), we persist the event to a
|
|
40
|
+
// per-mesh JSONL file so it survives daemon restarts. The 50-entry hard cap
|
|
41
|
+
// is removed; the file is drained atomically on each get_pending_mesh_events
|
|
42
|
+
// call and limited to 100 KB to prevent runaway growth.
|
|
43
|
+
//
|
|
44
|
+
// File: <ledgerDir>/<meshId>.pending-events.jsonl
|
|
29
45
|
// ---------------------------------------------------------------------------
|
|
30
46
|
|
|
31
47
|
export interface PendingMeshCoordinatorEvent {
|
|
@@ -38,30 +54,53 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
38
54
|
queuedAt: number;
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
const
|
|
57
|
+
function getPendingEventsPath(meshId: string): string {
|
|
58
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
59
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
60
|
+
}
|
|
43
61
|
|
|
44
62
|
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
45
|
-
|
|
63
|
+
try {
|
|
64
|
+
appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
|
|
65
|
+
return true;
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
46
68
|
return false;
|
|
47
69
|
}
|
|
48
|
-
pendingMeshCoordinatorEvents.push(event);
|
|
49
|
-
return true;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
|
-
/** Drain and return all pending coordinator events,
|
|
53
|
-
export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
|
|
54
|
-
|
|
72
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
73
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
74
|
+
if (!meshId) return [];
|
|
75
|
+
const path = getPendingEventsPath(meshId);
|
|
76
|
+
if (!existsSync(path)) return [];
|
|
77
|
+
try {
|
|
78
|
+
const raw = readFileSync(path, 'utf-8');
|
|
79
|
+
try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
|
|
80
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
81
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
82
|
+
});
|
|
83
|
+
} catch { return []; }
|
|
55
84
|
}
|
|
56
85
|
|
|
57
86
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
58
|
-
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
59
|
-
|
|
87
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
88
|
+
if (!meshId) return [];
|
|
89
|
+
const path = getPendingEventsPath(meshId);
|
|
90
|
+
if (!existsSync(path)) return [];
|
|
91
|
+
try {
|
|
92
|
+
const raw = readFileSync(path, 'utf-8');
|
|
93
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
94
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
95
|
+
});
|
|
96
|
+
} catch { return []; }
|
|
60
97
|
}
|
|
61
98
|
|
|
62
|
-
/** Explicitly clear all pending coordinator events. */
|
|
63
|
-
export function clearPendingMeshCoordinatorEvents(): void {
|
|
64
|
-
|
|
99
|
+
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
100
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
|
|
101
|
+
if (!meshId) return;
|
|
102
|
+
const path = getPendingEventsPath(meshId);
|
|
103
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
65
104
|
}
|
|
66
105
|
|
|
67
106
|
function readNonEmptyString(value: unknown): string {
|
|
@@ -180,7 +219,16 @@ export function tryAssignQueueTask(
|
|
|
180
219
|
message: task.message,
|
|
181
220
|
}).catch((e: any) => {
|
|
182
221
|
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
183
|
-
|
|
222
|
+
// Revert to pending so the task can be retried rather than permanently failing
|
|
223
|
+
updateTaskStatus(meshId, task.id, 'pending');
|
|
224
|
+
try {
|
|
225
|
+
appendLedgerEntry(meshId, {
|
|
226
|
+
kind: 'dispatch_failed' as any,
|
|
227
|
+
nodeId,
|
|
228
|
+
sessionId,
|
|
229
|
+
payload: { taskId: task.id, error: e?.message, retryable: true },
|
|
230
|
+
});
|
|
231
|
+
} catch { /* ledger write is best-effort */ }
|
|
184
232
|
});
|
|
185
233
|
return true;
|
|
186
234
|
}
|
|
@@ -614,10 +662,11 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
614
662
|
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
|
|
615
663
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
616
664
|
if (nodeId && providerType) {
|
|
617
|
-
//
|
|
618
|
-
|
|
665
|
+
// Queue state is already updated above; setImmediate avoids the
|
|
666
|
+
// 500 ms artificial delay while still deferring past this call frame.
|
|
667
|
+
setImmediate(() => {
|
|
619
668
|
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
620
|
-
}
|
|
669
|
+
});
|
|
621
670
|
}
|
|
622
671
|
}
|
|
623
672
|
} else if (args.event === 'agent:ready') {
|
|
@@ -658,13 +707,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
658
707
|
}
|
|
659
708
|
|
|
660
709
|
if (sessionId && nodeId && providerType) {
|
|
661
|
-
|
|
662
|
-
|
|
710
|
+
sweepExpiredRemoteIdleSessions();
|
|
711
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
|
|
712
|
+
nodeId, sessionId, providerType,
|
|
713
|
+
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
714
|
+
});
|
|
715
|
+
setImmediate(() => {
|
|
663
716
|
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
664
|
-
if (assigned) {
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
}, 500);
|
|
717
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
718
|
+
});
|
|
668
719
|
}
|
|
669
720
|
} else if (args.event === 'agent:generating_started') {
|
|
670
721
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|