@adhdev/daemon-core 0.9.82-rc.196 → 0.9.82-rc.197

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.
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Possible delivery statuses for a session delivery record.
3
+ */
4
+ export type MeshSessionDeliveryStatus = 'queued' | 'delivering' | 'delivered' | 'acked' | 'completed' | 'failed' | 'expired' | 'cancelled';
5
+ /**
6
+ * Kind of delivery — controls priority and policy handling.
7
+ */
8
+ export type MeshSessionDeliveryKind = 'task' | 'followup' | 'approval' | 'recovery' | 'system_notice';
9
+ /**
10
+ * A session delivery decision — what to do when a task arrives for a session.
11
+ */
12
+ export type MeshDeliveryDecision = 'immediate' | 'queued' | 'rejected';
13
+ export interface MeshDeliveryPolicyResult {
14
+ decision: MeshDeliveryDecision;
15
+ reason: string;
16
+ /** When decision='queued', estimated deliver-after ISO timestamp if known. */
17
+ deliverAfter?: string;
18
+ /** Human-readable explanation for coordinator/operator. */
19
+ message: string;
20
+ }
21
+ /**
22
+ * Determine whether to deliver immediately, queue, or reject based on session status.
23
+ *
24
+ * This is a pure function — it does not write to any store.
25
+ */
26
+ export declare function resolveDeliveryDecision(sessionStatus: string | undefined, opts?: {
27
+ kind?: MeshSessionDeliveryKind;
28
+ /** When true, busy session immediate injection is allowed (provider-specific capability). */
29
+ allowBusyInjection?: boolean;
30
+ }): MeshDeliveryPolicyResult;
31
+ export interface SessionDeliveryRecord {
32
+ id: string;
33
+ meshId: string;
34
+ nodeId?: string;
35
+ sessionId?: string;
36
+ providerType?: string;
37
+ taskId?: string;
38
+ kind: MeshSessionDeliveryKind;
39
+ priority: number;
40
+ message: string;
41
+ status: MeshSessionDeliveryStatus;
42
+ deliverAfter?: string;
43
+ expiresAt?: string;
44
+ attemptCount: number;
45
+ sourceCoordinatorSessionId?: string;
46
+ sourceCoordinatorDaemonId?: string;
47
+ lastError?: string;
48
+ createdAt: string;
49
+ updatedAt: string;
50
+ }
51
+ /**
52
+ * Create a delivery record in the store.
53
+ */
54
+ export declare function createSessionDelivery(opts: {
55
+ meshId: string;
56
+ nodeId?: string;
57
+ sessionId?: string;
58
+ providerType?: string;
59
+ taskId?: string;
60
+ kind: MeshSessionDeliveryKind;
61
+ message: string;
62
+ status: MeshSessionDeliveryStatus;
63
+ priority?: number;
64
+ deliverAfter?: string;
65
+ expiresAt?: string;
66
+ sourceCoordinatorSessionId?: string;
67
+ sourceCoordinatorDaemonId?: string;
68
+ }): SessionDeliveryRecord;
69
+ /**
70
+ * Update the status of a delivery record.
71
+ */
72
+ export declare function updateSessionDeliveryStatus(id: string, status: MeshSessionDeliveryStatus, opts?: {
73
+ lastError?: string;
74
+ incrementAttempt?: boolean;
75
+ }): void;
76
+ /**
77
+ * Get active (non-terminal) deliveries for a mesh, optionally filtered by session.
78
+ */
79
+ export declare function getActiveSessionDeliveries(meshId: string, sessionId?: string): {
80
+ id: string;
81
+ meshId: string;
82
+ nodeId: string | null;
83
+ sessionId: string | null;
84
+ providerType: string | null;
85
+ taskId: string | null;
86
+ kind: string;
87
+ priority: number;
88
+ message: string;
89
+ status: string;
90
+ deliverAfter: string | null;
91
+ expiresAt: string | null;
92
+ attemptCount: number;
93
+ sourceCoordinatorSessionId: string | null;
94
+ sourceCoordinatorDaemonId: string | null;
95
+ lastError: string | null;
96
+ createdAt: string;
97
+ updatedAt: string;
98
+ }[];
99
+ /**
100
+ * Record a completion conflict diagnostic when a duplicate event points to
101
+ * different task/session than the already-seen event with the same fingerprint.
102
+ */
103
+ export declare function recordCompletionConflict(opts: {
104
+ meshId: string;
105
+ fingerprint: string;
106
+ conflictingTaskId?: string;
107
+ conflictingSessionId?: string;
108
+ originalTaskId?: string;
109
+ originalSessionId?: string;
110
+ event: string;
111
+ }): void;
112
+ /**
113
+ * Get recent completion conflicts for diagnostic inspection.
114
+ */
115
+ export declare function getRecentCompletionConflicts(meshId: string, limitMs?: number): {
116
+ id: string;
117
+ meshId: string;
118
+ fingerprint: string;
119
+ conflictingTaskId: string | null;
120
+ conflictingSessionId: string | null;
121
+ originalTaskId: string | null;
122
+ originalSessionId: string | null;
123
+ event: string;
124
+ createdAt: string;
125
+ }[];
126
+ export declare function __clearSessionDeliveriesForTests(meshId: string): void;
@@ -80,4 +80,70 @@ export declare class MeshRuntimeStore {
80
80
  }>;
81
81
  deleteRemoteIdleSession(nodeId: string, sessionId: string): void;
82
82
  pruneExpiredRemoteIdleSessions(): void;
83
+ insertSessionDelivery(entry: {
84
+ id: string;
85
+ meshId: string;
86
+ nodeId?: string;
87
+ sessionId?: string;
88
+ providerType?: string;
89
+ taskId?: string;
90
+ kind: string;
91
+ priority?: number;
92
+ message: string;
93
+ status: string;
94
+ deliverAfter?: string;
95
+ expiresAt?: string;
96
+ sourceCoordinatorSessionId?: string;
97
+ sourceCoordinatorDaemonId?: string;
98
+ createdAt: string;
99
+ updatedAt: string;
100
+ }): void;
101
+ updateSessionDeliveryStatus(id: string, status: string, opts?: {
102
+ lastError?: string;
103
+ incrementAttempt?: boolean;
104
+ }): void;
105
+ getActiveSessionDeliveries(meshId: string, sessionId?: string): Array<{
106
+ id: string;
107
+ meshId: string;
108
+ nodeId: string | null;
109
+ sessionId: string | null;
110
+ providerType: string | null;
111
+ taskId: string | null;
112
+ kind: string;
113
+ priority: number;
114
+ message: string;
115
+ status: string;
116
+ deliverAfter: string | null;
117
+ expiresAt: string | null;
118
+ attemptCount: number;
119
+ sourceCoordinatorSessionId: string | null;
120
+ sourceCoordinatorDaemonId: string | null;
121
+ lastError: string | null;
122
+ createdAt: string;
123
+ updatedAt: string;
124
+ }>;
125
+ expireStaleSessionDeliveries(meshId: string): void;
126
+ deleteSessionDeliveries(meshId: string): void;
127
+ recordCompletionConflict(entry: {
128
+ id: string;
129
+ meshId: string;
130
+ fingerprint: string;
131
+ conflictingTaskId?: string;
132
+ conflictingSessionId?: string;
133
+ originalTaskId?: string;
134
+ originalSessionId?: string;
135
+ event: string;
136
+ createdAt: string;
137
+ }): void;
138
+ getRecentCompletionConflicts(meshId: string, limitMs?: number): Array<{
139
+ id: string;
140
+ meshId: string;
141
+ fingerprint: string;
142
+ conflictingTaskId: string | null;
143
+ conflictingSessionId: string | null;
144
+ originalTaskId: string | null;
145
+ originalSessionId: string | null;
146
+ event: string;
147
+ createdAt: string;
148
+ }>;
83
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.196",
3
+ "version": "0.9.82-rc.197",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -232,6 +232,10 @@ export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHost
232
232
  export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, reconcileDirectDispatchCompletionFromTranscript } from './mesh/mesh-events.js';
233
233
  export type { PendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
234
234
 
235
+ // ── Mesh Delivery Policy ──
236
+ export { resolveDeliveryDecision, createSessionDelivery, updateSessionDeliveryStatus, getActiveSessionDeliveries, recordCompletionConflict, getRecentCompletionConflicts } from './mesh/mesh-delivery-policy.js';
237
+ export type { MeshSessionDeliveryStatus, MeshSessionDeliveryKind, MeshDeliveryDecision, MeshDeliveryPolicyResult, SessionDeliveryRecord } from './mesh/mesh-delivery-policy.js';
238
+
235
239
  // ── Mesh P2P Relay Failure Classification ──
236
240
  export {
237
241
  P2pRelayFailureError,
@@ -0,0 +1,298 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { MeshRuntimeStore } from './mesh-runtime-store.js';
3
+
4
+ /**
5
+ * Possible delivery statuses for a session delivery record.
6
+ */
7
+ export type MeshSessionDeliveryStatus =
8
+ | 'queued'
9
+ | 'delivering'
10
+ | 'delivered'
11
+ | 'acked'
12
+ | 'completed'
13
+ | 'failed'
14
+ | 'expired'
15
+ | 'cancelled';
16
+
17
+ /**
18
+ * Kind of delivery — controls priority and policy handling.
19
+ */
20
+ export type MeshSessionDeliveryKind =
21
+ | 'task'
22
+ | 'followup'
23
+ | 'approval'
24
+ | 'recovery'
25
+ | 'system_notice';
26
+
27
+ /**
28
+ * A session delivery decision — what to do when a task arrives for a session.
29
+ */
30
+ export type MeshDeliveryDecision =
31
+ | 'immediate' // Session is idle: deliver now, create an 'acked' delivery record
32
+ | 'queued' // Session is busy: hold delivery until session becomes idle
33
+ | 'rejected'; // Session is terminal or unknown: cannot deliver
34
+
35
+ export interface MeshDeliveryPolicyResult {
36
+ decision: MeshDeliveryDecision;
37
+ reason: string;
38
+ /** When decision='queued', estimated deliver-after ISO timestamp if known. */
39
+ deliverAfter?: string;
40
+ /** Human-readable explanation for coordinator/operator. */
41
+ message: string;
42
+ }
43
+
44
+ /**
45
+ * Session statuses where immediate delivery is allowed.
46
+ * The session is ready to accept new work.
47
+ */
48
+ const IMMEDIATE_DELIVERY_STATUSES = new Set([
49
+ 'idle',
50
+ 'waiting_input',
51
+ 'ready',
52
+ ]);
53
+
54
+ /**
55
+ * Session statuses that indicate the session is busy but still alive.
56
+ * Delivery is queued rather than attempted immediately.
57
+ */
58
+ const BUSY_DELIVERY_STATUSES = new Set([
59
+ 'generating',
60
+ 'running',
61
+ 'streaming',
62
+ 'busy',
63
+ 'starting',
64
+ 'initializing',
65
+ 'waiting_approval',
66
+ ]);
67
+
68
+ /**
69
+ * Session statuses that indicate the session is permanently unavailable.
70
+ * Delivery should be rejected.
71
+ */
72
+ const TERMINAL_DELIVERY_STATUSES = new Set([
73
+ 'stopped',
74
+ 'failed',
75
+ 'terminated',
76
+ 'exited',
77
+ 'closed',
78
+ 'deleted',
79
+ 'error',
80
+ ]);
81
+
82
+ /**
83
+ * Determine whether to deliver immediately, queue, or reject based on session status.
84
+ *
85
+ * This is a pure function — it does not write to any store.
86
+ */
87
+ export function resolveDeliveryDecision(
88
+ sessionStatus: string | undefined,
89
+ opts?: {
90
+ kind?: MeshSessionDeliveryKind;
91
+ /** When true, busy session immediate injection is allowed (provider-specific capability). */
92
+ allowBusyInjection?: boolean;
93
+ },
94
+ ): MeshDeliveryPolicyResult {
95
+ const status = (sessionStatus || '').trim().toLowerCase();
96
+
97
+ if (!status) {
98
+ return {
99
+ decision: 'rejected',
100
+ reason: 'unknown_session_status',
101
+ message: 'Session status is unknown. Delivery rejected (fail-closed). Use mesh_launch_session to start a fresh session.',
102
+ };
103
+ }
104
+
105
+ if (IMMEDIATE_DELIVERY_STATUSES.has(status)) {
106
+ return {
107
+ decision: 'immediate',
108
+ reason: `session_${status}`,
109
+ message: `Session is ${status} — delivery allowed immediately.`,
110
+ };
111
+ }
112
+
113
+ if (BUSY_DELIVERY_STATUSES.has(status)) {
114
+ if (opts?.allowBusyInjection) {
115
+ return {
116
+ decision: 'immediate',
117
+ reason: `session_${status}_busy_injection_allowed`,
118
+ message: `Session is ${status} but provider supports busy injection. Delivered immediately.`,
119
+ };
120
+ }
121
+ // approval-kind may be delivered to waiting_approval sessions
122
+ if (status === 'waiting_approval' && opts?.kind === 'approval') {
123
+ return {
124
+ decision: 'immediate',
125
+ reason: 'session_waiting_approval_approval_message',
126
+ message: 'Session is waiting for approval — approval message delivered immediately.',
127
+ };
128
+ }
129
+ return {
130
+ decision: 'queued',
131
+ reason: `session_${status}_busy`,
132
+ message: `Session is ${status}. Task queued for delivery when session becomes idle. Do not inject directly into a busy session.`,
133
+ };
134
+ }
135
+
136
+ if (TERMINAL_DELIVERY_STATUSES.has(status)) {
137
+ return {
138
+ decision: 'rejected',
139
+ reason: `session_${status}_terminal`,
140
+ message: `Session is ${status} (terminal). Delivery rejected. Launch a new session before dispatching tasks.`,
141
+ };
142
+ }
143
+
144
+ // Unknown/unrecognized status: fail-closed
145
+ return {
146
+ decision: 'rejected',
147
+ reason: 'unrecognized_session_status',
148
+ message: `Session status '${sessionStatus}' is not recognized. Delivery rejected (fail-closed). Inspect session state before retrying.`,
149
+ };
150
+ }
151
+
152
+ export interface SessionDeliveryRecord {
153
+ id: string;
154
+ meshId: string;
155
+ nodeId?: string;
156
+ sessionId?: string;
157
+ providerType?: string;
158
+ taskId?: string;
159
+ kind: MeshSessionDeliveryKind;
160
+ priority: number;
161
+ message: string;
162
+ status: MeshSessionDeliveryStatus;
163
+ deliverAfter?: string;
164
+ expiresAt?: string;
165
+ attemptCount: number;
166
+ sourceCoordinatorSessionId?: string;
167
+ sourceCoordinatorDaemonId?: string;
168
+ lastError?: string;
169
+ createdAt: string;
170
+ updatedAt: string;
171
+ }
172
+
173
+ /**
174
+ * Create a delivery record in the store.
175
+ */
176
+ export function createSessionDelivery(opts: {
177
+ meshId: string;
178
+ nodeId?: string;
179
+ sessionId?: string;
180
+ providerType?: string;
181
+ taskId?: string;
182
+ kind: MeshSessionDeliveryKind;
183
+ message: string;
184
+ status: MeshSessionDeliveryStatus;
185
+ priority?: number;
186
+ deliverAfter?: string;
187
+ expiresAt?: string;
188
+ sourceCoordinatorSessionId?: string;
189
+ sourceCoordinatorDaemonId?: string;
190
+ }): SessionDeliveryRecord {
191
+ const now = new Date().toISOString();
192
+ const id = randomUUID();
193
+ const record: SessionDeliveryRecord = {
194
+ id,
195
+ meshId: opts.meshId,
196
+ nodeId: opts.nodeId,
197
+ sessionId: opts.sessionId,
198
+ providerType: opts.providerType,
199
+ taskId: opts.taskId,
200
+ kind: opts.kind,
201
+ priority: opts.priority ?? 0,
202
+ message: opts.message,
203
+ status: opts.status,
204
+ deliverAfter: opts.deliverAfter,
205
+ expiresAt: opts.expiresAt,
206
+ attemptCount: 0,
207
+ sourceCoordinatorSessionId: opts.sourceCoordinatorSessionId,
208
+ sourceCoordinatorDaemonId: opts.sourceCoordinatorDaemonId,
209
+ createdAt: now,
210
+ updatedAt: now,
211
+ };
212
+ MeshRuntimeStore.getInstance().insertSessionDelivery({
213
+ id,
214
+ meshId: opts.meshId,
215
+ nodeId: opts.nodeId,
216
+ sessionId: opts.sessionId,
217
+ providerType: opts.providerType,
218
+ taskId: opts.taskId,
219
+ kind: opts.kind,
220
+ priority: opts.priority ?? 0,
221
+ message: opts.message,
222
+ status: opts.status,
223
+ deliverAfter: opts.deliverAfter,
224
+ expiresAt: opts.expiresAt,
225
+ sourceCoordinatorSessionId: opts.sourceCoordinatorSessionId,
226
+ sourceCoordinatorDaemonId: opts.sourceCoordinatorDaemonId,
227
+ createdAt: now,
228
+ updatedAt: now,
229
+ });
230
+ return record;
231
+ }
232
+
233
+ /**
234
+ * Update the status of a delivery record.
235
+ */
236
+ export function updateSessionDeliveryStatus(
237
+ id: string,
238
+ status: MeshSessionDeliveryStatus,
239
+ opts?: { lastError?: string; incrementAttempt?: boolean },
240
+ ): void {
241
+ try {
242
+ MeshRuntimeStore.getInstance().updateSessionDeliveryStatus(id, status, opts);
243
+ } catch { /* best-effort */ }
244
+ }
245
+
246
+ /**
247
+ * Get active (non-terminal) deliveries for a mesh, optionally filtered by session.
248
+ */
249
+ export function getActiveSessionDeliveries(meshId: string, sessionId?: string) {
250
+ try {
251
+ return MeshRuntimeStore.getInstance().getActiveSessionDeliveries(meshId, sessionId);
252
+ } catch {
253
+ return [];
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Record a completion conflict diagnostic when a duplicate event points to
259
+ * different task/session than the already-seen event with the same fingerprint.
260
+ */
261
+ export function recordCompletionConflict(opts: {
262
+ meshId: string;
263
+ fingerprint: string;
264
+ conflictingTaskId?: string;
265
+ conflictingSessionId?: string;
266
+ originalTaskId?: string;
267
+ originalSessionId?: string;
268
+ event: string;
269
+ }): void {
270
+ try {
271
+ MeshRuntimeStore.getInstance().recordCompletionConflict({
272
+ id: randomUUID(),
273
+ meshId: opts.meshId,
274
+ fingerprint: opts.fingerprint,
275
+ conflictingTaskId: opts.conflictingTaskId,
276
+ conflictingSessionId: opts.conflictingSessionId,
277
+ originalTaskId: opts.originalTaskId,
278
+ originalSessionId: opts.originalSessionId,
279
+ event: opts.event,
280
+ createdAt: new Date().toISOString(),
281
+ });
282
+ } catch { /* best-effort diagnostics */ }
283
+ }
284
+
285
+ /**
286
+ * Get recent completion conflicts for diagnostic inspection.
287
+ */
288
+ export function getRecentCompletionConflicts(meshId: string, limitMs?: number) {
289
+ try {
290
+ return MeshRuntimeStore.getInstance().getRecentCompletionConflicts(meshId, limitMs);
291
+ } catch {
292
+ return [];
293
+ }
294
+ }
295
+
296
+ export function __clearSessionDeliveriesForTests(meshId: string): void {
297
+ MeshRuntimeStore.getInstance().deleteSessionDeliveries(meshId);
298
+ }
@@ -10,6 +10,7 @@ import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
10
10
  import { buildMeshNodeCapabilityTags, claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches } from './mesh-work-queue.js';
11
11
  import { MeshRuntimeStore } from './mesh-runtime-store.js';
12
12
  import { fastForwardMeshNode } from './mesh-fast-forward.js';
13
+ import { createSessionDelivery, updateSessionDeliveryStatus, recordCompletionConflict } from './mesh-delivery-policy.js';
13
14
 
14
15
  // ---------------------------------------------------------------------------
15
16
  // Remote Node Idle Session Tracking
@@ -499,10 +500,27 @@ function isDuplicateMeshCompletionEvent(args: {
499
500
  timestamp?: number | null;
500
501
  finalSummary?: string;
501
502
  coordinatorDaemonId?: string;
503
+ taskId?: string;
504
+ nodeId?: string;
502
505
  }): boolean {
503
506
  const fingerprint = buildMeshCompletionFingerprint(args);
504
507
  if (!fingerprint) return false;
505
- if (hasFingerprintSeen(fingerprint)) return true;
508
+ if (hasFingerprintSeen(fingerprint)) {
509
+ // Suppressed duplicate — but if we have a taskId and it differs from what the
510
+ // fingerprint was stamped for, record a conflict diagnostic so it doesn't disappear silently.
511
+ // (We can't recover the original taskId from the fingerprint alone, so we record
512
+ // the conflicting taskId/session as a diagnostic for coordinator inspection.)
513
+ if (args.taskId) {
514
+ recordCompletionConflict({
515
+ meshId: args.meshId,
516
+ fingerprint,
517
+ conflictingTaskId: args.taskId,
518
+ conflictingSessionId: args.sessionId,
519
+ event: args.event,
520
+ });
521
+ }
522
+ return true;
523
+ }
506
524
  recordFingerprintSeen(fingerprint);
507
525
  return false;
508
526
  }
@@ -837,13 +855,27 @@ export function tryAssignQueueTask(
837
855
  if (node?.daemonId && components.dispatchMeshCommand) {
838
856
  const isLocalNode = components.cliManager.adapters.has(sessionId);
839
857
  if (!isLocalNode) {
858
+ // Create delivery record before attempting P2P send
859
+ const delivery = createSessionDelivery({
860
+ meshId,
861
+ nodeId,
862
+ sessionId,
863
+ providerType,
864
+ taskId: task.id,
865
+ kind: 'task',
866
+ message: task.message,
867
+ status: 'delivering',
868
+ });
840
869
  components.dispatchMeshCommand(node.daemonId, 'agent_command', {
841
870
  targetSessionId: sessionId,
842
871
  cliType: providerType,
843
872
  action: 'send_chat',
844
873
  message: task.message,
874
+ }).then(() => {
875
+ updateSessionDeliveryStatus(delivery.id, 'delivered');
845
876
  }).catch((e: any) => {
846
877
  LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
878
+ updateSessionDeliveryStatus(delivery.id, 'failed', { lastError: e?.message, incrementAttempt: true });
847
879
  // Revert to pending so the task can be retried rather than permanently failing
848
880
  updateTaskStatus(meshId, task.id, 'pending');
849
881
  try {
@@ -851,7 +883,7 @@ export function tryAssignQueueTask(
851
883
  kind: 'dispatch_failed' as any,
852
884
  nodeId,
853
885
  sessionId,
854
- payload: { taskId: task.id, error: e?.message, retryable: true },
886
+ payload: { taskId: task.id, deliveryId: delivery.id, error: e?.message, retryable: true },
855
887
  });
856
888
  } catch { /* ledger write is best-effort */ }
857
889
  });
@@ -859,14 +891,27 @@ export function tryAssignQueueTask(
859
891
  }
860
892
  }
861
893
 
862
- // Local routing
894
+ // Local routing — create delivery record before send_chat
895
+ const delivery = createSessionDelivery({
896
+ meshId,
897
+ nodeId,
898
+ sessionId,
899
+ providerType,
900
+ taskId: task.id,
901
+ kind: 'task',
902
+ message: task.message,
903
+ status: 'delivering',
904
+ });
863
905
  components.cliManager.handleCliCommand('agent_command', {
864
906
  targetSessionId: sessionId,
865
907
  cliType: providerType,
866
908
  action: 'send_chat',
867
909
  message: task.message,
910
+ }).then(() => {
911
+ updateSessionDeliveryStatus(delivery.id, 'delivered');
868
912
  }).catch((e: any) => {
869
913
  LOG.error('MeshQueue', `Failed to dispatch task locally to node ${nodeId}: ${e?.message}`);
914
+ updateSessionDeliveryStatus(delivery.id, 'failed', { lastError: e?.message, incrementAttempt: true });
870
915
  updateTaskStatus(meshId, task.id, 'failed');
871
916
  });
872
917
 
@@ -1625,6 +1670,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1625
1670
  // Scope dedup to the coordinator daemon so two coordinators for the same mesh
1626
1671
  // don't suppress each other's completion events via shared fingerprint table.
1627
1672
  coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
1673
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
1674
+ nodeId: eventNodeId || undefined,
1628
1675
  });
1629
1676
  if (duplicateCompletion) {
1630
1677
  LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
@@ -1641,6 +1688,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1641
1688
  timestamp: eventTimestamp,
1642
1689
  finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1643
1690
  coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
1691
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
1692
+ nodeId: eventNodeId || undefined,
1644
1693
  });
1645
1694
  if (duplicateStopped) {
1646
1695
  LOG.info('MeshEvents', `Suppressed duplicate stopped event for mesh ${args.meshId} session ${eventSessionId}`);