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

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.26",
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",
@@ -137,60 +137,11 @@ function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
137
137
  return submodules.length > 0 ? submodules : undefined;
138
138
  }
139
139
 
140
- function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
141
- const cachedStatus = readObjectRecord(node?.cachedStatus);
142
- const cachedGit = readObjectRecord(cachedStatus.git);
143
- if (Object.keys(cachedGit).length) {
144
- const conflictFiles = Array.isArray(cachedGit.conflictFiles)
145
- ? cachedGit.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
146
- : [];
147
- const conflictCount = readNumberValue(cachedGit.conflicts) ?? conflictFiles.length;
148
- const hasConflicts = readBooleanValue(cachedGit.hasConflicts) ?? conflictCount > 0;
149
- const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
150
- if (isGitRepo !== undefined) {
151
- const submodules = readGitSubmodules(cachedGit.submodules);
152
- return {
153
- workspace: readStringValue(cachedGit.workspace, node?.workspace) || '',
154
- repoRoot: readStringValue(cachedGit.repoRoot, node?.repoRoot, node?.workspace) || null,
155
- isGitRepo,
156
- branch: readStringValue(cachedGit.branch) ?? null,
157
- headCommit: readStringValue(cachedGit.headCommit) ?? null,
158
- headMessage: readStringValue(cachedGit.headMessage) ?? null,
159
- upstream: readStringValue(cachedGit.upstream) ?? null,
160
- ahead: readNumberValue(cachedGit.ahead) ?? 0,
161
- behind: readNumberValue(cachedGit.behind) ?? 0,
162
- staged: readNumberValue(cachedGit.staged) ?? 0,
163
- modified: readNumberValue(cachedGit.modified) ?? 0,
164
- untracked: readNumberValue(cachedGit.untracked) ?? 0,
165
- deleted: readNumberValue(cachedGit.deleted) ?? 0,
166
- renamed: readNumberValue(cachedGit.renamed) ?? 0,
167
- hasConflicts,
168
- conflictFiles,
169
- stashCount: readNumberValue(cachedGit.stashCount) ?? 0,
170
- lastCheckedAt: readNumberValue(cachedGit.lastCheckedAt) ?? Date.now(),
171
- ...(submodules ? { submodules } : {}),
172
- };
173
- }
174
- }
175
-
176
- const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
177
- const gitResult = readObjectRecord(rawGit.result);
178
- const directStatus = readObjectRecord(rawGit.status);
179
- const nestedStatus = readObjectRecord(gitResult.status);
180
- const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
181
- const probeGit = readObjectRecord(rawProbe.git);
182
- const probeGitResult = readObjectRecord(probeGit.result);
183
- const probeDirectStatus = readObjectRecord(probeGit.status);
184
- const probeNestedStatus = readObjectRecord(probeGitResult.status);
185
- const status = Object.keys(directStatus).length
186
- ? directStatus
187
- : Object.keys(nestedStatus).length
188
- ? nestedStatus
189
- : Object.keys(probeDirectStatus).length
190
- ? probeDirectStatus
191
- : Object.keys(probeNestedStatus).length
192
- ? probeNestedStatus
193
- : {};
140
+ function normalizeInlineMeshGitStatus(
141
+ status: Record<string, unknown>,
142
+ node: any,
143
+ options?: { lastCheckedAt?: number },
144
+ ): Record<string, unknown> | undefined {
194
145
  const isGitRepo = readBooleanValue(status.isGitRepo);
195
146
  if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
196
147
  const conflictFiles = Array.isArray(status.conflictFiles)
@@ -217,11 +168,43 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
217
168
  hasConflicts,
218
169
  conflictFiles,
219
170
  stashCount: readNumberValue(status.stashCount) ?? 0,
220
- lastCheckedAt: Date.now(),
171
+ lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
221
172
  ...(submodules ? { submodules } : {}),
222
173
  };
223
174
  }
224
175
 
176
+ function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
177
+ const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
178
+ const gitResult = readObjectRecord(rawGit.result);
179
+ const directStatus = readObjectRecord(rawGit.status);
180
+ const nestedStatus = readObjectRecord(gitResult.status);
181
+ const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
182
+ const probeGit = readObjectRecord(rawProbe.git);
183
+ const probeGitResult = readObjectRecord(probeGit.result);
184
+ const probeDirectStatus = readObjectRecord(probeGit.status);
185
+ const probeNestedStatus = readObjectRecord(probeGitResult.status);
186
+ const status = Object.keys(directStatus).length
187
+ ? directStatus
188
+ : Object.keys(nestedStatus).length
189
+ ? nestedStatus
190
+ : Object.keys(probeDirectStatus).length
191
+ ? probeDirectStatus
192
+ : Object.keys(probeNestedStatus).length
193
+ ? probeNestedStatus
194
+ : {};
195
+ return normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
196
+ }
197
+
198
+ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
199
+ const liveGit = buildInlineMeshTransitGitStatus(node);
200
+ if (liveGit) return liveGit;
201
+
202
+ const cachedStatus = readObjectRecord(node?.cachedStatus);
203
+ const cachedGit = readObjectRecord(cachedStatus.git);
204
+ if (!Object.keys(cachedGit).length) return undefined;
205
+ return normalizeInlineMeshGitStatus(cachedGit, node);
206
+ }
207
+
225
208
  function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
226
209
  const cachedStatus = readObjectRecord(node?.cachedStatus);
227
210
  if (!Object.keys(cachedStatus).length) return false;
@@ -518,9 +501,10 @@ function collectLiveMeshSessionRecords(args: {
518
501
 
519
502
  function applyCachedInlineMeshNodeStatus(status: Record<string, unknown>, node: any): boolean {
520
503
  const cachedStatus = readObjectRecord(node?.cachedStatus);
521
- const git = buildCachedInlineMeshGitStatus(node);
522
- const error = readStringValue(cachedStatus.error, node?.error);
523
- const health = readStringValue(cachedStatus.health, node?.health);
504
+ const liveGit = buildInlineMeshTransitGitStatus(node);
505
+ const git = liveGit ?? buildCachedInlineMeshGitStatus(node);
506
+ const error = liveGit ? undefined : readStringValue(cachedStatus.error, node?.error);
507
+ const health = liveGit ? undefined : readStringValue(cachedStatus.health, node?.health);
524
508
  const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
525
509
  const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
526
510
  const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
@@ -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;