bulltrackers-module 1.0.640 → 1.0.642

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.
@@ -18,7 +18,28 @@ const crypto = require('crypto');
18
18
  const OOM_THRESHOLD_MB = 1500; // Unused
19
19
  const BASE_SECONDS_PER_WEIGHT_UNIT = 3;
20
20
  const SESSION_CACHE_DURATION_MS = 1000 * 60 * 30; // 30 Minutes
21
- const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15;
21
+ const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15;
22
+
23
+ // =============================================================================
24
+ // HELPER: Firestore Timestamp Conversion
25
+ // =============================================================================
26
+ /**
27
+ * Converts a Firestore Timestamp or Date to milliseconds.
28
+ * Firestore stores Date objects as Timestamp objects, which have a .toDate() method.
29
+ * This function handles both cases correctly.
30
+ * @param {any} field - Firestore Timestamp, Date object, or string
31
+ * @returns {number} Milliseconds since epoch, or 0 if invalid
32
+ */
33
+ function getMillis(field) {
34
+ if (!field) return 0;
35
+ // Handle Firestore Timestamp (has .toDate() method)
36
+ if (field.toDate && typeof field.toDate === 'function') {
37
+ return field.toDate().getTime();
38
+ }
39
+ // Handle standard Date object or string
40
+ const date = new Date(field);
41
+ return isNaN(date.getTime()) ? 0 : date.getTime();
42
+ }
22
43
 
23
44
  // =============================================================================
24
45
  // HELPER: Schedule Logic
@@ -73,9 +94,7 @@ async function filterActiveTasks(db, date, pass, tasks, logger, forceRun = false
73
94
  const isActive = ['PENDING', 'IN_PROGRESS'].includes(data.status);
74
95
 
75
96
  if (isActive) {
76
- const lastActivityTime = data.telemetry?.lastHeartbeat
77
- ? new Date(data.telemetry.lastHeartbeat).getTime()
78
- : (data.startedAt ? new Date(data.startedAt).getTime() : 0);
97
+ const lastActivityTime = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
79
98
 
80
99
  if ((Date.now() - lastActivityTime) > STALE_LOCK_THRESHOLD_MS) {
81
100
  if (logger) logger.log('WARN', `[Dispatcher] 🧟 Breaking stale lock for ${taskName}.`);
@@ -428,9 +447,7 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
428
447
 
429
448
  // 1. ACTIVE CHECK: Don't double-dispatch if already running... UNLESS IT'S A ZOMBIE
430
449
  if (['PENDING', 'IN_PROGRESS'].includes(data.status)) {
431
- const lastActivity = data.telemetry?.lastHeartbeat
432
- ? new Date(data.telemetry.lastHeartbeat).getTime()
433
- : (data.startedAt ? new Date(data.startedAt).getTime() : 0);
450
+ const lastActivity = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
434
451
 
435
452
  // If it's been silent for > 15 mins, it's a Zombie. Kill it and Re-run.
436
453
  if ((Date.now() - lastActivity) > STALE_LOCK_THRESHOLD_MS) {
@@ -566,6 +583,10 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
566
583
 
567
584
  // Optimization: If nothing is scheduled for today, skip expensive DB checks
568
585
  if (scheduledComputations.length === 0) {
586
+ // DEBUG: Log when schedule filtering removes all tasks
587
+ if (calcsInThisPass.length > 0) {
588
+ logger.log('TRACE', `[Dispatcher] Date ${selectedDate}: ${calcsInThisPass.length} pass computations, but 0 scheduled for this date. Skipping.`);
589
+ }
569
590
  currentCursor++;
570
591
  continue;
571
592
  }
@@ -587,6 +608,11 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
587
608
  checkRootDataAvailability(selectedDate, config, dependencies, DEFINITIVE_EARLIEST_DATES)
588
609
  ]);
589
610
 
611
+ // DEBUG: Log availability check
612
+ if (!availability || !availability.status) {
613
+ logger.log('WARN', `[Dispatcher] ⚠️ Date ${selectedDate}: Availability check failed or returned null. Skipping analysis.`);
614
+ }
615
+
590
616
  if (availability && availability.status) {
591
617
  const report = analyzeDateExecution(
592
618
  selectedDate,
@@ -597,19 +623,37 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
597
623
  prevDailyStatus
598
624
  );
599
625
  let rawTasks = [...report.runnable, ...report.reRuns];
626
+
627
+ // DEBUG: Log analysis results
628
+ if (rawTasks.length === 0 && (report.runnable.length > 0 || report.reRuns.length > 0)) {
629
+ logger.log('WARN', `[Dispatcher] ⚠️ Date ${selectedDate}: analyzeDateExecution found ${report.runnable.length} runnable + ${report.reRuns.length} reRuns, but rawTasks is empty!`);
630
+ }
631
+ if (rawTasks.length > 0) {
632
+ logger.log('TRACE', `[Dispatcher] Date ${selectedDate}: analyzeDateExecution found ${report.runnable.length} runnable, ${report.reRuns.length} reRuns. Total: ${rawTasks.length}`);
633
+ }
600
634
 
601
635
  if (rawTasks.length > 0) {
602
636
  rawTasks = await attemptSimHashResolution(dependencies, selectedDate, rawTasks, dailyStatus, manifestMap);
603
637
  const activeTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks, logger);
604
638
 
605
639
  if (activeTasks.length > 0) {
640
+ // DEBUG: Log what we're about to route
641
+ logger.log('INFO', `[Dispatcher] 🔍 Date ${selectedDate}: ${rawTasks.length} raw tasks → ${activeTasks.length} after filtering. Routing...`);
606
642
  const { standard, highMem } = await splitRoutes(db, selectedDate, passToRun, activeTasks, logger);
607
643
  selectedTasks = [...standard, ...highMem];
608
644
 
645
+ // DEBUG: Log routing results
646
+ if (selectedTasks.length === 0 && activeTasks.length > 0) {
647
+ logger.log('WARN', `[Dispatcher] ⚠️ Date ${selectedDate}: ${activeTasks.length} tasks filtered out by splitRoutes! Tasks: ${activeTasks.map(t => t.name).join(', ')}`);
648
+ }
649
+
609
650
  if (selectedTasks.length > 0) {
610
651
  // Found work! Break loop to dispatch.
611
652
  break;
612
653
  }
654
+ } else if (rawTasks.length > 0) {
655
+ // DEBUG: Log if filterActiveTasks removed all tasks
656
+ logger.log('WARN', `[Dispatcher] ⚠️ Date ${selectedDate}: ${rawTasks.length} raw tasks all filtered out by filterActiveTasks! Tasks: ${rawTasks.map(t => t.name).join(', ')}`);
613
657
  }
614
658
  }
615
659
  }
@@ -734,16 +778,31 @@ async function splitRoutes(db, date, pass, tasks, logger) {
734
778
  const doc = await db.doc(ledgerPath).get();
735
779
 
736
780
  if (!doc.exists) {
781
+ // No ledger entry - trust analyzeDateExecution, dispatch as standard
737
782
  standard.push(task);
738
783
  continue;
739
784
  }
740
785
 
741
786
  const data = doc.data();
742
787
 
743
- // CHECK: Is the task currently running or pending?
788
+ // CRITICAL FIX: If analyzeDateExecution says this task should run, we MUST trust it.
789
+ // The ledger might say COMPLETED, but if computation_status is missing/outdated,
790
+ // we need to re-run to repair the state. Only skip if actively running.
791
+ // Note: filterActiveTasks already filtered out non-stale PENDING/IN_PROGRESS,
792
+ // but we double-check here in case of race conditions.
744
793
  if (['PENDING', 'IN_PROGRESS'].includes(data.status)) {
745
- // It's active, don't dispatch again
746
- continue;
794
+ // Check if it's stale (should have been caught by filterActiveTasks, but double-check)
795
+ const lastActivityTime = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
796
+
797
+ if ((Date.now() - lastActivityTime) > STALE_LOCK_THRESHOLD_MS) {
798
+ // Stale lock - break it and continue
799
+ logger.log('WARN', `[Dispatcher] 🧟 splitRoutes: Breaking stale lock for ${name}.`);
800
+ // Fall through to handle as if no active lock
801
+ } else {
802
+ // Valid active lock - skip (shouldn't happen if filterActiveTasks worked correctly)
803
+ logger.log('TRACE', `[Dispatcher] splitRoutes: Skipping ${name} - Valid IN_PROGRESS (should have been filtered earlier).`);
804
+ continue;
805
+ }
747
806
  }
748
807
 
749
808
  if (data.status === 'FAILED') {
@@ -794,8 +853,12 @@ async function splitRoutes(db, date, pass, tasks, logger) {
794
853
  });
795
854
 
796
855
  } else {
797
- // Status is likely COMPLETED (but filterActiveTasks should have caught this)
798
- // or some other state. Default to standard if we are here.
856
+ // Status is likely COMPLETED or some other state.
857
+ // CRITICAL: If analyzeDateExecution says this should run, we MUST trust it.
858
+ // The ledger might show COMPLETED, but if computation_status is missing/outdated,
859
+ // we need to re-run to repair the state. This is the "ghost state fix" logic.
860
+ // Trust the Brain (analyzeDateExecution) over the Log (ledger).
861
+ logger.log('INFO', `[Dispatcher] 🔄 splitRoutes: ${name} has ledger status '${data.status}', but analyzeDateExecution says it should run. Trusting analysis and dispatching.`);
799
862
  standard.push(task);
800
863
  }
801
864
  }
@@ -399,35 +399,35 @@ async function generateBuildReport(config, dependencies, manifest) {
399
399
  trueReRuns.forEach(item => processItem(dateSummary.rerun, item, "Logic Changed"));
400
400
  stableUpdates.forEach(item => processItem(dateSummary.stable, item, "Logic Stable"));
401
401
 
402
- // Auto-Heal Status if Stable
403
- if (stableUpdates.length > 0) {
404
- const updatesPayload = {};
405
- for (const stable of stableUpdates) {
406
- const m = manifestMap.get(stable.name);
407
- const stored = dailyStatus[stable.name];
408
-
409
- // [FIX] Only auto-heal if we have valid result data
410
- // Otherwise, force re-run to regenerate everything properly
411
- const hasValidResults = stored?.resultHash &&
412
- stored.resultHash !== 'empty' &&
413
- stored.dependencyResultHashes &&
414
- Object.keys(stored.dependencyResultHashes).length > 0;
415
-
416
- if (m && stored && hasValidResults) {
417
- updatesPayload[stable.name] = {
418
- hash: m.hash, simHash: stable.simHash, resultHash: stored.resultHash,
419
- dependencyResultHashes: stored.dependencyResultHashes,
420
- category: m.category, composition: m.composition, lastUpdated: new Date()
421
- };
422
- } else {
423
- // No valid results - treat as true re-run
424
- processItem(dateSummary.rerun, stable, "Epoch Change - Rebuilding");
402
+ // Auto-Heal Status if Stable
403
+ if (stableUpdates.length > 0) {
404
+ const updatesPayload = {};
405
+ for (const stable of stableUpdates) {
406
+ const m = manifestMap.get(stable.name);
407
+ const stored = dailyStatus[stable.name];
408
+
409
+ // [FIX] Only auto-heal if we have valid result data
410
+ // Otherwise, force re-run to regenerate everything properly
411
+ const hasValidResults = stored?.resultHash &&
412
+ stored.resultHash !== 'empty' &&
413
+ stored.dependencyResultHashes &&
414
+ Object.keys(stored.dependencyResultHashes).length > 0;
415
+
416
+ if (m && stored && hasValidResults) {
417
+ updatesPayload[stable.name] = {
418
+ hash: m.hash, simHash: stable.simHash, resultHash: stored.resultHash,
419
+ dependencyResultHashes: stored.dependencyResultHashes,
420
+ category: m.category, composition: m.composition, lastUpdated: new Date()
421
+ };
422
+ } else {
423
+ // No valid results - treat as true re-run
424
+ processItem(dateSummary.rerun, stable, "Epoch Change - Rebuilding");
425
+ }
425
426
  }
426
- }
427
- if (Object.keys(updatesPayload).length > 0) {
428
- await updateComputationStatus(dateStr, updatesPayload, config, dependencies);
429
- }
430
- }
427
+ if (Object.keys(updatesPayload).length > 0) {
428
+ await updateComputationStatus(dateStr, updatesPayload, config, dependencies);
429
+ }
430
+ }}
431
431
 
432
432
  // Add skipped items to Stable count for metrics
433
433
  analysis.skipped.forEach(item => processItem(dateSummary.stable, item, "Up To Date"));
@@ -176,7 +176,16 @@ class FinalSweepReporter {
176
176
  forensics.ledgerState = ledgerState;
177
177
 
178
178
  if (['PENDING', 'IN_PROGRESS'].includes(data.status)) {
179
- const lastHb = data.telemetry?.lastHeartbeat ? new Date(data.telemetry.lastHeartbeat).getTime() : 0;
179
+ // CRITICAL FIX: Handle Firestore Timestamp objects correctly
180
+ const getMillis = (field) => {
181
+ if (!field) return 0;
182
+ if (field.toDate && typeof field.toDate === 'function') {
183
+ return field.toDate().getTime();
184
+ }
185
+ const date = new Date(field);
186
+ return isNaN(date.getTime()) ? 0 : date.getTime();
187
+ };
188
+ const lastHb = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
180
189
  if (Date.now() - lastHb > STALE_THRESHOLD_MS) {
181
190
  forensics.rootCause = 'ZOMBIE_PROCESS';
182
191
  forensics.reason = `Worker ${data.workerId} stopped heartbeating. Likely crashed/timeout.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.640",
3
+ "version": "1.0.642",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [