@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.
@@ -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, markMeshCoordinatorEventDirectDelivered, } from './mesh-events-pending.js';
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.272",
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.272",
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 {
@@ -5259,27 +5259,17 @@ export class DaemonCommandRouter {
5259
5259
  }
5260
5260
 
5261
5261
  case 'launch_cli': {
5262
- // Worker launch envelope hardening: a mesh worker session must carry
5263
- // meshCoordinatorDaemonId so completion events route back to the
5264
- // coordinator (the cloud relay and the daemon-core forwarder both key on
5265
- // it). mesh_launch_session resolves coordinatorNode.daemonId || ctx.localDaemonId
5266
- // upstream, but for a freshly-cloned local worktree both can be empty. When the
5267
- // launch is a coordinator-driven worker on this machine and the id is missing,
5268
- // stamp this daemon's own id — worker and coordinator are co-located here.
5269
- {
5270
- const launchSettings = (args?.settings && typeof args.settings === 'object')
5271
- ? args.settings as Record<string, unknown>
5272
- : undefined;
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, markMeshCoordinatorEventDirectDelivered } from './mesh-events-pending.js';
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
- const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
1396
- const instState = inst.getState();
1397
- if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
1398
- if (args.sourceInstanceId && instState.instanceId === args.sourceInstanceId) return false;
1399
- // canonicalize: the worker stamps the prefixed daemon id (standalone_/daemon_) while
1400
- // localDaemonId is the raw machineId a literal !== wrongly excludes the local coordinator.
1401
- if (workerCoordinatorDaemonId && localDaemonId && !sameDaemonId(workerCoordinatorDaemonId, localDaemonId)) return false;
1402
- return true;
1403
- });
1404
-
1405
- if (coordinatorInstances.length === 0) {
1406
- // R2: the coordinator is not a live instance on THIS daemon. If it lives on a remote
1407
- // daemon, forward over the injected transport (dispatchMeshCommand, supplied by cloud as
1408
- // P2P; absent/no-op on standalone). This replaces cloud's separate
1409
- // relayRemoteMeshCoordinatorEvent listener — remote delivery is now part of the single
1410
- // core forwarder path, so local-vs-remote is one code path and standalone == cloud for
1411
- // the local case. The pending queue remains the fallback when there is no remote target
1412
- // or the transport send fails (e.g. P2P down), so MCP coordinators can still backfill.
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 MCP coordinator (mesh ${args.meshId})`);
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: coordinatorInstances.length };
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 coordinator events
1568
- // that accumulated while it was generating. This runs before the delegate routing
1569
- // below so that coordinator-own idle transitions are handled first.
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, canonicalDaemonId, readNonEmptyString, readRecord, resolveEventSessionId } from './mesh-events-utils.js';
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
- // R3: TTL for the direct-delivered marker. A coordinator polls get_pending_mesh_events
87
- // well within this window after a terminal event; after it expires the marker is swept and
88
- // a late/duplicate drain would (harmlessly) re-surface but by then the event is long gone
89
- // from the queue too. 10 minutes mirrors the completion-fingerprint TTL.
90
- const DIRECT_DELIVERED_TTL_MS = 10 * 60 * 1000;
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: when this drain is scoped to a coordinator daemon, exclude events that were already
366
- // direct-injected into that coordinator's live CLI session. Unscoped drains (no daemon id)
367
- // keep everything they belong to consumers that never received the direct inject.
368
- const deliverable = coordinatorDaemonId
369
- ? merged.filter(event => !wasDirectDeliveredToCoordinator(coordinatorDaemonId, event))
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: hide events already direct-delivered to this coordinator from its status peek, so
408
- // mesh_status doesn't report a "pending" event the coordinator has in fact already received.
409
- const deliverable = coordinatorDaemonId
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
  /**
@@ -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