@adhdev/daemon-core 0.9.82-rc.6 → 0.9.82-rc.61

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 (42) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +2 -0
  2. package/dist/commands/router.d.ts +24 -0
  3. package/dist/config/mesh-config.d.ts +66 -1
  4. package/dist/git/git-commands.d.ts +1 -0
  5. package/dist/git/git-status.d.ts +5 -0
  6. package/dist/git/git-types.d.ts +10 -0
  7. package/dist/index.d.ts +13 -6
  8. package/dist/index.js +3522 -593
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +3496 -587
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/mesh/mesh-active-work.d.ts +48 -0
  13. package/dist/mesh/mesh-events.d.ts +17 -5
  14. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  15. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  16. package/dist/mesh/mesh-ledger.d.ts +38 -1
  17. package/dist/mesh/mesh-work-queue.d.ts +23 -5
  18. package/dist/mesh/refine-config.d.ts +119 -0
  19. package/dist/providers/chat-message-normalization.d.ts +1 -0
  20. package/dist/providers/cli-provider-instance.d.ts +1 -0
  21. package/dist/repo-mesh-types.d.ts +160 -0
  22. package/package.json +1 -1
  23. package/src/boot/daemon-lifecycle.ts +4 -0
  24. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  25. package/src/commands/router.ts +2178 -419
  26. package/src/config/mesh-config.ts +244 -1
  27. package/src/git/git-commands.ts +3 -3
  28. package/src/git/git-status.ts +97 -6
  29. package/src/git/git-summary.ts +3 -0
  30. package/src/git/git-types.ts +11 -0
  31. package/src/index.ts +39 -5
  32. package/src/mesh/coordinator-prompt.ts +4 -2
  33. package/src/mesh/mesh-active-work.ts +205 -0
  34. package/src/mesh/mesh-events.ts +210 -38
  35. package/src/mesh/mesh-fast-forward.ts +430 -0
  36. package/src/mesh/mesh-host-ownership.ts +73 -0
  37. package/src/mesh/mesh-ledger.ts +137 -0
  38. package/src/mesh/mesh-work-queue.ts +202 -122
  39. package/src/mesh/refine-config.ts +306 -0
  40. package/src/providers/chat-message-normalization.ts +3 -1
  41. package/src/providers/cli-provider-instance.ts +66 -1
  42. package/src/repo-mesh-types.ts +174 -0
@@ -0,0 +1,205 @@
1
+ import type { MeshLedgerEntry } from './mesh-ledger.js';
2
+ import type { MeshWorkQueueEntry } from './mesh-work-queue.js';
3
+
4
+ export type MeshActiveWorkSource = 'queue' | 'direct';
5
+ export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval';
6
+
7
+ export interface MeshActiveWorkRecord {
8
+ taskId: string;
9
+ source: MeshActiveWorkSource;
10
+ status: MeshActiveWorkStatus;
11
+ nodeId?: string;
12
+ sessionId?: string;
13
+ providerType?: string;
14
+ taskTitle: string;
15
+ taskSummary: string;
16
+ message?: string;
17
+ taskMode?: string;
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ dispatchedAt?: string;
21
+ elapsedMs: number;
22
+ terminal?: boolean;
23
+ terminalKind?: string;
24
+ terminalAt?: string;
25
+ }
26
+
27
+ export interface MeshActiveWorkSummary {
28
+ totalActiveCount: number;
29
+ queueActiveCount: number;
30
+ directActiveCount: number;
31
+ awaitingApprovalCount: number;
32
+ generatingCount: number;
33
+ failedCount: number;
34
+ idleCount: number;
35
+ sourceCounts: Record<MeshActiveWorkSource, number>;
36
+ statusCounts: Record<MeshActiveWorkStatus, number>;
37
+ }
38
+
39
+ export interface BuildMeshActiveWorkOptions {
40
+ meshId: string;
41
+ queue?: MeshWorkQueueEntry[];
42
+ ledgerEntries?: MeshLedgerEntry[];
43
+ nodes?: any[];
44
+ now?: number;
45
+ /** Include terminal direct rows (idle/failed) for handoff/recent-work surfaces. Defaults false. */
46
+ includeTerminalDirect?: boolean;
47
+ }
48
+
49
+ const DIRECT_DISPATCH_VIA = new Set(['p2p_direct', 'local_direct', 'mesh_send_task']);
50
+ const TERMINAL_LEDGER_KINDS = new Set(['task_completed', 'task_failed', 'task_stalled']);
51
+
52
+ function readString(value: unknown): string | undefined {
53
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
54
+ }
55
+
56
+ function summarizeMessage(message: string): { title: string; summary: string } {
57
+ const oneLine = message.replace(/\s+/g, ' ').trim();
58
+ const title = oneLine.length > 96 ? `${oneLine.slice(0, 93)}...` : oneLine;
59
+ return { title: title || '(untitled task)', summary: oneLine };
60
+ }
61
+
62
+ function elapsedSince(value: string | undefined, now: number): number {
63
+ const started = value ? new Date(value).getTime() : Number.NaN;
64
+ return Number.isFinite(started) ? Math.max(0, now - started) : 0;
65
+ }
66
+
67
+ function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): MeshActiveWorkStatus | undefined {
68
+ if (!nodeId || !sessionId || !Array.isArray(nodes)) return undefined;
69
+ const node = nodes.find(item => readString(item?.id) === nodeId || readString(item?.nodeId) === nodeId || readString(item?.node_id) === nodeId);
70
+ if (!node) return undefined;
71
+ const candidates: any[] = [];
72
+ for (const value of [node.sessions, node.activeSessions, node.active_sessions, node.lastProbe?.sessions, node.last_probe?.sessions, node.lastProbe?.status?.sessions, node.last_probe?.status?.sessions]) {
73
+ if (Array.isArray(value)) candidates.push(...value);
74
+ }
75
+ for (const value of [node.activeSession, node.active_session, node.currentSession, node.current_session, node.runtimeSession, node.runtime_session, node.session]) {
76
+ if (value && typeof value === 'object') candidates.push(value);
77
+ }
78
+ const session = candidates.find(item => {
79
+ const id = readString(item?.id) || readString(item?.sessionId) || readString(item?.session_id) || readString(item?.runtimeSessionId) || readString(item?.instanceId);
80
+ return id === sessionId;
81
+ });
82
+ if (!session) return undefined;
83
+ const raw = `${readString(session.status) || ''} ${readString(session.lifecycle) || ''} ${readString(session.state) || ''} ${readString(session.activeChat?.status) || ''}`.toLowerCase();
84
+ if (raw.includes('approval')) return 'awaiting_approval';
85
+ if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return 'generating';
86
+ if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return 'failed';
87
+ if (raw.includes('idle') || raw.includes('waiting_input') || raw.includes('ready')) return 'idle';
88
+ return undefined;
89
+ }
90
+
91
+ function isDirectDispatch(entry: MeshLedgerEntry): boolean {
92
+ if (entry.kind !== 'task_dispatched') return false;
93
+ const payload = entry.payload || {};
94
+ if (payload.source === 'direct') return true;
95
+ const via = readString(payload.via);
96
+ return Boolean(via && DIRECT_DISPATCH_VIA.has(via) && payload.source !== 'queue');
97
+ }
98
+
99
+ function directDispatchTaskId(entry: MeshLedgerEntry): string {
100
+ return readString(entry.payload?.taskId) || entry.id;
101
+ }
102
+
103
+ function terminalMatchesDispatch(terminal: MeshLedgerEntry, dispatch: MeshLedgerEntry, taskId: string): boolean {
104
+ const terminalTaskId = readString(terminal.payload?.taskId);
105
+ if (terminalTaskId && terminalTaskId === taskId) return true;
106
+ if (terminalTaskId && terminalTaskId !== taskId) return false;
107
+ if (dispatch.sessionId && terminal.sessionId === dispatch.sessionId) return true;
108
+ return Boolean(dispatch.nodeId && terminal.nodeId === dispatch.nodeId && !dispatch.sessionId);
109
+ }
110
+
111
+ function statusFromTerminal(entry: MeshLedgerEntry): MeshActiveWorkStatus {
112
+ if (entry.kind === 'task_approval_needed') return 'awaiting_approval';
113
+ if (entry.kind === 'task_completed') return 'idle';
114
+ return 'failed';
115
+ }
116
+
117
+ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]): MeshActiveWorkSummary {
118
+ const statusCounts: Record<MeshActiveWorkStatus, number> = {
119
+ pending: 0,
120
+ assigned: 0,
121
+ generating: 0,
122
+ idle: 0,
123
+ failed: 0,
124
+ awaiting_approval: 0,
125
+ };
126
+ const sourceCounts: Record<MeshActiveWorkSource, number> = { queue: 0, direct: 0 };
127
+ for (const item of activeWork) {
128
+ sourceCounts[item.source] += 1;
129
+ statusCounts[item.status] += 1;
130
+ }
131
+ return {
132
+ totalActiveCount: activeWork.length,
133
+ queueActiveCount: sourceCounts.queue,
134
+ directActiveCount: sourceCounts.direct,
135
+ awaitingApprovalCount: statusCounts.awaiting_approval,
136
+ generatingCount: statusCounts.generating,
137
+ failedCount: statusCounts.failed,
138
+ idleCount: statusCounts.idle,
139
+ sourceCounts,
140
+ statusCounts,
141
+ };
142
+ }
143
+
144
+ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeWork: MeshActiveWorkRecord[]; summary: MeshActiveWorkSummary } {
145
+ const now = opts.now ?? Date.now();
146
+ const records: MeshActiveWorkRecord[] = [];
147
+
148
+ for (const task of opts.queue || []) {
149
+ if (task.status !== 'pending' && task.status !== 'assigned') continue;
150
+ const { title, summary } = summarizeMessage(task.message || '');
151
+ records.push({
152
+ taskId: task.id,
153
+ source: 'queue',
154
+ status: task.status,
155
+ nodeId: task.assignedNodeId || task.targetNodeId,
156
+ sessionId: task.assignedSessionId || task.targetSessionId,
157
+ taskTitle: title,
158
+ taskSummary: summary,
159
+ message: task.message,
160
+ taskMode: task.taskMode,
161
+ createdAt: task.createdAt,
162
+ updatedAt: task.updatedAt,
163
+ dispatchedAt: task.dispatchTimestamp,
164
+ elapsedMs: elapsedSince(task.dispatchTimestamp || task.createdAt, now),
165
+ });
166
+ }
167
+
168
+ const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
169
+ const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
170
+ for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
171
+ const taskId = directDispatchTaskId(dispatch);
172
+ const terminal = terminals
173
+ .filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
174
+ .find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
175
+ const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
176
+ const liveStatus = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
177
+ const status = terminalStatus || liveStatus || 'assigned';
178
+ const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
179
+ if (terminalRow && opts.includeTerminalDirect !== true) continue;
180
+ const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
181
+ const { title, summary } = summarizeMessage(message);
182
+ records.push({
183
+ taskId,
184
+ source: 'direct',
185
+ status,
186
+ nodeId: dispatch.nodeId,
187
+ sessionId: dispatch.sessionId,
188
+ providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
189
+ taskTitle: readString(dispatch.payload?.taskTitle) || title,
190
+ taskSummary: readString(dispatch.payload?.taskSummary) || summary,
191
+ message,
192
+ taskMode: readString(dispatch.payload?.taskMode),
193
+ createdAt: dispatch.timestamp,
194
+ updatedAt: terminal?.timestamp || dispatch.timestamp,
195
+ dispatchedAt: dispatch.timestamp,
196
+ elapsedMs: elapsedSince(dispatch.timestamp, now),
197
+ terminal: terminalRow,
198
+ terminalKind: terminal?.kind,
199
+ terminalAt: terminal?.timestamp,
200
+ });
201
+ }
202
+
203
+ records.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
204
+ return { activeWork: records, summary: buildMeshActiveWorkSummary(records) };
205
+ }
@@ -1,63 +1,122 @@
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>;
36
58
  queuedAt: number;
37
59
  }
38
60
 
39
- const MAX_PENDING_EVENTS = 50;
40
- const pendingMeshCoordinatorEvents: PendingMeshCoordinatorEvent[] = [];
61
+ function getPendingEventsPath(meshId: string): string {
62
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
63
+ return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
64
+ }
65
+
66
+ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
67
+ try {
68
+ appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
69
+ return true;
70
+ } catch (e: any) {
71
+ LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
72
+ return false;
73
+ }
74
+ }
41
75
 
42
- /** Drain and return all pending coordinator events, clearing the queue. */
43
- export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
44
- return pendingMeshCoordinatorEvents.splice(0);
76
+ /** Drain and return all pending coordinator events for meshId, removing them from disk. */
77
+ export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
78
+ if (!meshId) return [];
79
+ const path = getPendingEventsPath(meshId);
80
+ if (!existsSync(path)) return [];
81
+ try {
82
+ const raw = readFileSync(path, 'utf-8');
83
+ try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
84
+ return raw.split('\n').filter(Boolean).flatMap(line => {
85
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
86
+ });
87
+ } catch { return []; }
45
88
  }
46
89
 
47
90
  /** Peek at pending coordinator events without draining (non-destructive). */
48
- export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
49
- return pendingMeshCoordinatorEvents.slice();
91
+ export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
92
+ if (!meshId) return [];
93
+ const path = getPendingEventsPath(meshId);
94
+ if (!existsSync(path)) return [];
95
+ try {
96
+ const raw = readFileSync(path, 'utf-8');
97
+ return raw.split('\n').filter(Boolean).flatMap(line => {
98
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
99
+ });
100
+ } catch { return []; }
50
101
  }
51
102
 
52
- /** Explicitly clear all pending coordinator events. */
53
- export function clearPendingMeshCoordinatorEvents(): void {
54
- pendingMeshCoordinatorEvents.splice(0);
103
+ /** Explicitly clear all pending coordinator events for a mesh. */
104
+ export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
105
+ if (!meshId) return;
106
+ const path = getPendingEventsPath(meshId);
107
+ if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
55
108
  }
56
109
 
57
110
  function readNonEmptyString(value: unknown): string {
58
111
  return typeof value === 'string' && value.trim() ? value.trim() : '';
59
112
  }
60
113
 
114
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
115
+ return value && typeof value === 'object' && !Array.isArray(value)
116
+ ? value as Record<string, unknown>
117
+ : undefined;
118
+ }
119
+
61
120
  function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
62
121
  return readNonEmptyString(event.targetSessionId)
63
122
  || readNonEmptyString(event.sessionId)
@@ -86,10 +145,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
86
145
  }
87
146
 
88
147
  function formatCompletionMetadata(event: Record<string, unknown>): string {
148
+ const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
149
+ ? event.completionDiagnostic as Record<string, unknown>
150
+ : null;
151
+ const diagnosticReason = completionDiagnostic
152
+ ? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
153
+ : '';
154
+ const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
155
+ ? String(completionDiagnostic.finalAssistantPresent)
156
+ : '';
89
157
  const parts = [
90
158
  readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
91
159
  readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
92
160
  readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
161
+ diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
162
+ finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
93
163
  ].filter(Boolean);
94
164
  return parts.length > 0 ? ` (${parts.join('; ')})` : '';
95
165
  }
@@ -140,6 +210,62 @@ function shouldSuppressIntentionalCleanupStop(args: {
140
210
  return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
141
211
  }
142
212
 
213
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
214
+ const recentCompletionFingerprints = new Map<string, number>();
215
+
216
+ function readEventTimestamp(value: unknown): number | null {
217
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
218
+ if (typeof value === 'string' && value.trim()) {
219
+ const numeric = Number(value);
220
+ if (Number.isFinite(numeric)) return numeric;
221
+ const parsed = Date.parse(value);
222
+ if (Number.isFinite(parsed)) return parsed;
223
+ }
224
+ return null;
225
+ }
226
+
227
+ function buildMeshCompletionFingerprint(args: {
228
+ meshId: string;
229
+ event: string;
230
+ sessionId: string;
231
+ providerType?: string;
232
+ providerSessionId?: string;
233
+ timestamp?: number | null;
234
+ finalSummary?: string;
235
+ }): string {
236
+ const timestampPart = Number.isFinite(args.timestamp)
237
+ ? String(args.timestamp)
238
+ : readNonEmptyString(args.finalSummary).slice(0, 200);
239
+ return [
240
+ args.meshId,
241
+ args.event,
242
+ args.sessionId,
243
+ args.providerType || '',
244
+ args.providerSessionId || '',
245
+ timestampPart,
246
+ ].join('::');
247
+ }
248
+
249
+ function isDuplicateMeshCompletionEvent(args: {
250
+ meshId: string;
251
+ event: string;
252
+ sessionId: string;
253
+ providerType?: string;
254
+ providerSessionId?: string;
255
+ timestamp?: number | null;
256
+ finalSummary?: string;
257
+ }): boolean {
258
+ const fingerprint = buildMeshCompletionFingerprint(args);
259
+ if (!fingerprint) return false;
260
+ const now = Date.now();
261
+ for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
262
+ if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
263
+ }
264
+ if (recentCompletionFingerprints.has(fingerprint)) return true;
265
+ recentCompletionFingerprints.set(fingerprint, now);
266
+ return false;
267
+ }
268
+
143
269
 
144
270
  export function tryAssignQueueTask(
145
271
  components: DaemonComponents,
@@ -170,7 +296,16 @@ export function tryAssignQueueTask(
170
296
  message: task.message,
171
297
  }).catch((e: any) => {
172
298
  LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
173
- updateTaskStatus(meshId, task.id, 'failed');
299
+ // Revert to pending so the task can be retried rather than permanently failing
300
+ updateTaskStatus(meshId, task.id, 'pending');
301
+ try {
302
+ appendLedgerEntry(meshId, {
303
+ kind: 'dispatch_failed' as any,
304
+ nodeId,
305
+ sessionId,
306
+ payload: { taskId: task.id, error: e?.message, retryable: true },
307
+ });
308
+ } catch { /* ledger write is best-effort */ }
174
309
  });
175
310
  return true;
176
311
  }
@@ -593,6 +728,23 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
593
728
  return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
594
729
  }
595
730
 
731
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
732
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
733
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
734
+ meshId: args.meshId,
735
+ event: args.event,
736
+ sessionId: eventSessionId,
737
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
738
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
739
+ timestamp: eventTimestamp,
740
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
741
+ });
742
+ if (duplicateCompletion) {
743
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
744
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
745
+ }
746
+ }
747
+
596
748
  // ── Task Queue & Ledger ──
597
749
  let completedTaskForLedger: { id?: string } | null = null;
598
750
  if (args.event === 'agent:generating_completed') {
@@ -601,19 +753,25 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
601
753
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
602
754
 
603
755
  if (sessionId) {
604
- const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
756
+ const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
757
+ occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
758
+ });
605
759
  completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
606
760
  if (nodeId && providerType) {
607
- // Short delay to allow completion event to propagate before pulling next
608
- setTimeout(() => {
761
+ // Queue state is already updated above; setImmediate avoids the
762
+ // 500 ms artificial delay while still deferring past this call frame.
763
+ setImmediate(() => {
609
764
  tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
610
- }, 500);
765
+ });
611
766
  }
612
767
  }
613
768
  } else if (args.event === 'agent:ready') {
614
769
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
615
770
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
616
771
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
772
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
773
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
774
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
617
775
  const completedTask = sessionId
618
776
  ? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
619
777
  : null;
@@ -630,15 +788,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
630
788
  nodeLabel: args.nodeLabel,
631
789
  taskId: completedTask.id,
632
790
  completedViaReady: true,
633
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
634
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
791
+ providerSessionId,
792
+ finalSummary,
793
+ workerResult,
635
794
  evidence: buildTaskCompletionEvidence({
636
795
  event: 'agent:ready',
637
796
  nodeId,
638
797
  sessionId,
639
798
  providerType: providerType || undefined,
640
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
641
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
799
+ providerSessionId,
800
+ finalSummary,
801
+ workerResult,
642
802
  }),
643
803
  },
644
804
  });
@@ -648,13 +808,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
648
808
  }
649
809
 
650
810
  if (sessionId && nodeId && providerType) {
651
- remoteIdleSessions.set(`${nodeId}:${sessionId}`, { nodeId, sessionId, providerType });
652
- setTimeout(() => {
811
+ sweepExpiredRemoteIdleSessions();
812
+ remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
813
+ nodeId, sessionId, providerType,
814
+ expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
815
+ });
816
+ setImmediate(() => {
653
817
  const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
654
- if (assigned) {
655
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
656
- }
657
- }, 500);
818
+ if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
819
+ });
658
820
  }
659
821
  } else if (args.event === 'agent:generating_started') {
660
822
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
@@ -679,14 +841,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
679
841
  const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
680
842
  const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
681
843
  const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
844
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
845
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
846
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
682
847
  const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
683
848
  ? buildTaskCompletionEvidence({
684
849
  event: 'agent:generating_completed',
685
850
  nodeId: ledgerNodeId,
686
851
  sessionId: ledgerSessionId,
687
852
  providerType: ledgerProviderType,
688
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
689
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
853
+ providerSessionId,
854
+ finalSummary,
855
+ workerResult,
690
856
  })
691
857
  : undefined;
692
858
  appendLedgerEntry(args.meshId, {
@@ -698,8 +864,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
698
864
  event: args.event,
699
865
  nodeLabel: args.nodeLabel,
700
866
  taskId: completedTaskForLedger?.id || undefined,
701
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
702
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
867
+ providerSessionId,
868
+ finalSummary,
869
+ workerResult,
870
+ completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
871
+ ? args.metadataEvent.completionDiagnostic
872
+ : undefined,
703
873
  evidence: completionEvidence,
704
874
  },
705
875
  });
@@ -781,17 +951,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
781
951
 
782
952
  if (coordinatorInstances.length === 0) {
783
953
  // No CLI coordinator session found — buffer for MCP-based coordinators.
784
- if (pendingMeshCoordinatorEvents.length < MAX_PENDING_EVENTS) {
785
- pendingMeshCoordinatorEvents.push({
954
+ if (queuePendingMeshCoordinatorEvent({
786
955
  event: args.event,
787
956
  meshId: args.meshId,
788
957
  nodeLabel: args.nodeLabel,
958
+ nodeId: args.nodeId || undefined,
959
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
789
960
  metadataEvent: {
790
961
  ...args.metadataEvent,
791
962
  ...(recoveryContext ? { recoveryContext } : {}),
792
963
  },
793
964
  queuedAt: Date.now(),
794
- });
965
+ })) {
795
966
  LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
796
967
  }
797
968
  return { success: true, forwarded: 0 };
@@ -834,6 +1005,7 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
834
1005
  providerType: readNonEmptyString(payload.providerType),
835
1006
  providerSessionId: readNonEmptyString(payload.providerSessionId),
836
1007
  finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
1008
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
837
1009
  intentional: payload.intentional === true,
838
1010
  intentionalStop: payload.intentionalStop === true,
839
1011
  operatorCleanup: payload.operatorCleanup === true,