@adhdev/daemon-core 0.9.82-rc.395 → 0.9.82-rc.396

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.
@@ -34,6 +34,15 @@ export declare function reconcileDirectDispatchCompletionFromTranscript(args: {
34
34
  transcriptMessageAt?: string;
35
35
  completedAt?: string;
36
36
  targetCoordinatorDaemonId?: string;
37
+ /**
38
+ * NOTIF-DROP-SYNTH-NO-MESSAGE: the originating coordinator SESSION that dispatched this
39
+ * task (the `coordinatorSessionId` stamped onto the task_dispatched ledger payload at
40
+ * dispatch). Stamped onto the synthesized completion's targetCoordinatorSessionId so PHASE 2
41
+ * STRICT routing matches the exact coordinator session — instead of relying on the
42
+ * daemon-keyed fallback. When the caller does not pass it, it is recovered from the dispatch
43
+ * ledger entry below; absent on legacy rows → daemon-level routing (unchanged).
44
+ */
45
+ targetCoordinatorSessionId?: string;
37
46
  source?: string;
38
47
  }): {
39
48
  reconciled: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.395",
3
+ "version": "0.9.82-rc.396",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.395",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.396",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -3,7 +3,7 @@ import type { MeshLedgerKind } from './mesh-ledger.js';
3
3
  import { updateDirectDispatchStatus, cleanupTerminalDirectDispatches } from './mesh-work-queue.js';
4
4
  import { markSessionDeliveriesTerminal } from './mesh-delivery-policy.js';
5
5
  import { queuePendingMeshCoordinatorEvent } from './mesh-events-pending.js';
6
- import { readNonEmptyString, readRecord, resolveEventSessionId, readWorkerResultMetadata, isWeakCompletionEvidence } from './mesh-events-utils.js';
6
+ import { readNonEmptyString, readRecord, resolveEventSessionId, readWorkerResultMetadata, isWeakCompletionEvidence, buildMeshSystemMessage } from './mesh-events-utils.js';
7
7
  import { meshNodeIdMatches, type MeshNodeIdentified } from '@adhdev/mesh-shared';
8
8
 
9
9
  // ---------------------------------------------------------------------------
@@ -188,6 +188,15 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
188
188
  transcriptMessageAt?: string;
189
189
  completedAt?: string;
190
190
  targetCoordinatorDaemonId?: string;
191
+ /**
192
+ * NOTIF-DROP-SYNTH-NO-MESSAGE: the originating coordinator SESSION that dispatched this
193
+ * task (the `coordinatorSessionId` stamped onto the task_dispatched ledger payload at
194
+ * dispatch). Stamped onto the synthesized completion's targetCoordinatorSessionId so PHASE 2
195
+ * STRICT routing matches the exact coordinator session — instead of relying on the
196
+ * daemon-keyed fallback. When the caller does not pass it, it is recovered from the dispatch
197
+ * ledger entry below; absent on legacy rows → daemon-level routing (unchanged).
198
+ */
199
+ targetCoordinatorSessionId?: string;
191
200
  source?: string;
192
201
  }): { reconciled: boolean; kind?: MeshLedgerKind; alreadyTerminal?: boolean; workerResult?: unknown; ledgerEntryId?: string; reason?: string } {
193
202
  const finalSummary = readNonEmptyString(args.finalSummary);
@@ -282,27 +291,44 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
282
291
  updateDirectDispatchStatus(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed', args.taskId);
283
292
  markSessionDeliveriesTerminal(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
284
293
  setImmediate(() => cleanupTerminalDirectDispatches());
294
+ // NOTIF-DROP-SYNTH-NO-MESSAGE: the queued synth completion MUST carry a coordinatorMessage,
295
+ // or injectPendingIntoCoordinator early-returns (`!pending.coordinatorMessage`) and the row is
296
+ // drained-without-inject → no [System] surface. The ~8s-later native completion then collides
297
+ // on the taskId-anchored fingerprint and is blocked at INSERT, so the notification is lost
298
+ // forever. Build the SAME [System] message the native path builds (buildMeshSystemMessage) so
299
+ // the synth is itself a complete, deliverable completion. Routing is made STRICT too: stamp the
300
+ // originating coordinator session so PHASE 2 strict-match delivers to the exact coordinator
301
+ // session that dispatched the task instead of falling back to a daemon-level broadcast.
302
+ const eventName = kind === 'task_completed' ? 'agent:generating_completed' : 'agent:stopped';
303
+ const nodeLabel = nodeId ? `Node '${nodeId}'` : 'Remote agent';
304
+ const metadataEvent = {
305
+ targetSessionId: args.sessionId,
306
+ providerType: providerType || undefined,
307
+ providerSessionId: readNonEmptyString(args.providerSessionId),
308
+ finalSummary,
309
+ taskId: args.taskId,
310
+ workerResult,
311
+ completionDiagnostic: {
312
+ reason: 'direct_task_transcript_reconciliation',
313
+ terminalLedgerKind: kind,
314
+ terminalLedgerId: entry.id,
315
+ },
316
+ };
317
+ // Originating coordinator session: prefer the explicit arg; otherwise recover it from the
318
+ // task_dispatched ledger payload (the MCP dispatch path stamps `coordinatorSessionId` there).
319
+ // Absent on legacy rows → undefined → daemon-level routing (unchanged, no regression).
320
+ const targetCoordinatorSessionId = readNonEmptyString(args.targetCoordinatorSessionId)
321
+ || readNonEmptyString(dispatch?.payload?.coordinatorSessionId);
285
322
  queuePendingMeshCoordinatorEvent({
286
- event: kind === 'task_completed' ? 'agent:generating_completed' : 'agent:stopped',
323
+ event: eventName,
287
324
  meshId: args.meshId,
288
- nodeLabel: nodeId ? `Node '${nodeId}'` : 'Remote agent',
325
+ nodeLabel,
289
326
  nodeId: nodeId || undefined,
290
- metadataEvent: {
291
- targetSessionId: args.sessionId,
292
- providerType: providerType || undefined,
293
- providerSessionId: readNonEmptyString(args.providerSessionId),
294
- finalSummary,
295
- taskId: args.taskId,
296
- workerResult,
297
- completionDiagnostic: {
298
- reason: 'direct_task_transcript_reconciliation',
299
- terminalLedgerKind: kind,
300
- terminalLedgerId: entry.id,
301
- },
302
- },
303
- coordinatorMessage: undefined,
327
+ metadataEvent,
328
+ coordinatorMessage: buildMeshSystemMessage({ event: eventName, nodeLabel, metadataEvent }),
304
329
  queuedAt: Date.now(),
305
330
  ...(readNonEmptyString(args.targetCoordinatorDaemonId) ? { targetCoordinatorDaemonId: readNonEmptyString(args.targetCoordinatorDaemonId) } : {}),
331
+ ...(targetCoordinatorSessionId ? { targetCoordinatorSessionId } : {}),
306
332
  });
307
333
 
308
334
  return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
@@ -55,7 +55,7 @@ import {
55
55
  ackUnresolvedDelegateForward,
56
56
  expireStaleUnresolvedDelegateForwards,
57
57
  } from './mesh-unresolved-forward-outbox.js';
58
- import { readNonEmptyString, readMeshCompletionSummary } from './mesh-events-utils.js';
58
+ import { readNonEmptyString, readMeshCompletionSummary, buildMeshSystemMessage } from './mesh-events-utils.js';
59
59
  import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
60
60
  import { expandDaemonIdForms, daemonIdsEquivalent } from '@adhdev/mesh-shared';
61
61
  import { getActiveDirectDispatches, getQueue, reclaimStrandedAssignedTask, updateTaskStatus } from './mesh-work-queue.js';
@@ -368,7 +368,30 @@ function injectPendingIntoCoordinator(
368
368
  coordinator: LiveCoordinator['instance'],
369
369
  pending: PendingMeshCoordinatorEvent,
370
370
  ): void {
371
- if (!coordinator || !pending.coordinatorMessage) return;
371
+ if (!coordinator) return;
372
+ // NOTIF-DROP-SYNTH-NO-MESSAGE (defence-in-depth): a queued event with no coordinatorMessage
373
+ // used to be dropped here (drain-without-inject) — the row had already been consumed
374
+ // (drained=1) by the caller's drain, so silently returning lost it forever. The primary fix
375
+ // makes the transcript-reconcile synth always carry a coordinatorMessage, but as a backstop,
376
+ // lazily synthesize the [System] text for any force-inject (terminal: completion / approval /
377
+ // stop / refine·bootstrap) event that still arrives message-less, so it surfaces instead of
378
+ // vanishing. A NON-force lifecycle event (agent:ready / generating_started) legitimately
379
+ // carries no message and must NOT be injected (it is queued only to re-drive the claim state
380
+ // machine on pull) — for it we still return without injecting.
381
+ let coordinatorMessage = pending.coordinatorMessage;
382
+ if (!coordinatorMessage) {
383
+ if (!shouldForceInjectMeshEvent(pending.event)) return;
384
+ const metadataEvent = pending.metadataEvent && typeof pending.metadataEvent === 'object'
385
+ ? pending.metadataEvent
386
+ : {};
387
+ coordinatorMessage = buildMeshSystemMessage({
388
+ event: pending.event,
389
+ nodeLabel: pending.nodeLabel,
390
+ metadataEvent,
391
+ });
392
+ if (!coordinatorMessage) return; // builder produced nothing — nothing to surface
393
+ LOG.warn('MeshReconcile', `Lazily synthesized missing coordinatorMessage for ${pending.event} (mesh ${pending.meshId}) at inject time — a queued terminal event arrived message-less`);
394
+ }
372
395
  const force = shouldForceInjectMeshEvent(pending.event);
373
396
  // EVTTRACE: event surfaced to the coordinator (injected into its live CLI session).
374
397
  // This is the terminal happy-path stage. Observation only.
@@ -380,7 +403,7 @@ function injectPendingIntoCoordinator(
380
403
  event: pending.event,
381
404
  }, force ? 'force-inject' : 'inject');
382
405
  coordinator.onEvent('send_message', {
383
- input: { text: pending.coordinatorMessage, textFallback: pending.coordinatorMessage },
406
+ input: { text: coordinatorMessage, textFallback: coordinatorMessage },
384
407
  ...(force ? { force: true } : {}),
385
408
  });
386
409
  }