@adhdev/daemon-core 0.9.82-rc.8 → 0.9.82-rc.80

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 (73) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  2. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
  4. package/dist/commands/router.d.ts +22 -0
  5. package/dist/config/mesh-config.d.ts +66 -1
  6. package/dist/git/git-commands.d.ts +1 -0
  7. package/dist/git/git-status.d.ts +5 -0
  8. package/dist/git/git-types.d.ts +10 -0
  9. package/dist/index.d.ts +13 -6
  10. package/dist/index.js +4936 -1171
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +4900 -1157
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/installer.d.ts +1 -4
  15. package/dist/launch.d.ts +1 -1
  16. package/dist/logging/async-batch-writer.d.ts +10 -0
  17. package/dist/mesh/beads-db.d.ts +18 -0
  18. package/dist/mesh/mesh-active-work.d.ts +51 -0
  19. package/dist/mesh/mesh-events.d.ts +29 -5
  20. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  21. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  22. package/dist/mesh/mesh-ledger.d.ts +38 -1
  23. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  24. package/dist/mesh/refine-config.d.ts +119 -0
  25. package/dist/providers/chat-message-normalization.d.ts +1 -0
  26. package/dist/providers/cli-provider-instance.d.ts +2 -1
  27. package/dist/repo-mesh-types.d.ts +39 -0
  28. package/dist/status/reporter.d.ts +2 -0
  29. package/package.json +3 -1
  30. package/src/boot/daemon-lifecycle.ts +1 -0
  31. package/src/cli-adapters/provider-cli-adapter.ts +91 -3
  32. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  33. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  34. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  35. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  36. package/src/cli-adapters/provider-cli-shared.ts +20 -10
  37. package/src/commands/chat-commands.ts +242 -7
  38. package/src/commands/cli-manager.ts +63 -0
  39. package/src/commands/handler.ts +8 -1
  40. package/src/commands/mesh-coordinator.ts +13 -143
  41. package/src/commands/router.ts +2435 -414
  42. package/src/config/chat-history.ts +9 -7
  43. package/src/config/mesh-config.ts +244 -1
  44. package/src/daemon/dev-cli-debug.ts +10 -1
  45. package/src/detection/ide-detector.ts +26 -16
  46. package/src/git/git-commands.ts +3 -3
  47. package/src/git/git-status.ts +97 -6
  48. package/src/git/git-summary.ts +3 -0
  49. package/src/git/git-types.ts +11 -0
  50. package/src/index.ts +31 -5
  51. package/src/installer.d.ts +1 -1
  52. package/src/installer.ts +8 -6
  53. package/src/launch.d.ts +1 -1
  54. package/src/launch.ts +37 -28
  55. package/src/logging/async-batch-writer.ts +55 -0
  56. package/src/logging/logger.ts +2 -1
  57. package/src/mesh/beads-db.ts +176 -0
  58. package/src/mesh/coordinator-prompt.ts +27 -7
  59. package/src/mesh/mesh-active-work.ts +222 -0
  60. package/src/mesh/mesh-events.ts +342 -47
  61. package/src/mesh/mesh-fast-forward.ts +430 -0
  62. package/src/mesh/mesh-host-ownership.ts +73 -0
  63. package/src/mesh/mesh-ledger.ts +138 -1
  64. package/src/mesh/mesh-work-queue.ts +199 -137
  65. package/src/mesh/refine-config.ts +306 -0
  66. package/src/providers/chat-message-normalization.ts +3 -1
  67. package/src/providers/cli-provider-instance.ts +91 -13
  68. package/src/providers/ide-provider-instance.ts +17 -3
  69. package/src/providers/provider-loader.ts +10 -4
  70. package/src/providers/version-archive.ts +38 -20
  71. package/src/repo-mesh-types.ts +43 -0
  72. package/src/status/reporter.ts +15 -0
  73. package/src/system/host-memory.ts +29 -12
@@ -1,63 +1,150 @@
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 {
32
52
  event: string;
33
53
  meshId: string;
34
54
  nodeLabel: string;
55
+ nodeId?: string;
56
+ workspace?: string;
35
57
  metadataEvent: Record<string, unknown>;
58
+ coordinatorMessage?: string;
36
59
  queuedAt: number;
37
60
  }
38
61
 
39
- const MAX_PENDING_EVENTS = 50;
40
- 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 getPendingMeshCoordinatorEvents(event.meshId).some((pending) =>
81
+ pending.event === event.event && readRefineJobId(pending) === jobId,
82
+ );
83
+ }
41
84
 
42
- /** Drain and return all pending coordinator events, clearing the queue. */
43
- export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
44
- return pendingMeshCoordinatorEvents.splice(0);
85
+ function getPendingEventsPath(meshId: string): string {
86
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
87
+ return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
88
+ }
89
+
90
+ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
91
+ try {
92
+ if (hasPendingRefineTerminalEventDuplicate(event)) {
93
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
94
+ return true;
95
+ }
96
+ appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
97
+ return true;
98
+ } catch (e: any) {
99
+ LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
100
+ return false;
101
+ }
102
+ }
103
+
104
+ /** Drain and return all pending coordinator events for meshId, removing them from disk. */
105
+ export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
106
+ if (!meshId) return [];
107
+ const path = getPendingEventsPath(meshId);
108
+ if (!existsSync(path)) return [];
109
+ try {
110
+ const raw = readFileSync(path, 'utf-8');
111
+ try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
112
+ return raw.split('\n').filter(Boolean).flatMap(line => {
113
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
114
+ });
115
+ } catch { return []; }
45
116
  }
46
117
 
47
118
  /** Peek at pending coordinator events without draining (non-destructive). */
48
- export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
49
- return pendingMeshCoordinatorEvents.slice();
119
+ export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
120
+ if (!meshId) return [];
121
+ const path = getPendingEventsPath(meshId);
122
+ if (!existsSync(path)) return [];
123
+ try {
124
+ const raw = readFileSync(path, 'utf-8');
125
+ return raw.split('\n').filter(Boolean).flatMap(line => {
126
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
127
+ });
128
+ } catch { return []; }
50
129
  }
51
130
 
52
- /** Explicitly clear all pending coordinator events. */
53
- export function clearPendingMeshCoordinatorEvents(): void {
54
- pendingMeshCoordinatorEvents.splice(0);
131
+ /** Explicitly clear all pending coordinator events for a mesh. */
132
+ export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
133
+ if (!meshId) return;
134
+ const path = getPendingEventsPath(meshId);
135
+ if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
55
136
  }
56
137
 
57
138
  function readNonEmptyString(value: unknown): string {
58
139
  return typeof value === 'string' && value.trim() ? value.trim() : '';
59
140
  }
60
141
 
142
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
143
+ return value && typeof value === 'object' && !Array.isArray(value)
144
+ ? value as Record<string, unknown>
145
+ : undefined;
146
+ }
147
+
61
148
  function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
62
149
  return readNonEmptyString(event.targetSessionId)
63
150
  || readNonEmptyString(event.sessionId)
@@ -72,6 +159,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
72
159
  'agent:stopped',
73
160
  'agent:ready',
74
161
  'monitor:long_generating',
162
+ 'refine:accepted',
163
+ 'refine:completed',
164
+ 'refine:failed',
75
165
  ]);
76
166
 
77
167
  const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
@@ -86,10 +176,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
86
176
  }
87
177
 
88
178
  function formatCompletionMetadata(event: Record<string, unknown>): string {
179
+ const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
180
+ ? event.completionDiagnostic as Record<string, unknown>
181
+ : null;
182
+ const diagnosticReason = completionDiagnostic
183
+ ? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
184
+ : '';
185
+ const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
186
+ ? String(completionDiagnostic.finalAssistantPresent)
187
+ : '';
89
188
  const parts = [
90
189
  readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
91
190
  readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
92
191
  readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
192
+ diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
193
+ finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
93
194
  ].filter(Boolean);
94
195
  return parts.length > 0 ? ` (${parts.join('; ')})` : '';
95
196
  }
@@ -140,6 +241,74 @@ function shouldSuppressIntentionalCleanupStop(args: {
140
241
  return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
141
242
  }
142
243
 
244
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
245
+ const recentCompletionFingerprints = new Map<string, number>();
246
+
247
+ function readEventTimestamp(value: unknown): number | null {
248
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
249
+ if (typeof value === 'string' && value.trim()) {
250
+ const numeric = Number(value);
251
+ if (Number.isFinite(numeric)) return numeric;
252
+ const parsed = Date.parse(value);
253
+ if (Number.isFinite(parsed)) return parsed;
254
+ }
255
+ return null;
256
+ }
257
+
258
+ function buildMeshCompletionFingerprint(args: {
259
+ meshId: string;
260
+ event: string;
261
+ sessionId: string;
262
+ providerType?: string;
263
+ providerSessionId?: string;
264
+ timestamp?: number | null;
265
+ finalSummary?: string;
266
+ }): string {
267
+ const timestampPart = Number.isFinite(args.timestamp)
268
+ ? String(args.timestamp)
269
+ : readNonEmptyString(args.finalSummary).slice(0, 200);
270
+ return [
271
+ args.meshId,
272
+ args.event,
273
+ args.sessionId,
274
+ args.providerType || '',
275
+ args.providerSessionId || '',
276
+ timestampPart,
277
+ ].join('::');
278
+ }
279
+
280
+ function isDuplicateMeshCompletionEvent(args: {
281
+ meshId: string;
282
+ event: string;
283
+ sessionId: string;
284
+ providerType?: string;
285
+ providerSessionId?: string;
286
+ timestamp?: number | null;
287
+ finalSummary?: string;
288
+ }): boolean {
289
+ const fingerprint = buildMeshCompletionFingerprint(args);
290
+ if (!fingerprint) return false;
291
+ const now = Date.now();
292
+ for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
293
+ if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
294
+ }
295
+ if (recentCompletionFingerprints.has(fingerprint)) return true;
296
+ recentCompletionFingerprints.set(fingerprint, now);
297
+ return false;
298
+ }
299
+
300
+ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
301
+ const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
302
+ if (!fingerprint) return false;
303
+ const now = Date.now();
304
+ for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
305
+ if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
306
+ }
307
+ if (recentCompletionFingerprints.has(fingerprint)) return true;
308
+ recentCompletionFingerprints.set(fingerprint, now);
309
+ return false;
310
+ }
311
+
143
312
 
144
313
  export function tryAssignQueueTask(
145
314
  components: DaemonComponents,
@@ -170,7 +339,16 @@ export function tryAssignQueueTask(
170
339
  message: task.message,
171
340
  }).catch((e: any) => {
172
341
  LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
173
- updateTaskStatus(meshId, task.id, 'failed');
342
+ // Revert to pending so the task can be retried rather than permanently failing
343
+ updateTaskStatus(meshId, task.id, 'pending');
344
+ try {
345
+ appendLedgerEntry(meshId, {
346
+ kind: 'dispatch_failed' as any,
347
+ nodeId,
348
+ sessionId,
349
+ payload: { taskId: task.id, error: e?.message, retryable: true },
350
+ });
351
+ } catch { /* ledger write is best-effort */ }
174
352
  });
175
353
  return true;
176
354
  }
@@ -565,6 +743,71 @@ function buildMeshSystemMessage(args: {
565
743
  if (args.event === 'monitor:long_generating') {
566
744
  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.`;
567
745
  }
746
+ if (args.event === 'refine:accepted') {
747
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
748
+ 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.`;
749
+ }
750
+ if (args.event === 'refine:completed') {
751
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
752
+ const result = readRecord(args.metadataEvent.result);
753
+ const validationSummary = readRecord(result?.validationSummary);
754
+ const patchEquivalence = readRecord(result?.patchEquivalence);
755
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
756
+ const validationStatus = readNonEmptyString(validationSummary?.status);
757
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
758
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
759
+ const into = readNonEmptyString(result?.into);
760
+ const branch = readNonEmptyString(result?.branch);
761
+ const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
762
+ const convergenceStatus = readNonEmptyString(finalConvergence?.status);
763
+ const nextStep = readNonEmptyString(result?.nextStep)
764
+ || readNonEmptyString(finalConvergence?.nextStep)
765
+ || 'Continue from the updated mesh state.';
766
+ const details = [
767
+ jobId ? `job_id=${jobId}` : '',
768
+ branch && into ? `${branch}→${into}` : '',
769
+ validationStatus ? `validation=${validationStatus}` : '',
770
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
771
+ mergeStatus ? `merge=${mergeStatus}` : '',
772
+ convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
773
+ ].filter(Boolean).join('; ');
774
+ return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
775
+ }
776
+ if (args.event === 'refine:failed') {
777
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
778
+ const result = readRecord(args.metadataEvent.result);
779
+ const validationSummary = readRecord(result?.validationSummary);
780
+ const patchEquivalence = readRecord(result?.patchEquivalence);
781
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
782
+ const code = readNonEmptyString(result?.code);
783
+ const error = readNonEmptyString(result?.error);
784
+ const validationStatus = readNonEmptyString(validationSummary?.status);
785
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
786
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
787
+ const mergeStatus = result?.merged === true
788
+ ? 'merged'
789
+ : finalConvergence?.merged === false
790
+ ? 'not_merged'
791
+ : '';
792
+ const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
793
+ || readNonEmptyString(finalConvergence?.status);
794
+ const blockedReason = readNonEmptyString(result?.blockedReason);
795
+ const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
796
+ const details = [
797
+ jobId ? `job_id=${jobId}` : '',
798
+ code ? `code=${code}` : '',
799
+ validationStatus ? `validation=${validationStatus}` : '',
800
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
801
+ mergeStatus ? `merge=${mergeStatus}` : '',
802
+ convergenceStatus ? `convergence=${convergenceStatus}` : '',
803
+ blockedReason ? `reason=${blockedReason}` : '',
804
+ ].filter(Boolean).join('; ');
805
+ const parts = [
806
+ `[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
807
+ nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
808
+ ];
809
+ return parts.join('\n');
810
+ }
568
811
  return '';
569
812
  }
570
813
 
@@ -593,6 +836,28 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
593
836
  return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
594
837
  }
595
838
 
839
+ if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
840
+ LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
841
+ return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
842
+ }
843
+
844
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
845
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
846
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
847
+ meshId: args.meshId,
848
+ event: args.event,
849
+ sessionId: eventSessionId,
850
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
851
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
852
+ timestamp: eventTimestamp,
853
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
854
+ });
855
+ if (duplicateCompletion) {
856
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
857
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
858
+ }
859
+ }
860
+
596
861
  // ── Task Queue & Ledger ──
597
862
  let completedTaskForLedger: { id?: string } | null = null;
598
863
  if (args.event === 'agent:generating_completed') {
@@ -601,19 +866,25 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
601
866
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
602
867
 
603
868
  if (sessionId) {
604
- const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
869
+ const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
870
+ occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
871
+ });
605
872
  completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
606
873
  if (nodeId && providerType) {
607
- // Short delay to allow completion event to propagate before pulling next
608
- setTimeout(() => {
874
+ // Queue state is already updated above; setImmediate avoids the
875
+ // 500 ms artificial delay while still deferring past this call frame.
876
+ setImmediate(() => {
609
877
  tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
610
- }, 500);
878
+ });
611
879
  }
612
880
  }
613
881
  } else if (args.event === 'agent:ready') {
614
882
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
615
883
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
616
884
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
885
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
886
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
887
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
617
888
  const completedTask = sessionId
618
889
  ? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
619
890
  : null;
@@ -630,15 +901,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
630
901
  nodeLabel: args.nodeLabel,
631
902
  taskId: completedTask.id,
632
903
  completedViaReady: true,
633
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
634
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
904
+ providerSessionId,
905
+ finalSummary,
906
+ workerResult,
635
907
  evidence: buildTaskCompletionEvidence({
636
908
  event: 'agent:ready',
637
909
  nodeId,
638
910
  sessionId,
639
911
  providerType: providerType || undefined,
640
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
641
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
912
+ providerSessionId,
913
+ finalSummary,
914
+ workerResult,
642
915
  }),
643
916
  },
644
917
  });
@@ -648,13 +921,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
648
921
  }
649
922
 
650
923
  if (sessionId && nodeId && providerType) {
651
- remoteIdleSessions.set(`${nodeId}:${sessionId}`, { nodeId, sessionId, providerType });
652
- setTimeout(() => {
924
+ sweepExpiredRemoteIdleSessions();
925
+ remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
926
+ nodeId, sessionId, providerType,
927
+ expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
928
+ });
929
+ setImmediate(() => {
653
930
  const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
654
- if (assigned) {
655
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
656
- }
657
- }, 500);
931
+ if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
932
+ });
658
933
  }
659
934
  } else if (args.event === 'agent:generating_started') {
660
935
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
@@ -669,7 +944,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
669
944
  remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
670
945
  }
671
946
  if (sessionId) {
672
- updateSessionTaskStatus(args.meshId, sessionId, 'failed');
947
+ const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
948
+ completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
673
949
  }
674
950
  }
675
951
 
@@ -679,14 +955,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
679
955
  const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
680
956
  const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
681
957
  const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
958
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
959
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
960
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
682
961
  const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
683
962
  ? buildTaskCompletionEvidence({
684
963
  event: 'agent:generating_completed',
685
964
  nodeId: ledgerNodeId,
686
965
  sessionId: ledgerSessionId,
687
966
  providerType: ledgerProviderType,
688
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
689
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
967
+ providerSessionId,
968
+ finalSummary,
969
+ workerResult,
690
970
  })
691
971
  : undefined;
692
972
  appendLedgerEntry(args.meshId, {
@@ -698,8 +978,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
698
978
  event: args.event,
699
979
  nodeLabel: args.nodeLabel,
700
980
  taskId: completedTaskForLedger?.id || undefined,
701
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
702
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
981
+ providerSessionId,
982
+ finalSummary,
983
+ workerResult,
984
+ completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
985
+ ? args.metadataEvent.completionDiagnostic
986
+ : undefined,
703
987
  evidence: completionEvidence,
704
988
  },
705
989
  });
@@ -772,6 +1056,14 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
772
1056
  }
773
1057
  }
774
1058
 
1059
+ const messageText = buildMeshSystemMessage({
1060
+ event: args.event,
1061
+ nodeLabel: args.nodeLabel,
1062
+ metadataEvent: args.metadataEvent,
1063
+ recoveryContext,
1064
+ });
1065
+ if (!messageText) return { success: false, error: 'unsupported mesh event' };
1066
+
775
1067
  const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
776
1068
  const instState = inst.getState();
777
1069
  if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
@@ -781,30 +1073,24 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
781
1073
 
782
1074
  if (coordinatorInstances.length === 0) {
783
1075
  // No CLI coordinator session found — buffer for MCP-based coordinators.
784
- if (pendingMeshCoordinatorEvents.length < MAX_PENDING_EVENTS) {
785
- pendingMeshCoordinatorEvents.push({
1076
+ if (queuePendingMeshCoordinatorEvent({
786
1077
  event: args.event,
787
1078
  meshId: args.meshId,
788
1079
  nodeLabel: args.nodeLabel,
1080
+ nodeId: args.nodeId || undefined,
1081
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
789
1082
  metadataEvent: {
790
1083
  ...args.metadataEvent,
791
1084
  ...(recoveryContext ? { recoveryContext } : {}),
792
1085
  },
1086
+ coordinatorMessage: messageText,
793
1087
  queuedAt: Date.now(),
794
- });
1088
+ })) {
795
1089
  LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
796
1090
  }
797
1091
  return { success: true, forwarded: 0 };
798
1092
  }
799
1093
 
800
- const messageText = buildMeshSystemMessage({
801
- event: args.event,
802
- nodeLabel: args.nodeLabel,
803
- metadataEvent: args.metadataEvent,
804
- recoveryContext,
805
- });
806
- if (!messageText) return { success: false, error: 'unsupported mesh event' };
807
-
808
1094
  for (const coord of coordinatorInstances) {
809
1095
  const coordState = coord.getState();
810
1096
  LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}`);
@@ -834,6 +1120,15 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
834
1120
  providerType: readNonEmptyString(payload.providerType),
835
1121
  providerSessionId: readNonEmptyString(payload.providerSessionId),
836
1122
  finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
1123
+ jobId: readNonEmptyString(payload.jobId),
1124
+ interactionId: readNonEmptyString(payload.interactionId),
1125
+ status: readNonEmptyString(payload.status),
1126
+ targetDaemonId: readNonEmptyString(payload.targetDaemonId),
1127
+ startedAt: readNonEmptyString(payload.startedAt),
1128
+ completedAt: readNonEmptyString(payload.completedAt),
1129
+ retryOfJobId: readNonEmptyString(payload.retryOfJobId),
1130
+ ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
1131
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
837
1132
  intentional: payload.intentional === true,
838
1133
  intentionalStop: payload.intentionalStop === true,
839
1134
  operatorCleanup: payload.operatorCleanup === true,