@adhdev/daemon-core 0.9.82-rc.379 → 0.9.82-rc.380

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.379",
3
+ "version": "0.9.82-rc.380",
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.379",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.380",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -310,6 +310,44 @@ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metad
310
310
  return false;
311
311
  }
312
312
 
313
+ // NOTIF-MISS (FIX 2): the set of `source` tags the transcript-reconcile synthesis path stamps
314
+ // onto the terminal ledger entry it writes (mesh-events-stale.reconcileDirectDispatchCompletion
315
+ // FromTranscript). A terminal carrying one of these was NOT produced by an actual provider
316
+ // agent_status_event — it is a coordinator-side reconstruction from the worker's transcript. The
317
+ // authoritative real completion must never be permanently masked by such a synthesized record.
318
+ const RECONCILED_COMPLETION_SOURCES = new Set([
319
+ 'direct_task_transcript_reconciliation',
320
+ 'daemon_reconcile_transcript_completion',
321
+ 'mcp_mesh_status_transcript_reconciliation',
322
+ 'no_progress_reconciliation',
323
+ ]);
324
+
325
+ // True when the recorded terminal ledger entry was SYNTHESIZED by transcript reconciliation
326
+ // rather than emitted by the real provider event. The reconcile path tags its ledger payload
327
+ // with a reconcile `source`; a genuine provider completion carries no such tag (or a normal
328
+ // completionDiagnostic.reason). Both the explicit source AND the diagnostic reason are checked so
329
+ // either carrier identifies the synthesized record.
330
+ function isSynthesizedReconciledTerminal(terminalPayload: Record<string, unknown>): boolean {
331
+ const source = readNonEmptyString(terminalPayload.source);
332
+ if (source && RECONCILED_COMPLETION_SOURCES.has(source)) return true;
333
+ const diagnostic = terminalPayload.completionDiagnostic;
334
+ if (diagnostic && typeof diagnostic === 'object' && !Array.isArray(diagnostic)) {
335
+ const reason = readNonEmptyString((diagnostic as Record<string, unknown>).reason);
336
+ if (reason && RECONCILED_COMPLETION_SOURCES.has(reason)) return true;
337
+ }
338
+ return false;
339
+ }
340
+
341
+ // True when the INCOMING completion event is a REAL provider agent_status_event (an actual
342
+ // agent:generating_completed from the live session) rather than itself a synthesized/reconciled
343
+ // re-injection. We must only let a real event override a synthesized terminal — a second
344
+ // synthesized re-arrival should still dedup normally. A real provider event carries no reconcile
345
+ // `source` tag; a re-injected reconciliation does.
346
+ function isRealProviderCompletionEvent(metadataEvent: Record<string, unknown>): boolean {
347
+ const source = readNonEmptyString(metadataEvent.source);
348
+ return !source || !RECONCILED_COMPLETION_SOURCES.has(source);
349
+ }
350
+
313
351
  // The genuine-completion counterpart: a real final summary / worker result is present and
314
352
  // the completion is not flagged as a missing-final-assistant false idle. Used to decide
315
353
  // whether a new completion may supersede a prior WEAK (false-idle) terminal. NOTE this gates
@@ -516,7 +554,20 @@ function evaluateMeshEventSuppression(
516
554
  terminalTaskId,
517
555
  eventTaskId,
518
556
  });
519
- if (!newDispatchAfterTerminal && !supersedesWeakTerminal && !distinctTaskCompletion && !supersedesTruncatedTerminal) {
557
+ // NOTIF-MISS (FIX 2): the recorded terminal was SYNTHESIZED by transcript reconciliation
558
+ // (no real provider event), and THIS incoming event is the authoritative REAL provider
559
+ // completion. The reconcile path may fire ~1s after a direct dispatch from a reused
560
+ // session's stale transcript tail, writing a synthesized terminal whose providerSessionId
561
+ // (stable across a session's turns) and/or finalSummary then match the real completion —
562
+ // so the providerSessionId/finalSummary dedup below would drop the genuine event and the
563
+ // coordinator would never learn the task finished. A synthesized record must NEVER
564
+ // permanently mask the real completion: let the real event through (it is re-recorded by
565
+ // the normal task_completed path, superseding the synthesized one). A second SYNTHESIZED
566
+ // re-arrival is NOT a real event and still dedups normally.
567
+ const supersedesSynthesizedTerminal = isSynthesizedReconciledTerminal(terminal.payload)
568
+ && isRealProviderCompletionEvent(args.metadataEvent)
569
+ && isGenuineCompletionEvidence(args.metadataEvent);
570
+ if (!newDispatchAfterTerminal && !supersedesWeakTerminal && !distinctTaskCompletion && !supersedesTruncatedTerminal && !supersedesSynthesizedTerminal) {
520
571
  const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
521
572
  const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
522
573
  const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
@@ -1180,8 +1231,14 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
1180
1231
  return { success: false, error: 'meshId required' };
1181
1232
  }
1182
1233
  // EVTTRACE: forwarded event accepted at receive (meshId resolved).
1234
+ // NOTIF-MISS (FIX 3): mirror buildRelayMetadataEvent's taskId fallback. A worker provider
1235
+ // completion stamps its task id as `meshActiveTaskId` (settings → event), not always the
1236
+ // top-level `taskId`, so reading payload.taskId alone rendered `task=-` at the received stage
1237
+ // (the [stage:queued] task=<id> → [stage:received] task=- loss). Falling back to
1238
+ // meshActiveTaskId keeps the trace task-scoped end-to-end so same-task redelivery is
1239
+ // distinguishable from a different task's real completion.
1183
1240
  traceMeshEventStage('received', {
1184
- taskId: payload.taskId,
1241
+ taskId: readNonEmptyString(payload.taskId) || readNonEmptyString(payload.meshActiveTaskId),
1185
1242
  sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
1186
1243
  nodeId,
1187
1244
  meshId,
@@ -10,6 +10,25 @@ import { meshNodeIdMatches, type MeshNodeIdentified } from '@adhdev/mesh-shared'
10
10
  // Stale direct-dispatch detection & transcript reconciliation
11
11
  // ---------------------------------------------------------------------------
12
12
 
13
+ // NOTIF-MISS grace gate. A direct dispatch (mesh_send_task) to an ALREADY-IDLE,
14
+ // previously-used session reuses a transcript that still holds the PRIOR turn's final
15
+ // assistant message. The session momentarily reads `idle` in the first second after
16
+ // dispatch (the worker hasn't started generating yet), so the transcript reconcile here
17
+ // would otherwise synthesize a "completion" ~1s after dispatch from that stale tail —
18
+ // writing a synthesized task_completed + queuing a completion event. When the REAL
19
+ // completion fires minutes later, checkSuppressMeshEvent drops it as a duplicate of the
20
+ // synthesized one (same providerSessionId/finalSummary), so the coordinator never learns
21
+ // the task actually finished. The grace period refuses to synthesize until enough time has
22
+ // passed since dispatch that a genuine same-task completion is plausible, closing the race
23
+ // window. The stale-summary timestamp guard (transcript predates dispatch) and FIX 2 in
24
+ // mesh-event-forwarding (a synthesized completion never suppresses the authoritative real
25
+ // one) are the correctness backstops; this grace just stops the synthesis from firing in
26
+ // the first place for the common case. An idle-session dispatch (dispatchedToIdleSession)
27
+ // is the exact race above, so it gets a LONGER grace than a dispatch to a session that was
28
+ // already generating (where a transcript reconcile is far less likely to be premature).
29
+ const DIRECT_DISPATCH_RECONCILE_GRACE_MS = 60_000;
30
+ const DIRECT_DISPATCH_IDLE_SESSION_RECONCILE_GRACE_MS = 120_000;
31
+
13
32
  export function findRecentTerminalLedgerEvidence(args: {
14
33
  meshId: string;
15
34
  sessionId?: string;
@@ -210,6 +229,29 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
210
229
  if (workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
211
230
  return { reconciled: false, reason: 'transcript_not_proven_after_dispatch' };
212
231
  }
232
+ // NOTIF-MISS grace gate (FIX 1): refuse to synthesize a completion for a direct dispatch
233
+ // until a grace period has elapsed since its dispatch. A direct dispatch to an ALREADY-IDLE,
234
+ // previously-used session reuses a transcript that still holds the PRIOR turn's final
235
+ // assistant message; the session momentarily reads `idle` in the first second after dispatch
236
+ // (the worker hasn't started generating yet), so without this gate the reconcile would
237
+ // synthesize a "completion" ~1s after dispatch from that stale tail. When the REAL completion
238
+ // fires minutes later, checkSuppressMeshEvent drops it as a duplicate of the synthesized one
239
+ // (same providerSessionId/finalSummary) — the coordinator never learns the task finished.
240
+ // The grace refuses synthesis until a genuine same-task completion is plausible. An idle-
241
+ // session dispatch (dispatchedToIdleSession) is the exact race, so it gets a LONGER grace.
242
+ // A structured final_summary_json is self-attributing (the provider emitted it for THIS turn),
243
+ // so it is exempt; the grace only guards the ambiguous plain-text-tail case. When the dispatch
244
+ // entry is missing we cannot date it and do NOT block (the stale-summary timestamp guard +
245
+ // FIX 2 supersession remain the correctness backstops).
246
+ if (Number.isFinite(dispatchTime) && workerResult.source !== 'final_summary_json') {
247
+ const dispatchedToIdleSession = dispatch?.payload?.dispatchedToIdleSession === true;
248
+ const graceMs = dispatchedToIdleSession
249
+ ? DIRECT_DISPATCH_IDLE_SESSION_RECONCILE_GRACE_MS
250
+ : DIRECT_DISPATCH_RECONCILE_GRACE_MS;
251
+ if (Date.now() - dispatchTime < graceMs) {
252
+ return { reconciled: false, reason: 'direct_dispatch_grace_period' };
253
+ }
254
+ }
213
255
  const workerFailed = workerResult.status === 'failed' || (workerResult.status !== 'completed' && workerResult.errors.length > 0);
214
256
  const kind: MeshLedgerKind = workerFailed ? 'task_failed' : 'task_completed';
215
257
 
@@ -1291,6 +1291,17 @@ function buildForwardPayloadFromPending(event: any): Record<string, unknown> {
1291
1291
  ? { targetCoordinatorSessionId: readNonEmptyString(event.targetCoordinatorSessionId) }
1292
1292
  : {}),
1293
1293
  ...metadata,
1294
+ // NOTIF-MISS (FIX 3): surface the dispatch task id at the TOP LEVEL so the relay's
1295
+ // received-stage trace (and buildRelayMetadataEvent) recovers it regardless of which
1296
+ // carrier the producing daemon used. The metadata spread above may carry the id only as
1297
+ // `meshActiveTaskId` (a worker provider event), leaving top-level `taskId` unset and the
1298
+ // received stage rendering `task=-`. Resolve both carriers into an explicit `taskId` so
1299
+ // dedup stays task-scoped end-to-end. Only set when a non-empty id exists (no clobber to
1300
+ // undefined when neither is present).
1301
+ ...((): Record<string, unknown> => {
1302
+ const tid = readNonEmptyString(metadata.taskId) || readNonEmptyString(metadata.meshActiveTaskId);
1303
+ return tid ? { taskId: tid } : {};
1304
+ })(),
1294
1305
  };
1295
1306
  }
1296
1307