@adhdev/daemon-core 0.9.82-rc.272 → 0.9.82-rc.274
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/boot/daemon-lifecycle.d.ts +3 -0
- package/dist/index.js +199 -128
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +199 -128
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-coordinator.d.ts +1 -21
- package/dist/mesh/mesh-events-pending.d.ts +0 -7
- package/dist/mesh/mesh-events.d.ts +2 -1
- package/dist/mesh/mesh-missions.d.ts +10 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
- package/dist/repo-mesh-types.d.ts +8 -0
- package/package.json +2 -2
- package/src/boot/daemon-lifecycle.ts +16 -2
- package/src/commands/router.ts +14 -21
- package/src/mesh/mesh-events-coordinator.ts +31 -107
- package/src/mesh/mesh-events-pending.ts +14 -56
- package/src/mesh/mesh-events.ts +5 -1
- package/src/mesh/mesh-missions.ts +21 -0
- package/src/mesh/mesh-reconcile-loop.ts +256 -0
- package/src/providers/cli-provider-instance.ts +23 -0
- package/src/repo-mesh-types.ts +8 -0
|
@@ -28,6 +28,7 @@ export interface MeshQueueTriggerResult {
|
|
|
28
28
|
}
|
|
29
29
|
export declare function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<MeshQueueTriggerResult>;
|
|
30
30
|
export declare function isMeshCoordinatorEvent(eventName: unknown): eventName is string;
|
|
31
|
+
export declare function shouldForceInjectMeshEvent(eventName: unknown): boolean;
|
|
31
32
|
export declare function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>): {
|
|
32
33
|
success: boolean;
|
|
33
34
|
forwarded: number;
|
|
@@ -40,7 +41,6 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
40
41
|
duplicateCompletion?: undefined;
|
|
41
42
|
duplicateStopped?: undefined;
|
|
42
43
|
error?: undefined;
|
|
43
|
-
remoteForwarded?: undefined;
|
|
44
44
|
} | {
|
|
45
45
|
success: boolean;
|
|
46
46
|
forwarded: number;
|
|
@@ -53,7 +53,6 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
53
53
|
duplicateCompletion?: undefined;
|
|
54
54
|
duplicateStopped?: undefined;
|
|
55
55
|
error?: undefined;
|
|
56
|
-
remoteForwarded?: undefined;
|
|
57
56
|
} | {
|
|
58
57
|
success: boolean;
|
|
59
58
|
forwarded: number;
|
|
@@ -66,7 +65,6 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
66
65
|
duplicateCompletion?: undefined;
|
|
67
66
|
duplicateStopped?: undefined;
|
|
68
67
|
error?: undefined;
|
|
69
|
-
remoteForwarded?: undefined;
|
|
70
68
|
} | {
|
|
71
69
|
success: boolean;
|
|
72
70
|
forwarded: number;
|
|
@@ -79,7 +77,6 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
79
77
|
duplicateCompletion?: undefined;
|
|
80
78
|
duplicateStopped?: undefined;
|
|
81
79
|
error?: undefined;
|
|
82
|
-
remoteForwarded?: undefined;
|
|
83
80
|
} | {
|
|
84
81
|
success: boolean;
|
|
85
82
|
forwarded: number;
|
|
@@ -92,7 +89,6 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
92
89
|
duplicateApproval?: undefined;
|
|
93
90
|
duplicateStopped?: undefined;
|
|
94
91
|
error?: undefined;
|
|
95
|
-
remoteForwarded?: undefined;
|
|
96
92
|
} | {
|
|
97
93
|
success: boolean;
|
|
98
94
|
forwarded: number;
|
|
@@ -105,7 +101,6 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
105
101
|
duplicateApproval?: undefined;
|
|
106
102
|
duplicateStopped?: undefined;
|
|
107
103
|
error?: undefined;
|
|
108
|
-
remoteForwarded?: undefined;
|
|
109
104
|
} | {
|
|
110
105
|
success: boolean;
|
|
111
106
|
forwarded: number;
|
|
@@ -118,11 +113,9 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
118
113
|
duplicateApproval?: undefined;
|
|
119
114
|
duplicateCompletion?: undefined;
|
|
120
115
|
error?: undefined;
|
|
121
|
-
remoteForwarded?: undefined;
|
|
122
116
|
} | {
|
|
123
117
|
success: boolean;
|
|
124
118
|
forwarded: number;
|
|
125
|
-
remoteForwarded: boolean;
|
|
126
119
|
suppressed?: undefined;
|
|
127
120
|
intentionalCleanupStop?: undefined;
|
|
128
121
|
terminalLedgerEvidence?: undefined;
|
|
@@ -132,19 +125,6 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
132
125
|
duplicateCompletion?: undefined;
|
|
133
126
|
duplicateStopped?: undefined;
|
|
134
127
|
error?: undefined;
|
|
135
|
-
} | {
|
|
136
|
-
success: boolean;
|
|
137
|
-
forwarded: number;
|
|
138
|
-
suppressed?: undefined;
|
|
139
|
-
intentionalCleanupStop?: undefined;
|
|
140
|
-
terminalLedgerEvidence?: undefined;
|
|
141
|
-
terminalLedgerKind?: undefined;
|
|
142
|
-
duplicateRefineTerminalEvent?: undefined;
|
|
143
|
-
duplicateApproval?: undefined;
|
|
144
|
-
duplicateCompletion?: undefined;
|
|
145
|
-
duplicateStopped?: undefined;
|
|
146
|
-
error?: undefined;
|
|
147
|
-
remoteForwarded?: undefined;
|
|
148
128
|
} | {
|
|
149
129
|
success: boolean;
|
|
150
130
|
error: string;
|
|
@@ -18,13 +18,6 @@ export declare function readRefineJobId(event: {
|
|
|
18
18
|
metadataEvent?: Record<string, unknown>;
|
|
19
19
|
} | Record<string, unknown>): string;
|
|
20
20
|
export declare function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string;
|
|
21
|
-
/**
|
|
22
|
-
* R3: record that an event was direct-injected into a live coordinator on `coordinatorDaemonId`.
|
|
23
|
-
* That coordinator's own drain (get_pending_mesh_events with the same coordinatorDaemonId) will
|
|
24
|
-
* skip the queued copy, so it receives the event exactly once instead of twice (PTY + poll).
|
|
25
|
-
* Other consumers (unscoped drainers, other daemons) are unaffected — they did not get the inject.
|
|
26
|
-
*/
|
|
27
|
-
export declare function markMeshCoordinatorEventDirectDelivered(coordinatorDaemonId: string, event: PendingMeshCoordinatorEvent): void;
|
|
28
21
|
export declare function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean;
|
|
29
22
|
export declare function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean;
|
|
30
23
|
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
2
|
-
export { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents,
|
|
2
|
+
export { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, } from './mesh-events-pending.js';
|
|
3
3
|
export { reconcileDirectDispatchCompletionFromTranscript, } from './mesh-events-stale.js';
|
|
4
|
+
export { setupMeshReconcileLoop, runMeshReconcileTick, } from './mesh-reconcile-loop.js';
|
|
4
5
|
export type { MeshQueueTriggerResult } from './mesh-events-coordinator.js';
|
|
5
6
|
export { tryAssignQueueTask, triggerMeshQueue, handleMeshForwardEvent, setupMeshEventForwarding, isMeshCoordinatorEvent, __resetIdleAutoFastForwardForTests, __resetMeshWorkspaceCacheForTests, } from './mesh-events-coordinator.js';
|
|
@@ -50,6 +50,16 @@ export declare function summarizeMissionTasks(meshId: string, missionId: string)
|
|
|
50
50
|
export declare function summarizeMeshMission(meshId: string, mission: MeshMissionRecord): MeshMissionSummary;
|
|
51
51
|
/** Active mission summaries for mesh_status / coordinator prompt injection. */
|
|
52
52
|
export declare function getActiveMeshMissionSummaries(meshId: string): MeshMissionSummary[];
|
|
53
|
+
/**
|
|
54
|
+
* Mission summaries for the mesh_status dashboard surface: every active/paused
|
|
55
|
+
* mission plus a capped, newest-first slice of completed/abandoned history so
|
|
56
|
+
* the dashboard can render a collapsible "history" section without unbounded
|
|
57
|
+
* payload growth. Returned newest-first within each group (active/paused first,
|
|
58
|
+
* then history), so the frontend can split on `status` directly.
|
|
59
|
+
*/
|
|
60
|
+
export declare function getMeshStatusMissionSummaries(meshId: string, options?: {
|
|
61
|
+
historyLimit?: number;
|
|
62
|
+
}): MeshMissionSummary[];
|
|
53
63
|
/**
|
|
54
64
|
* M3-3: render active missions as a prompt section for {{mission}}.
|
|
55
65
|
* Empty string when no active mission — the prompt stays byte-identical to
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
|
+
export declare function runMeshReconcileTick(components: DaemonComponents): Promise<void>;
|
|
3
|
+
interface ReconcileLoopHandle {
|
|
4
|
+
stop(): void;
|
|
5
|
+
}
|
|
6
|
+
export declare function setupMeshReconcileLoop(components: DaemonComponents): ReconcileLoopHandle;
|
|
7
|
+
export {};
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* IMPORTANT: This file must remain runtime-free (types only).
|
|
12
12
|
*/
|
|
13
13
|
import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
|
|
14
|
+
import type { MeshMissionSummary } from './mesh/mesh-missions.js';
|
|
14
15
|
export interface RepoMesh {
|
|
15
16
|
id: string;
|
|
16
17
|
name: string;
|
|
@@ -337,6 +338,13 @@ export interface RepoMeshStatus {
|
|
|
337
338
|
nodes: RepoMeshNodeStatus[];
|
|
338
339
|
queue?: RepoMeshQueueStatus;
|
|
339
340
|
ledger?: RepoMeshLedgerStatus;
|
|
341
|
+
/**
|
|
342
|
+
* Mission summaries for the dashboard overview. Active/paused missions plus a
|
|
343
|
+
* capped, newest-first slice of completed/abandoned history. Omitted by older
|
|
344
|
+
* daemons — the dashboard must treat this as optional and render an empty
|
|
345
|
+
* state when absent. Split on each entry's `status` for live vs. history.
|
|
346
|
+
*/
|
|
347
|
+
missions?: MeshMissionSummary[];
|
|
340
348
|
}
|
|
341
349
|
export type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
|
|
342
350
|
import type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.274",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.274",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -37,6 +37,7 @@ import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
|
37
37
|
import type { IdeProviderInstance } from '../providers/ide-provider-instance.js';
|
|
38
38
|
import { createDefaultGitCommandServices } from '../git/git-commands.js';
|
|
39
39
|
import { setupMeshEventForwarding } from '../mesh/mesh-events.js';
|
|
40
|
+
import { setupMeshReconcileLoop } from '../mesh/mesh-reconcile-loop.js';
|
|
40
41
|
import { loadMeshCoordinatorRegistry } from '../mesh/coordinator-registry.js';
|
|
41
42
|
import { applyProcessHardening } from './process-hardening.js';
|
|
42
43
|
import { installProviderProcessShim } from '../providers/sdk/v1/sandbox/require-whitelist.js';
|
|
@@ -117,6 +118,11 @@ export interface DaemonComponents {
|
|
|
117
118
|
// subscriptions). Injected by daemon-cloud; absent/no-op on standalone. Replaces cloud's
|
|
118
119
|
// former separate instanceManager.onEvent listener so the event path stays single-listener.
|
|
119
120
|
onMeshCoordinatorEventForwarded?: (payload: Record<string, unknown>) => void;
|
|
121
|
+
// Periodic queue → live-coordinator reconcile loop handle. Set during
|
|
122
|
+
// initDaemonComponents, stopped during shutdownDaemonComponents. Drives the
|
|
123
|
+
// single-model (queue + polling) delivery: drains the pending-events queue
|
|
124
|
+
// on a fixed interval and injects into live CLI coordinators when idle.
|
|
125
|
+
meshReconcileLoop?: { stop(): void };
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
export interface DaemonDevSupportOptions {
|
|
@@ -341,7 +347,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
341
347
|
// 10. Start instance ticking
|
|
342
348
|
instanceManager.startTicking(config.tickIntervalMs ?? 5_000);
|
|
343
349
|
|
|
344
|
-
const components = {
|
|
350
|
+
const components: DaemonComponents = {
|
|
345
351
|
providerLoader,
|
|
346
352
|
instanceManager,
|
|
347
353
|
cliManager,
|
|
@@ -358,8 +364,14 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
358
364
|
onMeshCoordinatorEventForwarded: config.onMeshCoordinatorEventForwarded,
|
|
359
365
|
};
|
|
360
366
|
|
|
361
|
-
// 11. Setup Mesh Event Forwarding
|
|
367
|
+
// 11. Setup Mesh Event Forwarding (queue persistence) + periodic reconcile loop.
|
|
368
|
+
// injectMeshSystemMessage now ONLY persists events to the pending-events queue;
|
|
369
|
+
// the reconcile loop drains that queue on a fixed interval and injects into live
|
|
370
|
+
// CLI coordinators when idle (and, in cloud mode, pulls remote worker daemons'
|
|
371
|
+
// queues over P2P). This is the single-model (queue + polling) replacement for the
|
|
372
|
+
// old spontaneous-forward push paths.
|
|
362
373
|
setupMeshEventForwarding(components);
|
|
374
|
+
components.meshReconcileLoop = setupMeshReconcileLoop(components);
|
|
363
375
|
|
|
364
376
|
// 12. Resume any refine jobs that were interrupted by a previous daemon restart.
|
|
365
377
|
setImmediate(() => void router.resumePendingRefineJobsOnStartup());
|
|
@@ -404,11 +416,13 @@ export async function shutdownDaemonComponents(components: DaemonComponents): Pr
|
|
|
404
416
|
const {
|
|
405
417
|
poller, cdpInitializer, agentStreamManager,
|
|
406
418
|
cliManager, instanceManager, cdpManagers,
|
|
419
|
+
meshReconcileLoop,
|
|
407
420
|
} = components;
|
|
408
421
|
|
|
409
422
|
// 1. Stop timers
|
|
410
423
|
poller.stop();
|
|
411
424
|
cdpInitializer.stop();
|
|
425
|
+
try { meshReconcileLoop?.stop(); } catch { /* noop */ }
|
|
412
426
|
|
|
413
427
|
// 2. Dispose agent stream
|
|
414
428
|
try {
|
package/src/commands/router.ts
CHANGED
|
@@ -5259,27 +5259,17 @@ export class DaemonCommandRouter {
|
|
|
5259
5259
|
}
|
|
5260
5260
|
|
|
5261
5261
|
case 'launch_cli': {
|
|
5262
|
-
//
|
|
5263
|
-
//
|
|
5264
|
-
//
|
|
5265
|
-
//
|
|
5266
|
-
//
|
|
5267
|
-
//
|
|
5268
|
-
//
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
const isMeshWorkerLaunch = !!launchSettings
|
|
5274
|
-
&& (readStringValue(launchSettings.meshNodeFor) || launchSettings.launchedByCoordinator === true);
|
|
5275
|
-
const hasCoordinatorDaemonId = !!launchSettings && !!readStringValue(launchSettings.meshCoordinatorDaemonId);
|
|
5276
|
-
if (launchSettings && isMeshWorkerLaunch && !hasCoordinatorDaemonId) {
|
|
5277
|
-
try {
|
|
5278
|
-
const localDaemonId = readStringValue(loadConfig().machineId);
|
|
5279
|
-
if (localDaemonId) launchSettings.meshCoordinatorDaemonId = localDaemonId;
|
|
5280
|
-
} catch { /* best-effort — launch proceeds without the stamp */ }
|
|
5281
|
-
}
|
|
5282
|
-
}
|
|
5262
|
+
// The coordinator routing anchor (meshCoordinatorDaemonId) is stamped
|
|
5263
|
+
// upstream by mesh_launch_session, which resolves
|
|
5264
|
+
// coordinatorNode.daemonId || ctx.localDaemonId || ctx.localMachineId and
|
|
5265
|
+
// fail-closes for a remote node when none resolve. We deliberately do NOT
|
|
5266
|
+
// self-stamp this daemon's own id when the field is missing: for a
|
|
5267
|
+
// P2P-relayed remote worker launch, stamping the worker's own id would make
|
|
5268
|
+
// the self-forward gate (mesh-events-coordinator: sameDaemonId) treat the
|
|
5269
|
+
// worker as its own coordinator, suppressing the spontaneous completion-event
|
|
5270
|
+
// forward and leaving the event in the pending inbox until a read_chat
|
|
5271
|
+
// reconcile drains it. If the anchor is genuinely absent here, leave it
|
|
5272
|
+
// absent rather than poison the routing.
|
|
5283
5273
|
const launchResult = await this.deps.cliManager.handleCliCommand(cmd, args);
|
|
5284
5274
|
// Bug C fix (part 1): when launching a mesh node worker session, surface
|
|
5285
5275
|
// bootstrapPending:true if the node's worktree bootstrap is still running.
|
|
@@ -8428,6 +8418,8 @@ export class DaemonCommandRouter {
|
|
|
8428
8418
|
nodes: mesh.nodes || [],
|
|
8429
8419
|
liveSessionRecords: liveMeshSessions,
|
|
8430
8420
|
});
|
|
8421
|
+
const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
|
|
8422
|
+
const missions = getMeshStatusMissionSummaries(meshId);
|
|
8431
8423
|
const statusResult = {
|
|
8432
8424
|
success: true,
|
|
8433
8425
|
meshId: mesh.id,
|
|
@@ -8470,6 +8462,7 @@ export class DaemonCommandRouter {
|
|
|
8470
8462
|
nodes: nodeStatuses,
|
|
8471
8463
|
queue: { tasks: queue, summary: queueSummary },
|
|
8472
8464
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
8465
|
+
...(missions.length > 0 ? { missions } : {}),
|
|
8473
8466
|
...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
|
|
8474
8467
|
...(historicalSessions ? { historicalSessions } : {}),
|
|
8475
8468
|
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
@@ -10,7 +10,7 @@ import { buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, claimNextTask,
|
|
|
10
10
|
import { fastForwardMeshNode } from './mesh-fast-forward.js';
|
|
11
11
|
import { createSessionDelivery, markSessionDeliveriesTerminal, updateSessionDeliveryStatus, recordCompletionConflict } from './mesh-delivery-policy.js';
|
|
12
12
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
13
|
-
import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents
|
|
13
|
+
import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents } from './mesh-events-pending.js';
|
|
14
14
|
import type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
15
15
|
import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent } from './mesh-routing.js';
|
|
16
16
|
import { resolveDelegatedWorkerAutoApprove } from '../repo-mesh-types.js';
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
resolveEventSessionId,
|
|
28
28
|
readRefineJobId,
|
|
29
29
|
readWorkerResultMetadata,
|
|
30
|
-
sameDaemonId,
|
|
31
30
|
} from './mesh-events-utils.js';
|
|
32
31
|
|
|
33
32
|
// ---------------------------------------------------------------------------
|
|
@@ -986,7 +985,7 @@ const MESH_FORCE_INJECT_EVENTS = new Set([
|
|
|
986
985
|
'worktree_bootstrap_failed',
|
|
987
986
|
]);
|
|
988
987
|
|
|
989
|
-
function shouldForceInjectMeshEvent(eventName: unknown): boolean {
|
|
988
|
+
export function shouldForceInjectMeshEvent(eventName: unknown): boolean {
|
|
990
989
|
return typeof eventName === 'string' && MESH_FORCE_INJECT_EVENTS.has(eventName);
|
|
991
990
|
}
|
|
992
991
|
|
|
@@ -1007,7 +1006,6 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1007
1006
|
const workerCoordinatorDaemonId = readNonEmptyString(
|
|
1008
1007
|
(sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorDaemonId,
|
|
1009
1008
|
);
|
|
1010
|
-
const localDaemonId = readNonEmptyString(loadConfig().machineId);
|
|
1011
1009
|
|
|
1012
1010
|
// R2: cloud P2P dashboard metadata sync. The cloud daemon used to do this from its own
|
|
1013
1011
|
// relay listener; now the single core forwarder invokes the injected hook (no-op on
|
|
@@ -1392,82 +1390,24 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1392
1390
|
});
|
|
1393
1391
|
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
1394
1392
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
const remoteCoordinatorDaemonId = workerCoordinatorDaemonId && localDaemonId && !sameDaemonId(workerCoordinatorDaemonId, localDaemonId)
|
|
1414
|
-
? workerCoordinatorDaemonId
|
|
1415
|
-
: '';
|
|
1416
|
-
if (remoteCoordinatorDaemonId && components.dispatchMeshCommand) {
|
|
1417
|
-
const forwardPayload: Record<string, unknown> = {
|
|
1418
|
-
event: args.event,
|
|
1419
|
-
meshId: args.meshId,
|
|
1420
|
-
nodeId: args.nodeId || undefined,
|
|
1421
|
-
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
1422
|
-
...args.metadataEvent,
|
|
1423
|
-
...(recoveryContext ? { recoveryContext } : {}),
|
|
1424
|
-
};
|
|
1425
|
-
components.dispatchMeshCommand(remoteCoordinatorDaemonId, 'mesh_forward_event', forwardPayload)
|
|
1426
|
-
.then(() => {
|
|
1427
|
-
LOG.info('MeshEvents', `Forwarded ${args.event} for mesh ${args.meshId} to remote coordinator daemon ${remoteCoordinatorDaemonId.slice(0, 12)}…`);
|
|
1428
|
-
})
|
|
1429
|
-
.catch((error: any) => {
|
|
1430
|
-
LOG.warn('MeshEvents', `Remote forward of ${args.event} failed (${error?.message || error}); queuing for backfill`);
|
|
1431
|
-
queuePendingMeshCoordinatorEvent({
|
|
1432
|
-
event: args.event,
|
|
1433
|
-
meshId: args.meshId,
|
|
1434
|
-
nodeLabel: args.nodeLabel,
|
|
1435
|
-
nodeId: args.nodeId || undefined,
|
|
1436
|
-
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
1437
|
-
metadataEvent: { ...args.metadataEvent, ...(recoveryContext ? { recoveryContext } : {}) },
|
|
1438
|
-
coordinatorMessage: messageText,
|
|
1439
|
-
queuedAt: Date.now(),
|
|
1440
|
-
targetCoordinatorDaemonId: remoteCoordinatorDaemonId,
|
|
1441
|
-
});
|
|
1442
|
-
});
|
|
1443
|
-
return { success: true, forwarded: 0, remoteForwarded: true };
|
|
1444
|
-
}
|
|
1445
|
-
if (queuePendingMeshCoordinatorEvent({
|
|
1446
|
-
event: args.event,
|
|
1447
|
-
meshId: args.meshId,
|
|
1448
|
-
nodeLabel: args.nodeLabel,
|
|
1449
|
-
nodeId: args.nodeId || undefined,
|
|
1450
|
-
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
1451
|
-
metadataEvent: {
|
|
1452
|
-
...args.metadataEvent,
|
|
1453
|
-
...(recoveryContext ? { recoveryContext } : {}),
|
|
1454
|
-
},
|
|
1455
|
-
coordinatorMessage: messageText,
|
|
1456
|
-
queuedAt: Date.now(),
|
|
1457
|
-
...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
|
|
1458
|
-
})) {
|
|
1459
|
-
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''})`);
|
|
1460
|
-
}
|
|
1461
|
-
return { success: true, forwarded: 0 };
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
// All events — terminal and non-terminal — are queued for MCP coordinator
|
|
1465
|
-
// delivery via the pending-events file/SQLite path. This ensures that MCP
|
|
1466
|
-
// coordinators (which poll via get_pending_mesh_events / mesh_status) always
|
|
1467
|
-
// receive terminal events even when a live CLI coordinator session is present.
|
|
1468
|
-
// Previously, terminal events skipped the queue and were only direct-injected
|
|
1469
|
-
// into live CLI coordinators via send_message, which silently dropped them
|
|
1470
|
-
// when the coordinator was in a generating state.
|
|
1393
|
+
// ── Queue-only delivery (single-model: queue + periodic poll) ──────────────
|
|
1394
|
+
// Every mesh coordinator event — terminal or not, local-coordinator or
|
|
1395
|
+
// remote — is persisted to the pending-events queue (SQLite + JSONL) and
|
|
1396
|
+
// NOTHING is pushed here. The old spontaneous-forward paths were removed:
|
|
1397
|
+
// - F1 remote P2P `mesh_forward_event` dispatch (network/stamp-dependent,
|
|
1398
|
+
// silently dropped on P2P failure or missing meshCoordinatorDaemonId)
|
|
1399
|
+
// - F3 live-CLI PTY `send_message` fire-and-forget inject (silently
|
|
1400
|
+
// dropped when the coordinator was generating)
|
|
1401
|
+
// Delivery to a live CLI coordinator now happens via setupMeshReconcileLoop,
|
|
1402
|
+
// which drains this queue on a fixed interval and injects into the coordinator
|
|
1403
|
+
// only when it is idle. A pure stdio MCP (LLM) coordinator — which has no live
|
|
1404
|
+
// CLI session to inject into — drains the queue itself when it calls a mesh
|
|
1405
|
+
// tool (mesh_status / mesh_read_chat). Either way the queue is the single
|
|
1406
|
+
// source of truth and the only thing this function writes to.
|
|
1407
|
+
//
|
|
1408
|
+
// targetCoordinatorDaemonId scopes the event to a specific coordinator daemon
|
|
1409
|
+
// (unicast) when the worker carries one, so the reconcile loop on the right
|
|
1410
|
+
// daemon drains it and other daemons skip it. Absent → broadcast/backfill.
|
|
1471
1411
|
const pendingEvent = {
|
|
1472
1412
|
event: args.event,
|
|
1473
1413
|
meshId: args.meshId,
|
|
@@ -1483,30 +1423,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1483
1423
|
...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
|
|
1484
1424
|
};
|
|
1485
1425
|
if (queuePendingMeshCoordinatorEvent(pendingEvent)) {
|
|
1486
|
-
LOG.info('MeshEvents', `Queued ${args.event} for
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
// R3: the live coordinators below receive this event directly in their PTY. They (or their
|
|
1490
|
-
// MCP client) also poll get_pending_mesh_events with coordinatorDaemonId = this daemon, which
|
|
1491
|
-
// would re-deliver the queued copy → the user saw the same completion twice. Mark the event
|
|
1492
|
-
// direct-delivered to THIS daemon so that coordinator's own drain skips it. The queue entry
|
|
1493
|
-
// stays for every OTHER consumer (idle / MCP-only / remote) that did not get the direct inject.
|
|
1494
|
-
// markMeshCoordinatorEventDirectDelivered canonicalizes the id internally so the raw
|
|
1495
|
-
// machineId here matches the prefixed instanceId (standalone_mach_X) the coordinator drains with.
|
|
1496
|
-
if (localDaemonId) {
|
|
1497
|
-
markMeshCoordinatorEventDirectDelivered(localDaemonId, pendingEvent);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
const forceInject = shouldForceInjectMeshEvent(args.event);
|
|
1501
|
-
for (const coord of coordinatorInstances) {
|
|
1502
|
-
const coordState = coord.getState();
|
|
1503
|
-
LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}${forceInject ? ' (force)' : ''}`);
|
|
1504
|
-
coord.onEvent('send_message', {
|
|
1505
|
-
input: { text: messageText, textFallback: messageText },
|
|
1506
|
-
...(forceInject ? { force: true } : {}),
|
|
1507
|
-
});
|
|
1426
|
+
LOG.info('MeshEvents', `Queued ${args.event} for coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''})`);
|
|
1508
1427
|
}
|
|
1509
|
-
return { success: true, forwarded:
|
|
1428
|
+
return { success: true, forwarded: 0 };
|
|
1510
1429
|
}
|
|
1511
1430
|
|
|
1512
1431
|
export function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>) {
|
|
@@ -1563,10 +1482,15 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
1563
1482
|
|
|
1564
1483
|
export function setupMeshEventForwarding(components: DaemonComponents) {
|
|
1565
1484
|
components.instanceManager.onEvent((event) => {
|
|
1566
|
-
// --- Coordinator idle auto-flush ---
|
|
1567
|
-
// When a coordinator session becomes idle, flush any pending
|
|
1568
|
-
// that accumulated while it was generating
|
|
1569
|
-
//
|
|
1485
|
+
// --- Coordinator idle auto-flush (fast path) ---
|
|
1486
|
+
// When a coordinator session becomes idle, immediately flush any pending
|
|
1487
|
+
// coordinator events that accumulated while it was generating, rather than
|
|
1488
|
+
// waiting up to one reconcile interval for setupMeshReconcileLoop to do it.
|
|
1489
|
+
// Both paths drain the SAME queue via drainPendingMeshCoordinatorEvents,
|
|
1490
|
+
// whose SQLite drained=1 marking is atomic — whichever fires first consumes
|
|
1491
|
+
// the events and the other gets nothing, so there is no double-delivery.
|
|
1492
|
+
// This runs before the delegate routing below so that coordinator-own idle
|
|
1493
|
+
// transitions are handled first.
|
|
1570
1494
|
// Exception: a coordinator that is itself a direct-dispatch target still needs
|
|
1571
1495
|
// to go through delegate routing so that the dispatching coordinator receives a
|
|
1572
1496
|
// pendingCoordinatorEvents entry for the completion.
|
|
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto';
|
|
|
4
4
|
import { LOG } from '../logging/logger.js';
|
|
5
5
|
import { getLedgerDir, readLedgerEntries } from './mesh-ledger.js';
|
|
6
6
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
7
|
-
import { buildMeshSystemMessage,
|
|
7
|
+
import { buildMeshSystemMessage, readNonEmptyString, readRecord, resolveEventSessionId } from './mesh-events-utils.js';
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
|
|
@@ -83,46 +83,11 @@ export function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent)
|
|
|
83
83
|
].join('::');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* R3: record that an event was direct-injected into a live coordinator on `coordinatorDaemonId`.
|
|
94
|
-
* That coordinator's own drain (get_pending_mesh_events with the same coordinatorDaemonId) will
|
|
95
|
-
* skip the queued copy, so it receives the event exactly once instead of twice (PTY + poll).
|
|
96
|
-
* Other consumers (unscoped drainers, other daemons) are unaffected — they did not get the inject.
|
|
97
|
-
*/
|
|
98
|
-
export function markMeshCoordinatorEventDirectDelivered(
|
|
99
|
-
coordinatorDaemonId: string,
|
|
100
|
-
event: PendingMeshCoordinatorEvent,
|
|
101
|
-
): void {
|
|
102
|
-
// Canonicalize so the raw machineId used at inject time and the prefixed instanceId the
|
|
103
|
-
// coordinator drains with resolve to the same key.
|
|
104
|
-
const canonical = canonicalDaemonId(coordinatorDaemonId);
|
|
105
|
-
if (!canonical) return;
|
|
106
|
-
const fingerprint = buildPendingEventFingerprint(event);
|
|
107
|
-
if (!fingerprint.trim()) return;
|
|
108
|
-
try {
|
|
109
|
-
const store = MeshRuntimeStore.getInstance();
|
|
110
|
-
store.recordDirectDelivered(canonical, fingerprint, DIRECT_DELIVERED_TTL_MS);
|
|
111
|
-
store.sweepExpiredDirectDelivered();
|
|
112
|
-
} catch { /* best-effort — a duplicate is preferable to a crash */ }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function wasDirectDeliveredToCoordinator(coordinatorDaemonId: string, event: PendingMeshCoordinatorEvent): boolean {
|
|
116
|
-
const canonical = canonicalDaemonId(coordinatorDaemonId);
|
|
117
|
-
if (!canonical) return false;
|
|
118
|
-
const fingerprint = buildPendingEventFingerprint(event);
|
|
119
|
-
if (!fingerprint.trim()) return false;
|
|
120
|
-
try {
|
|
121
|
-
return MeshRuntimeStore.getInstance().wasDirectDelivered(canonical, fingerprint);
|
|
122
|
-
} catch {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
86
|
+
// NOTE: the former R3 "direct-delivered" marker (markMeshCoordinatorEventDirectDelivered /
|
|
87
|
+
// wasDirectDeliveredToCoordinator) was removed when spontaneous PTY direct-inject was retired.
|
|
88
|
+
// Delivery is now queue-drain-only: an event is consumed by exactly one drainer via the atomic
|
|
89
|
+
// SQLite drained=1 marking, so there is no PTY-vs-poll double-delivery left to dedup against.
|
|
90
|
+
// The dormant mesh_direct_delivered_events table / store helpers remain (harmless) but unused.
|
|
126
91
|
|
|
127
92
|
export function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
128
93
|
const fingerprint = buildPendingEventFingerprint(event);
|
|
@@ -362,14 +327,11 @@ export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDa
|
|
|
362
327
|
for (const event of filtered) pushUnique(event);
|
|
363
328
|
}
|
|
364
329
|
if (merged.length === 0) return [];
|
|
365
|
-
// R3
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
: merged;
|
|
371
|
-
if (deliverable.length === 0) return [];
|
|
372
|
-
return reconcilePendingMeshCoordinatorEvents(meshId, deliverable);
|
|
330
|
+
// (Former R3 direct-delivered dedup removed.) Spontaneous PTY direct-inject no
|
|
331
|
+
// longer exists — delivery is now queue-drain-only (reconcile loop or MCP pull),
|
|
332
|
+
// so an event is consumed by exactly one drainer via the atomic SQLite drained=1
|
|
333
|
+
// marking. There is no PTY-vs-poll double path left to dedup against.
|
|
334
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, merged);
|
|
373
335
|
}
|
|
374
336
|
|
|
375
337
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
@@ -404,13 +366,9 @@ export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaem
|
|
|
404
366
|
pushUnique(event);
|
|
405
367
|
}
|
|
406
368
|
|
|
407
|
-
// R3
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
? merged.filter(event => !wasDirectDeliveredToCoordinator(coordinatorDaemonId, event))
|
|
411
|
-
: merged;
|
|
412
|
-
|
|
413
|
-
return reconcilePendingMeshCoordinatorEvents(meshId, deliverable);
|
|
369
|
+
// (Former R3 direct-delivered filter removed — no PTY direct-inject path exists
|
|
370
|
+
// anymore, so a peeked pending event has genuinely not yet been consumed.)
|
|
371
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, merged);
|
|
414
372
|
}
|
|
415
373
|
|
|
416
374
|
/**
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -11,13 +11,17 @@ export {
|
|
|
11
11
|
drainPendingMeshCoordinatorEvents,
|
|
12
12
|
getPendingMeshCoordinatorEvents,
|
|
13
13
|
clearPendingMeshCoordinatorEvents,
|
|
14
|
-
markMeshCoordinatorEventDirectDelivered,
|
|
15
14
|
} from './mesh-events-pending.js';
|
|
16
15
|
|
|
17
16
|
export {
|
|
18
17
|
reconcileDirectDispatchCompletionFromTranscript,
|
|
19
18
|
} from './mesh-events-stale.js';
|
|
20
19
|
|
|
20
|
+
export {
|
|
21
|
+
setupMeshReconcileLoop,
|
|
22
|
+
runMeshReconcileTick,
|
|
23
|
+
} from './mesh-reconcile-loop.js';
|
|
24
|
+
|
|
21
25
|
export type { MeshQueueTriggerResult } from './mesh-events-coordinator.js';
|
|
22
26
|
export {
|
|
23
27
|
tryAssignQueueTask,
|
|
@@ -125,6 +125,27 @@ export function getActiveMeshMissionSummaries(meshId: string): MeshMissionSummar
|
|
|
125
125
|
return getMeshMissions(meshId, ['active']).map(mission => summarizeMeshMission(meshId, mission));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Mission summaries for the mesh_status dashboard surface: every active/paused
|
|
130
|
+
* mission plus a capped, newest-first slice of completed/abandoned history so
|
|
131
|
+
* the dashboard can render a collapsible "history" section without unbounded
|
|
132
|
+
* payload growth. Returned newest-first within each group (active/paused first,
|
|
133
|
+
* then history), so the frontend can split on `status` directly.
|
|
134
|
+
*/
|
|
135
|
+
export function getMeshStatusMissionSummaries(
|
|
136
|
+
meshId: string,
|
|
137
|
+
options?: { historyLimit?: number },
|
|
138
|
+
): MeshMissionSummary[] {
|
|
139
|
+
const historyLimit = Math.max(0, options?.historyLimit ?? 10);
|
|
140
|
+
const all = getMeshMissions(meshId);
|
|
141
|
+
const live = all.filter(m => m.status === 'active' || m.status === 'paused');
|
|
142
|
+
const history = all
|
|
143
|
+
.filter(m => m.status === 'completed' || m.status === 'abandoned')
|
|
144
|
+
.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))
|
|
145
|
+
.slice(0, historyLimit);
|
|
146
|
+
return [...live, ...history].map(mission => summarizeMeshMission(meshId, mission));
|
|
147
|
+
}
|
|
148
|
+
|
|
128
149
|
/**
|
|
129
150
|
* M3-3: render active missions as a prompt section for {{mission}}.
|
|
130
151
|
* Empty string when no active mission — the prompt stays byte-identical to
|