@adhdev/daemon-core 0.9.82-rc.13 → 0.9.82-rc.130

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.
Files changed (82) hide show
  1. package/dist/chat/subscription-updates.d.ts +1 -0
  2. package/dist/cli-adapter-types.d.ts +4 -1
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +25 -1
  4. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  5. package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
  6. package/dist/commands/router.d.ts +22 -0
  7. package/dist/config/chat-history.d.ts +4 -0
  8. package/dist/config/mesh-config.d.ts +68 -1
  9. package/dist/git/git-commands.d.ts +5 -1
  10. package/dist/index.d.ts +15 -5
  11. package/dist/index.js +7461 -1380
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +7417 -1364
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/installer.d.ts +1 -4
  16. package/dist/launch.d.ts +1 -1
  17. package/dist/logging/async-batch-writer.d.ts +10 -0
  18. package/dist/mesh/beads-db.d.ts +18 -0
  19. package/dist/mesh/mesh-active-work.d.ts +73 -0
  20. package/dist/mesh/mesh-events.d.ts +54 -5
  21. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  22. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  23. package/dist/mesh/mesh-ledger.d.ts +38 -1
  24. package/dist/mesh/mesh-refine-status.d.ts +27 -0
  25. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  26. package/dist/mesh/preview-freshness.d.ts +18 -0
  27. package/dist/mesh/refine-config.d.ts +193 -0
  28. package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
  29. package/dist/providers/chat-message-normalization.d.ts +1 -0
  30. package/dist/providers/cli-provider-instance.d.ts +6 -1
  31. package/dist/repo-mesh-types.d.ts +62 -0
  32. package/dist/status/reporter.d.ts +2 -0
  33. package/package.json +3 -1
  34. package/src/boot/daemon-lifecycle.ts +1 -0
  35. package/src/chat/subscription-updates.ts +5 -1
  36. package/src/cli-adapter-types.ts +2 -1
  37. package/src/cli-adapters/provider-cli-adapter.ts +473 -18
  38. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  39. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  40. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  41. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  42. package/src/cli-adapters/provider-cli-shared.ts +32 -10
  43. package/src/commands/chat-commands.ts +960 -40
  44. package/src/commands/cli-manager.ts +138 -2
  45. package/src/commands/handler.ts +8 -1
  46. package/src/commands/mesh-coordinator.ts +13 -143
  47. package/src/commands/router.ts +3238 -423
  48. package/src/config/chat-history.ts +37 -9
  49. package/src/config/mesh-config.ts +249 -2
  50. package/src/config/recent-activity.ts +8 -2
  51. package/src/daemon/dev-cli-debug.ts +10 -1
  52. package/src/detection/ide-detector.ts +26 -16
  53. package/src/git/git-commands.ts +17 -5
  54. package/src/index.ts +41 -4
  55. package/src/installer.d.ts +1 -1
  56. package/src/installer.ts +8 -6
  57. package/src/launch.d.ts +1 -1
  58. package/src/launch.ts +37 -28
  59. package/src/logging/async-batch-writer.ts +55 -0
  60. package/src/logging/logger.ts +2 -1
  61. package/src/mesh/beads-db.ts +176 -0
  62. package/src/mesh/coordinator-prompt.ts +31 -8
  63. package/src/mesh/mesh-active-work.ts +295 -0
  64. package/src/mesh/mesh-events.ts +595 -48
  65. package/src/mesh/mesh-fast-forward.ts +430 -0
  66. package/src/mesh/mesh-host-ownership.ts +73 -0
  67. package/src/mesh/mesh-ledger.ts +138 -1
  68. package/src/mesh/mesh-refine-status.ts +145 -0
  69. package/src/mesh/mesh-work-queue.ts +199 -137
  70. package/src/mesh/preview-freshness.ts +118 -0
  71. package/src/mesh/refine-config.ts +366 -0
  72. package/src/mesh/worktree-bootstrap-config.ts +234 -0
  73. package/src/providers/approval-utils.ts +12 -5
  74. package/src/providers/chat-message-normalization.ts +7 -12
  75. package/src/providers/cli-provider-instance.ts +289 -36
  76. package/src/providers/ide-provider-instance.ts +17 -3
  77. package/src/providers/provider-loader.ts +10 -4
  78. package/src/providers/read-chat-contract.ts +1 -1
  79. package/src/providers/version-archive.ts +38 -20
  80. package/src/repo-mesh-types.ts +67 -0
  81. package/src/status/reporter.ts +15 -0
  82. package/src/system/host-memory.ts +29 -12
@@ -1,31 +1,51 @@
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 readWorkerResultMetadata(event: Record<string, unknown>): Record<string, unknown> | undefined {
29
+ return readRecord(event.workerResult) || readRecord(event.meshWorkerResult) || readRecord(event.structuredResult);
30
+ }
31
+
32
+ function sweepExpiredRemoteIdleSessions(): void {
33
+ const now = Date.now();
34
+ for (const [key, session] of remoteIdleSessions) {
35
+ if (session.expiresAt <= now) remoteIdleSessions.delete(key);
36
+ }
37
+ }
38
+
23
39
  // ---------------------------------------------------------------------------
24
- // MCP coordinator pending-event queue
40
+ // MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
25
41
  // ---------------------------------------------------------------------------
26
42
  // When a mesh event fires but no CLI coordinator session is registered (e.g.
27
- // the coordinator is Claude Code running via MCP), we buffer the event here.
28
- // The MCP server drains this queue on every mesh_status / mesh_send_task poll.
43
+ // the coordinator is Claude Code running via MCP), we persist the event to a
44
+ // per-mesh JSONL file so it survives daemon restarts. The 50-entry hard cap
45
+ // is removed; the file is drained atomically on each get_pending_mesh_events
46
+ // call and limited to 100 KB to prevent runaway growth.
47
+ //
48
+ // File: <ledgerDir>/<meshId>.pending-events.jsonl
29
49
  // ---------------------------------------------------------------------------
30
50
 
31
51
  export interface PendingMeshCoordinatorEvent {
@@ -35,39 +55,198 @@ export interface PendingMeshCoordinatorEvent {
35
55
  nodeId?: string;
36
56
  workspace?: string;
37
57
  metadataEvent: Record<string, unknown>;
58
+ coordinatorMessage?: string;
38
59
  queuedAt: number;
39
60
  }
40
61
 
41
- const MAX_PENDING_EVENTS = 50;
42
- const pendingMeshCoordinatorEvents: PendingMeshCoordinatorEvent[] = [];
62
+ const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
63
+
64
+ function readRefineJobId(event: { metadataEvent?: Record<string, unknown> } | Record<string, unknown>): string {
65
+ const metadata = readRecord((event as any).metadataEvent) || event as Record<string, unknown>;
66
+ const result = readRecord(metadata.result);
67
+ const refineJob = readRecord(result?.refineJob);
68
+ return readNonEmptyString(metadata.jobId) || readNonEmptyString(refineJob?.jobId);
69
+ }
70
+
71
+ function buildRefineTerminalEventFingerprint(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): string {
72
+ const jobId = readRefineJobId({ metadataEvent });
73
+ return jobId && REFINE_TERMINAL_EVENTS.has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
74
+ }
75
+
76
+ function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
77
+ if (!REFINE_TERMINAL_EVENTS.has(event.event)) return false;
78
+ const jobId = readRefineJobId(event);
79
+ if (!jobId) return false;
80
+ return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) =>
81
+ pending.event === event.event && readRefineJobId(pending) === jobId,
82
+ );
83
+ }
84
+
85
+ function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string {
86
+ const metadata = readRecord(event.metadataEvent) || {};
87
+ const sessionId = resolveEventSessionId(metadata);
88
+ const providerSessionId = readNonEmptyString(metadata.providerSessionId);
89
+ const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
90
+ const jobId = readRefineJobId(event);
91
+ const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
92
+ return [
93
+ event.meshId,
94
+ event.event,
95
+ event.nodeId || '',
96
+ sessionId || '',
97
+ providerSessionId || '',
98
+ taskId || '',
99
+ jobId || '',
100
+ timestamp || '',
101
+ ].join('::');
102
+ }
103
+
104
+ function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
105
+ const fingerprint = buildPendingEventFingerprint(event);
106
+ if (!fingerprint.trim()) return false;
107
+ return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
108
+ }
109
+
110
+ function getPendingEventsPath(meshId: string): string {
111
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
112
+ return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
113
+ }
114
+
115
+ function readPendingMeshCoordinatorEventsFromDisk(meshId?: string): PendingMeshCoordinatorEvent[] {
116
+ if (!meshId) return [];
117
+ const path = getPendingEventsPath(meshId);
118
+ if (!existsSync(path)) return [];
119
+ try {
120
+ const raw = readFileSync(path, 'utf-8');
121
+ return raw.split('\n').filter(Boolean).flatMap(line => {
122
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
123
+ });
124
+ } catch { return []; }
125
+ }
126
+
127
+ function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
128
+ const acceptedJobIds = new Set(
129
+ pending
130
+ .filter(event => event.event === 'refine:accepted')
131
+ .map(event => readRefineJobId(event))
132
+ .filter(Boolean),
133
+ );
134
+ if (acceptedJobIds.size === 0) return [];
135
+ const existingTerminalJobIds = new Set(
136
+ pending
137
+ .filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
138
+ .map(event => `${event.event}:${readRefineJobId(event)}`)
139
+ .filter(value => !value.endsWith(':')),
140
+ );
141
+ const backfilled: PendingMeshCoordinatorEvent[] = [];
142
+ const entries = readLedgerEntries(meshId);
143
+ for (let i = entries.length - 1; i >= 0; i--) {
144
+ const entry = entries[i];
145
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
146
+ const payload = readRecord(entry.payload);
147
+ if (payload?.source !== 'refine_mesh_node_async_job') continue;
148
+ const refineJob = readRecord(payload.refineJob);
149
+ const jobId = readNonEmptyString(refineJob?.jobId);
150
+ if (!jobId || !acceptedJobIds.has(jobId)) continue;
151
+ const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
152
+ if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
153
+ existingTerminalJobIds.add(`${eventName}:${jobId}`);
154
+ const result = readRecord(payload.result);
155
+ const metadataEvent = {
156
+ source: 'refine_mesh_node_async_job',
157
+ jobId,
158
+ interactionId: readNonEmptyString(refineJob?.interactionId),
159
+ meshId,
160
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
161
+ targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
162
+ workspace: readNonEmptyString(refineJob?.workspace),
163
+ status: eventName === 'refine:completed' ? 'completed' : 'failed',
164
+ startedAt: readNonEmptyString(refineJob?.startedAt),
165
+ completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
166
+ retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
167
+ ...(result ? { result } : {}),
168
+ };
169
+ backfilled.push({
170
+ event: eventName,
171
+ meshId,
172
+ nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
173
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
174
+ workspace: readNonEmptyString(refineJob?.workspace),
175
+ metadataEvent,
176
+ coordinatorMessage: buildMeshSystemMessage({
177
+ event: eventName,
178
+ nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
179
+ metadataEvent,
180
+ }),
181
+ queuedAt: Date.now(),
182
+ });
183
+ }
184
+ return backfilled.reverse();
185
+ }
186
+
187
+ function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
188
+ const backfilled = refineTerminalEventFromLedger(meshId, events);
189
+ if (backfilled.length === 0) return events;
190
+ const terminalJobIds = new Set(backfilled.map(event => readRefineJobId(event)).filter(Boolean));
191
+ return [
192
+ ...events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event)))),
193
+ ...backfilled,
194
+ ];
195
+ }
43
196
 
44
197
  export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
45
- if (pendingMeshCoordinatorEvents.length >= MAX_PENDING_EVENTS) {
198
+ try {
199
+ if (hasPendingRefineTerminalEventDuplicate(event)) {
200
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
201
+ return true;
202
+ }
203
+ if (hasPendingCoordinatorEventDuplicate(event)) {
204
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
205
+ return true;
206
+ }
207
+ appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
208
+ return true;
209
+ } catch (e: any) {
210
+ LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
46
211
  return false;
47
212
  }
48
- pendingMeshCoordinatorEvents.push(event);
49
- return true;
50
213
  }
51
214
 
52
- /** Drain and return all pending coordinator events, clearing the queue. */
53
- export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
54
- return pendingMeshCoordinatorEvents.splice(0);
215
+ /** Drain and return all pending coordinator events for meshId, removing them from disk. */
216
+ export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
217
+ if (!meshId) return [];
218
+ const path = getPendingEventsPath(meshId);
219
+ if (!existsSync(path)) return [];
220
+ try {
221
+ const parsed = readPendingMeshCoordinatorEventsFromDisk(meshId);
222
+ try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
223
+ return reconcilePendingMeshCoordinatorEvents(meshId, parsed);
224
+ } catch { return []; }
55
225
  }
56
226
 
57
227
  /** Peek at pending coordinator events without draining (non-destructive). */
58
- export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
59
- return pendingMeshCoordinatorEvents.slice();
228
+ export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
229
+ if (!meshId) return [];
230
+ return reconcilePendingMeshCoordinatorEvents(meshId, readPendingMeshCoordinatorEventsFromDisk(meshId));
60
231
  }
61
232
 
62
- /** Explicitly clear all pending coordinator events. */
63
- export function clearPendingMeshCoordinatorEvents(): void {
64
- pendingMeshCoordinatorEvents.splice(0);
233
+ /** Explicitly clear all pending coordinator events for a mesh. */
234
+ export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
235
+ if (!meshId) return;
236
+ const path = getPendingEventsPath(meshId);
237
+ if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
65
238
  }
66
239
 
67
240
  function readNonEmptyString(value: unknown): string {
68
241
  return typeof value === 'string' && value.trim() ? value.trim() : '';
69
242
  }
70
243
 
244
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
245
+ return value && typeof value === 'object' && !Array.isArray(value)
246
+ ? value as Record<string, unknown>
247
+ : undefined;
248
+ }
249
+
71
250
  function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
72
251
  return readNonEmptyString(event.targetSessionId)
73
252
  || readNonEmptyString(event.sessionId)
@@ -82,6 +261,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
82
261
  'agent:stopped',
83
262
  'agent:ready',
84
263
  'monitor:long_generating',
264
+ 'refine:accepted',
265
+ 'refine:completed',
266
+ 'refine:failed',
85
267
  ]);
86
268
 
87
269
  const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
@@ -96,10 +278,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
96
278
  }
97
279
 
98
280
  function formatCompletionMetadata(event: Record<string, unknown>): string {
281
+ const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
282
+ ? event.completionDiagnostic as Record<string, unknown>
283
+ : null;
284
+ const diagnosticReason = completionDiagnostic
285
+ ? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
286
+ : '';
287
+ const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
288
+ ? String(completionDiagnostic.finalAssistantPresent)
289
+ : '';
99
290
  const parts = [
100
291
  readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
101
292
  readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
102
293
  readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
294
+ diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
295
+ finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
103
296
  ].filter(Boolean);
104
297
  return parts.length > 0 ? ` (${parts.join('; ')})` : '';
105
298
  }
@@ -150,6 +343,148 @@ function shouldSuppressIntentionalCleanupStop(args: {
150
343
  return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
151
344
  }
152
345
 
346
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
347
+ const recentCompletionFingerprints = new Map<string, number>();
348
+
349
+ function readEventTimestamp(value: unknown): number | null {
350
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
351
+ if (typeof value === 'string' && value.trim()) {
352
+ const numeric = Number(value);
353
+ if (Number.isFinite(numeric)) return numeric;
354
+ const parsed = Date.parse(value);
355
+ if (Number.isFinite(parsed)) return parsed;
356
+ }
357
+ return null;
358
+ }
359
+
360
+ function buildMeshCompletionFingerprint(args: {
361
+ meshId: string;
362
+ event: string;
363
+ sessionId: string;
364
+ providerType?: string;
365
+ providerSessionId?: string;
366
+ timestamp?: number | null;
367
+ finalSummary?: string;
368
+ }): string {
369
+ const timestampPart = Number.isFinite(args.timestamp)
370
+ ? String(args.timestamp)
371
+ : readNonEmptyString(args.finalSummary).slice(0, 200);
372
+ return [
373
+ args.meshId,
374
+ args.event,
375
+ args.sessionId,
376
+ args.providerType || '',
377
+ args.providerSessionId || '',
378
+ timestampPart,
379
+ ].join('::');
380
+ }
381
+
382
+ function isDuplicateMeshCompletionEvent(args: {
383
+ meshId: string;
384
+ event: string;
385
+ sessionId: string;
386
+ providerType?: string;
387
+ providerSessionId?: string;
388
+ timestamp?: number | null;
389
+ finalSummary?: string;
390
+ }): boolean {
391
+ const fingerprint = buildMeshCompletionFingerprint(args);
392
+ if (!fingerprint) return false;
393
+ const now = Date.now();
394
+ for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
395
+ if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
396
+ }
397
+ if (recentCompletionFingerprints.has(fingerprint)) return true;
398
+ recentCompletionFingerprints.set(fingerprint, now);
399
+ return false;
400
+ }
401
+
402
+ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
403
+ const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
404
+ if (!fingerprint) return false;
405
+ const now = Date.now();
406
+ for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
407
+ if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
408
+ }
409
+ if (recentCompletionFingerprints.has(fingerprint)) return true;
410
+ recentCompletionFingerprints.set(fingerprint, now);
411
+ return false;
412
+ }
413
+
414
+ function findRecentTerminalLedgerEvidence(args: {
415
+ meshId: string;
416
+ sessionId?: string;
417
+ nodeId?: string;
418
+ }): { kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
419
+ if (!args.sessionId && !args.nodeId) return null;
420
+ const entries = readLedgerEntries(args.meshId);
421
+ for (let i = entries.length - 1; i >= 0; i--) {
422
+ const entry = entries[i];
423
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
424
+ if (args.sessionId && entry.sessionId === args.sessionId) {
425
+ return { kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
426
+ }
427
+ if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
428
+ return { kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
429
+ }
430
+ }
431
+ return null;
432
+ }
433
+
434
+ function buildLongGeneratingCompletionReconciliation(args: {
435
+ meshId: string;
436
+ nodeId?: string;
437
+ nodeLabel: string;
438
+ metadataEvent: Record<string, unknown>;
439
+ sourceInstanceId?: string;
440
+ }): Record<string, unknown> | null {
441
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
442
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
443
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
444
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
445
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
446
+ const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
447
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
448
+ const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
449
+ const explicitCompletionEvidence = Boolean(
450
+ finalSummary
451
+ || workerResult
452
+ || completionDiagnostic?.finalAssistantPresent === true
453
+ || status === 'idle'
454
+ || status === 'ready'
455
+ || status === 'completed',
456
+ );
457
+ if (explicitCompletionEvidence) {
458
+ return {
459
+ ...args.metadataEvent,
460
+ targetSessionId: sessionId,
461
+ providerType,
462
+ providerSessionId,
463
+ finalSummary,
464
+ source: 'long_generating_reconciliation',
465
+ reconciledFromEvent: 'monitor:long_generating',
466
+ timestamp: args.metadataEvent.timestamp ?? Date.now(),
467
+ completionDiagnostic: {
468
+ ...(completionDiagnostic || {}),
469
+ reconciliationReason: 'provider_completion_evidence',
470
+ },
471
+ };
472
+ }
473
+
474
+ const terminal = findRecentTerminalLedgerEvidence({
475
+ meshId: args.meshId,
476
+ sessionId: sessionId || undefined,
477
+ nodeId: nodeId || undefined,
478
+ });
479
+ if (!terminal) return null;
480
+ return {
481
+ ...args.metadataEvent,
482
+ source: 'long_generating_terminal_ledger_suppression',
483
+ terminalLedgerKind: terminal.kind,
484
+ terminalLedgerAt: terminal.timestamp,
485
+ };
486
+ }
487
+
153
488
 
154
489
  export function tryAssignQueueTask(
155
490
  components: DaemonComponents,
@@ -180,7 +515,16 @@ export function tryAssignQueueTask(
180
515
  message: task.message,
181
516
  }).catch((e: any) => {
182
517
  LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
183
- updateTaskStatus(meshId, task.id, 'failed');
518
+ // Revert to pending so the task can be retried rather than permanently failing
519
+ updateTaskStatus(meshId, task.id, 'pending');
520
+ try {
521
+ appendLedgerEntry(meshId, {
522
+ kind: 'dispatch_failed' as any,
523
+ nodeId,
524
+ sessionId,
525
+ payload: { taskId: task.id, error: e?.message, retryable: true },
526
+ });
527
+ } catch { /* ledger write is best-effort */ }
184
528
  });
185
529
  return true;
186
530
  }
@@ -277,6 +621,10 @@ function nodeHasActiveAssignment(meshId: string, nodeId: string): boolean {
277
621
  return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedNodeId === nodeId);
278
622
  }
279
623
 
624
+ function sessionHasActiveAssignment(meshId: string, sessionId: string): boolean {
625
+ return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedSessionId === sessionId);
626
+ }
627
+
280
628
  function liveSessionCountForNode(components: DaemonComponents, meshId: string, nodeId: string): number {
281
629
  return components.instanceManager.getByCategory('cli').filter((inst: any) => {
282
630
  const state = inst.getState();
@@ -542,6 +890,9 @@ function buildMeshSystemMessage(args: {
542
890
  }): string {
543
891
  const metadata = formatCompletionMetadata(args.metadataEvent);
544
892
  if (args.event === 'agent:generating_completed') {
893
+ if (args.metadataEvent.source === 'long_generating_reconciliation') {
894
+ return `[System] ${args.nodeLabel} already has completion evidence${metadata}. The long-generating monitor reconciled the terminal handoff and marked the session complete; wait for the queued completion event/status refresh before doing any manual transcript check.`;
895
+ }
545
896
  return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. This completion came from the agent status event path; use mesh_read_chat once to review its final progress, but do not poll repeatedly.`;
546
897
  }
547
898
  if (args.event === 'agent:waiting_approval') {
@@ -573,7 +924,72 @@ function buildMeshSystemMessage(args: {
573
924
  return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
574
925
  }
575
926
  if (args.event === 'monitor:long_generating') {
576
- return `[System] ${args.nodeLabel} has been generating for a long time${metadata}. Use mesh_read_chat once for a status check, but do not poll repeatedly.`;
927
+ return `[System] ${args.nodeLabel} is still reported as generating after a long interval${metadata}. Wait for pendingCoordinatorEvents or a completion/status event; if the user explicitly asks for status, make one bounded status check and then wait again.`;
928
+ }
929
+ if (args.event === 'refine:accepted') {
930
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
931
+ return `[System] Refinery accepted async job${jobId ? ` ${jobId}` : ''} for ${args.nodeLabel}. Completion/failure will be delivered as a terminal refine event; do not poll repeatedly.`;
932
+ }
933
+ if (args.event === 'refine:completed') {
934
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
935
+ const result = readRecord(args.metadataEvent.result);
936
+ const validationSummary = readRecord(result?.validationSummary);
937
+ const patchEquivalence = readRecord(result?.patchEquivalence);
938
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
939
+ const validationStatus = readNonEmptyString(validationSummary?.status);
940
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
941
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
942
+ const into = readNonEmptyString(result?.into);
943
+ const branch = readNonEmptyString(result?.branch);
944
+ const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
945
+ const convergenceStatus = readNonEmptyString(finalConvergence?.status);
946
+ const nextStep = readNonEmptyString(result?.nextStep)
947
+ || readNonEmptyString(finalConvergence?.nextStep)
948
+ || 'Continue from the updated mesh state.';
949
+ const details = [
950
+ jobId ? `job_id=${jobId}` : '',
951
+ branch && into ? `${branch}→${into}` : '',
952
+ validationStatus ? `validation=${validationStatus}` : '',
953
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
954
+ mergeStatus ? `merge=${mergeStatus}` : '',
955
+ convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
956
+ ].filter(Boolean).join('; ');
957
+ return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
958
+ }
959
+ if (args.event === 'refine:failed') {
960
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
961
+ const result = readRecord(args.metadataEvent.result);
962
+ const validationSummary = readRecord(result?.validationSummary);
963
+ const patchEquivalence = readRecord(result?.patchEquivalence);
964
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
965
+ const code = readNonEmptyString(result?.code);
966
+ const error = readNonEmptyString(result?.error);
967
+ const validationStatus = readNonEmptyString(validationSummary?.status);
968
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
969
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
970
+ const mergeStatus = result?.merged === true
971
+ ? 'merged'
972
+ : finalConvergence?.merged === false
973
+ ? 'not_merged'
974
+ : '';
975
+ const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
976
+ || readNonEmptyString(finalConvergence?.status);
977
+ const blockedReason = readNonEmptyString(result?.blockedReason);
978
+ const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
979
+ const details = [
980
+ jobId ? `job_id=${jobId}` : '',
981
+ code ? `code=${code}` : '',
982
+ validationStatus ? `validation=${validationStatus}` : '',
983
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
984
+ mergeStatus ? `merge=${mergeStatus}` : '',
985
+ convergenceStatus ? `convergence=${convergenceStatus}` : '',
986
+ blockedReason ? `reason=${blockedReason}` : '',
987
+ ].filter(Boolean).join('; ');
988
+ const parts = [
989
+ `[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
990
+ nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
991
+ ];
992
+ return parts.join('\n');
577
993
  }
578
994
  return '';
579
995
  }
@@ -603,6 +1019,75 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
603
1019
  return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
604
1020
  }
605
1021
 
1022
+ if (args.event === 'monitor:long_generating') {
1023
+ const reconciledCompletion = buildLongGeneratingCompletionReconciliation({
1024
+ meshId: args.meshId,
1025
+ nodeId: args.nodeId,
1026
+ nodeLabel: args.nodeLabel,
1027
+ metadataEvent: args.metadataEvent,
1028
+ sourceInstanceId: args.sourceInstanceId,
1029
+ });
1030
+ if (reconciledCompletion?.source === 'long_generating_reconciliation') {
1031
+ LOG.info('MeshEvents', `Reconciled long-generating monitor to completion for session ${eventSessionId || '(unknown session)'}`);
1032
+ return injectMeshSystemMessage(components, {
1033
+ ...args,
1034
+ event: 'agent:generating_completed',
1035
+ metadataEvent: reconciledCompletion,
1036
+ });
1037
+ }
1038
+ if (reconciledCompletion?.source === 'long_generating_terminal_ledger_suppression') {
1039
+ LOG.info('MeshEvents', `Suppressed long-generating monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
1040
+ return {
1041
+ success: true,
1042
+ forwarded: 0,
1043
+ suppressed: true,
1044
+ terminalLedgerEvidence: true,
1045
+ terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
1046
+ };
1047
+ }
1048
+ }
1049
+
1050
+ if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
1051
+ LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
1052
+ return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
1053
+ }
1054
+
1055
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
1056
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
1057
+ const terminal = findRecentTerminalLedgerEvidence({
1058
+ meshId: args.meshId,
1059
+ sessionId: eventSessionId,
1060
+ nodeId: eventNodeId || undefined,
1061
+ });
1062
+ if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
1063
+ const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
1064
+ const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
1065
+ const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
1066
+ const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
1067
+ if (
1068
+ (terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
1069
+ || (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
1070
+ || args.metadataEvent.source === 'long_generating_reconciliation'
1071
+ ) {
1072
+ LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
1073
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
1074
+ }
1075
+ }
1076
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
1077
+ meshId: args.meshId,
1078
+ event: args.event,
1079
+ sessionId: eventSessionId,
1080
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1081
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
1082
+ timestamp: eventTimestamp,
1083
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1084
+ });
1085
+ if (duplicateCompletion) {
1086
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
1087
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
1088
+ }
1089
+ }
1090
+
606
1091
  // ── Task Queue & Ledger ──
607
1092
  let completedTaskForLedger: { id?: string } | null = null;
608
1093
  if (args.event === 'agent:generating_completed') {
@@ -611,20 +1096,27 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
611
1096
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
612
1097
 
613
1098
  if (sessionId) {
614
- const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
1099
+ const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
1100
+ occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
1101
+ });
615
1102
  completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
616
1103
  if (nodeId && providerType) {
617
- // Short delay to allow completion event to propagate before pulling next
618
- setTimeout(() => {
1104
+ // Queue state is already updated above; setImmediate avoids the
1105
+ // 500 ms artificial delay while still deferring past this call frame.
1106
+ setImmediate(() => {
619
1107
  tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
620
- }, 500);
1108
+ });
621
1109
  }
622
1110
  }
623
1111
  } else if (args.event === 'agent:ready') {
624
1112
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
625
1113
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
626
1114
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
627
- const completedTask = sessionId
1115
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1116
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1117
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
1118
+ const hasCompletionEvidence = !!finalSummary || !!workerResult;
1119
+ const completedTask = sessionId && hasCompletionEvidence
628
1120
  ? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
629
1121
  : null;
630
1122
  if (completedTask) {
@@ -640,15 +1132,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
640
1132
  nodeLabel: args.nodeLabel,
641
1133
  taskId: completedTask.id,
642
1134
  completedViaReady: true,
643
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
644
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1135
+ providerSessionId,
1136
+ finalSummary,
1137
+ workerResult,
645
1138
  evidence: buildTaskCompletionEvidence({
646
1139
  event: 'agent:ready',
647
1140
  nodeId,
648
1141
  sessionId,
649
1142
  providerType: providerType || undefined,
650
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
651
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1143
+ providerSessionId,
1144
+ finalSummary,
1145
+ workerResult,
652
1146
  }),
653
1147
  },
654
1148
  });
@@ -658,13 +1152,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
658
1152
  }
659
1153
 
660
1154
  if (sessionId && nodeId && providerType) {
661
- remoteIdleSessions.set(`${nodeId}:${sessionId}`, { nodeId, sessionId, providerType });
662
- setTimeout(() => {
1155
+ sweepExpiredRemoteIdleSessions();
1156
+ remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
1157
+ nodeId, sessionId, providerType,
1158
+ expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
1159
+ });
1160
+ setImmediate(() => {
663
1161
  const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
664
- if (assigned) {
665
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
666
- }
667
- }, 500);
1162
+ if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
1163
+ });
668
1164
  }
669
1165
  } else if (args.event === 'agent:generating_started') {
670
1166
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
@@ -679,7 +1175,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
679
1175
  remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
680
1176
  }
681
1177
  if (sessionId) {
682
- updateSessionTaskStatus(args.meshId, sessionId, 'failed');
1178
+ const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
1179
+ completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
683
1180
  }
684
1181
  }
685
1182
 
@@ -689,14 +1186,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
689
1186
  const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
690
1187
  const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
691
1188
  const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
1189
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1190
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1191
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
692
1192
  const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
693
1193
  ? buildTaskCompletionEvidence({
694
1194
  event: 'agent:generating_completed',
695
1195
  nodeId: ledgerNodeId,
696
1196
  sessionId: ledgerSessionId,
697
1197
  providerType: ledgerProviderType,
698
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
699
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1198
+ providerSessionId,
1199
+ finalSummary,
1200
+ workerResult,
700
1201
  })
701
1202
  : undefined;
702
1203
  appendLedgerEntry(args.meshId, {
@@ -708,8 +1209,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
708
1209
  event: args.event,
709
1210
  nodeLabel: args.nodeLabel,
710
1211
  taskId: completedTaskForLedger?.id || undefined,
711
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
712
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1212
+ providerSessionId,
1213
+ finalSummary,
1214
+ workerResult,
1215
+ completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
1216
+ ? args.metadataEvent.completionDiagnostic
1217
+ : undefined,
713
1218
  evidence: completionEvidence,
714
1219
  },
715
1220
  });
@@ -782,6 +1287,14 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
782
1287
  }
783
1288
  }
784
1289
 
1290
+ const messageText = buildMeshSystemMessage({
1291
+ event: args.event,
1292
+ nodeLabel: args.nodeLabel,
1293
+ metadataEvent: args.metadataEvent,
1294
+ recoveryContext,
1295
+ });
1296
+ if (!messageText) return { success: false, error: 'unsupported mesh event' };
1297
+
785
1298
  const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
786
1299
  const instState = inst.getState();
787
1300
  if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
@@ -789,6 +1302,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
789
1302
  return true;
790
1303
  });
791
1304
 
1305
+ // Refine terminal events (refine:completed, refine:failed) are coordinator-delivered
1306
+ // synchronously; only buffer them for MCP when no CLI coordinator is present.
1307
+ // Agent runtime events (agent:*) use dual delivery so both CLI and MCP coordinators
1308
+ // receive them regardless of whether a live CLI coordinator session is active.
1309
+ const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
1310
+
792
1311
  if (coordinatorInstances.length === 0) {
793
1312
  // No CLI coordinator session found — buffer for MCP-based coordinators.
794
1313
  if (queuePendingMeshCoordinatorEvent({
@@ -801,6 +1320,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
801
1320
  ...args.metadataEvent,
802
1321
  ...(recoveryContext ? { recoveryContext } : {}),
803
1322
  },
1323
+ coordinatorMessage: messageText,
804
1324
  queuedAt: Date.now(),
805
1325
  })) {
806
1326
  LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
@@ -808,13 +1328,27 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
808
1328
  return { success: true, forwarded: 0 };
809
1329
  }
810
1330
 
811
- const messageText = buildMeshSystemMessage({
812
- event: args.event,
813
- nodeLabel: args.nodeLabel,
814
- metadataEvent: args.metadataEvent,
815
- recoveryContext,
816
- });
817
- if (!messageText) return { success: false, error: 'unsupported mesh event' };
1331
+ // CLI coordinator is present. For non-refine events, also buffer for MCP coordinators
1332
+ // that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
1333
+ // forwarded directly only — they must not accumulate in the pending queue when a live
1334
+ // coordinator already received them.
1335
+ if (!isRefineTerminalEvent) {
1336
+ if (queuePendingMeshCoordinatorEvent({
1337
+ event: args.event,
1338
+ meshId: args.meshId,
1339
+ nodeLabel: args.nodeLabel,
1340
+ nodeId: args.nodeId || undefined,
1341
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
1342
+ metadataEvent: {
1343
+ ...args.metadataEvent,
1344
+ ...(recoveryContext ? { recoveryContext } : {}),
1345
+ },
1346
+ coordinatorMessage: messageText,
1347
+ queuedAt: Date.now(),
1348
+ })) {
1349
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
1350
+ }
1351
+ }
818
1352
 
819
1353
  for (const coord of coordinatorInstances) {
820
1354
  const coordState = coord.getState();
@@ -845,6 +1379,19 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
845
1379
  providerType: readNonEmptyString(payload.providerType),
846
1380
  providerSessionId: readNonEmptyString(payload.providerSessionId),
847
1381
  finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
1382
+ jobId: readNonEmptyString(payload.jobId),
1383
+ interactionId: readNonEmptyString(payload.interactionId),
1384
+ status: readNonEmptyString(payload.status),
1385
+ targetDaemonId: readNonEmptyString(payload.targetDaemonId),
1386
+ startedAt: readNonEmptyString(payload.startedAt),
1387
+ completedAt: readNonEmptyString(payload.completedAt),
1388
+ retryOfJobId: readNonEmptyString(payload.retryOfJobId),
1389
+ ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
1390
+ ...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
1391
+ ...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
1392
+ ...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
1393
+ ...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
1394
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
848
1395
  intentional: payload.intentional === true,
849
1396
  intentionalStop: payload.intentionalStop === true,
850
1397
  operatorCleanup: payload.operatorCleanup === true,