@adhdev/daemon-core 0.9.82-rc.24 → 0.9.82-rc.25

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.
@@ -26,12 +26,21 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
26
26
  forwarded: number;
27
27
  suppressed: boolean;
28
28
  intentionalCleanupStop: boolean;
29
+ duplicateCompletion?: undefined;
30
+ error?: undefined;
31
+ } | {
32
+ success: boolean;
33
+ forwarded: number;
34
+ suppressed: boolean;
35
+ duplicateCompletion: boolean;
36
+ intentionalCleanupStop?: undefined;
29
37
  error?: undefined;
30
38
  } | {
31
39
  success: boolean;
32
40
  forwarded: number;
33
41
  suppressed?: undefined;
34
42
  intentionalCleanupStop?: undefined;
43
+ duplicateCompletion?: undefined;
35
44
  error?: undefined;
36
45
  } | {
37
46
  success: boolean;
@@ -80,7 +80,9 @@ export declare function requeueTask(meshId: string, taskId: string, opts?: {
80
80
  /**
81
81
  * Update the status of the task currently assigned to a specific session.
82
82
  */
83
- export declare function updateSessionTaskStatus(meshId: string, sessionId: string, status: MeshTaskStatus): MeshWorkQueueEntry | null;
83
+ export declare function updateSessionTaskStatus(meshId: string, sessionId: string, status: MeshTaskStatus, opts?: {
84
+ occurredAt?: string;
85
+ }): MeshWorkQueueEntry | null;
84
86
  export interface MeshWorkQueueStats {
85
87
  total: number;
86
88
  active: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.24",
3
+ "version": "0.9.82-rc.25",
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",
@@ -189,6 +189,62 @@ function shouldSuppressIntentionalCleanupStop(args: {
189
189
  return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
190
190
  }
191
191
 
192
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
193
+ const recentCompletionFingerprints = new Map<string, number>();
194
+
195
+ function readEventTimestamp(value: unknown): number | null {
196
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
197
+ if (typeof value === 'string' && value.trim()) {
198
+ const numeric = Number(value);
199
+ if (Number.isFinite(numeric)) return numeric;
200
+ const parsed = Date.parse(value);
201
+ if (Number.isFinite(parsed)) return parsed;
202
+ }
203
+ return null;
204
+ }
205
+
206
+ function buildMeshCompletionFingerprint(args: {
207
+ meshId: string;
208
+ event: string;
209
+ sessionId: string;
210
+ providerType?: string;
211
+ providerSessionId?: string;
212
+ timestamp?: number | null;
213
+ finalSummary?: string;
214
+ }): string {
215
+ const timestampPart = Number.isFinite(args.timestamp)
216
+ ? String(args.timestamp)
217
+ : readNonEmptyString(args.finalSummary).slice(0, 200);
218
+ return [
219
+ args.meshId,
220
+ args.event,
221
+ args.sessionId,
222
+ args.providerType || '',
223
+ args.providerSessionId || '',
224
+ timestampPart,
225
+ ].join('::');
226
+ }
227
+
228
+ function isDuplicateMeshCompletionEvent(args: {
229
+ meshId: string;
230
+ event: string;
231
+ sessionId: string;
232
+ providerType?: string;
233
+ providerSessionId?: string;
234
+ timestamp?: number | null;
235
+ finalSummary?: string;
236
+ }): boolean {
237
+ const fingerprint = buildMeshCompletionFingerprint(args);
238
+ if (!fingerprint) return false;
239
+ const now = Date.now();
240
+ for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
241
+ if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
242
+ }
243
+ if (recentCompletionFingerprints.has(fingerprint)) return true;
244
+ recentCompletionFingerprints.set(fingerprint, now);
245
+ return false;
246
+ }
247
+
192
248
 
193
249
  export function tryAssignQueueTask(
194
250
  components: DaemonComponents,
@@ -651,6 +707,23 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
651
707
  return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
652
708
  }
653
709
 
710
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
711
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
712
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
713
+ meshId: args.meshId,
714
+ event: args.event,
715
+ sessionId: eventSessionId,
716
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
717
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
718
+ timestamp: eventTimestamp,
719
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
720
+ });
721
+ if (duplicateCompletion) {
722
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
723
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
724
+ }
725
+ }
726
+
654
727
  // ── Task Queue & Ledger ──
655
728
  let completedTaskForLedger: { id?: string } | null = null;
656
729
  if (args.event === 'agent:generating_completed') {
@@ -659,7 +732,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
659
732
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
660
733
 
661
734
  if (sessionId) {
662
- const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
735
+ const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
736
+ occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
737
+ });
663
738
  completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
664
739
  if (nodeId && providerType) {
665
740
  // Queue state is already updated above; setImmediate avoids the
@@ -896,6 +971,7 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
896
971
  providerType: readNonEmptyString(payload.providerType),
897
972
  providerSessionId: readNonEmptyString(payload.providerSessionId),
898
973
  finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
974
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
899
975
  intentional: payload.intentional === true,
900
976
  intentionalStop: payload.intentionalStop === true,
901
977
  operatorCleanup: payload.operatorCleanup === true,
@@ -263,16 +263,19 @@ export function updateSessionTaskStatus(
263
263
  meshId: string,
264
264
  sessionId: string,
265
265
  status: MeshTaskStatus,
266
+ opts?: { occurredAt?: string },
266
267
  ): MeshWorkQueueEntry | null {
267
268
  return withQueueLock(meshId, () => {
268
269
  const queue = readQueue(meshId);
270
+ const occurredAtTime = opts?.occurredAt ? new Date(opts.occurredAt).getTime() : Number.NaN;
271
+ const hasOccurredAt = Number.isFinite(occurredAtTime);
269
272
  let bestIdx = -1;
270
273
  let bestTime = 0;
271
274
  for (let i = queue.length - 1; i >= 0; i--) {
272
- if (queue[i].assignedSessionId === sessionId && queue[i].status === 'assigned') {
273
- const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
274
- if (time > bestTime) { bestTime = time; bestIdx = i; }
275
- }
275
+ if (queue[i].assignedSessionId !== sessionId || queue[i].status !== 'assigned') continue;
276
+ const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
277
+ if (hasOccurredAt && Number.isFinite(time) && time > occurredAtTime) continue;
278
+ if (time > bestTime) { bestTime = time; bestIdx = i; }
276
279
  }
277
280
  if (bestIdx === -1) return null;
278
281
  queue[bestIdx].status = status;