moltedopus 1.9.1 → 1.9.3

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.
Files changed (2) hide show
  1. package/lib/heartbeat.js +52 -3
  2. package/package.json +1 -1
package/lib/heartbeat.js CHANGED
@@ -55,7 +55,7 @@
55
55
  * Restart hint → stdout as: RESTART:moltedopus [flags]
56
56
  */
57
57
 
58
- const VERSION = '1.9.1';
58
+ const VERSION = '1.9.3';
59
59
 
60
60
  // ============================================================
61
61
  // IMPORTS (zero dependencies — Node.js built-ins only)
@@ -519,9 +519,11 @@ function fmtMsg(msg, maxLen) {
519
519
  }
520
520
 
521
521
  async function processActions(actions, heartbeatData, args, roomsFilter) {
522
+ let realActionCount = 0;
523
+
522
524
  if (args.json) {
523
525
  console.log(JSON.stringify(heartbeatData));
524
- return;
526
+ return actions.length;
525
527
  }
526
528
 
527
529
  for (const action of actions) {
@@ -569,6 +571,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
569
571
  unread,
570
572
  messages,
571
573
  }));
574
+ realActionCount++;
572
575
 
573
576
  // Mark as read AFTER outputting — messages are now in the output file
574
577
  // Even if Claude doesn't process them, they're captured in the output
@@ -605,6 +608,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
605
608
  unread: action.unread || messages.length,
606
609
  messages,
607
610
  }));
611
+ realActionCount++;
608
612
  break;
609
613
  }
610
614
 
@@ -612,6 +616,14 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
612
616
  const data = await fetchMentions();
613
617
  const mentions = data?.mentions || [];
614
618
 
619
+ // Phantom mentions: server counted unread but fetch returned empty
620
+ // Force mark-read to clear stale state and skip ACTION output
621
+ if (mentions.length === 0) {
622
+ log(` WARN: Phantom mentions detected (server unread=${action.unread || '?'}, fetched=0). Clearing stale state.`);
623
+ await markMentionsRead();
624
+ break; // Don't count as real action — continue polling
625
+ }
626
+
615
627
  // Rich output
616
628
  log('');
617
629
  log(`── MENTIONS: ${mentions.length} unread ──`);
@@ -630,6 +642,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
630
642
  unread: action.unread || mentions.length,
631
643
  mentions,
632
644
  }));
645
+ realActionCount++;
633
646
 
634
647
  // Mark read AFTER emitting ACTION — if parent crashes, mentions won't be lost
635
648
  await markMentionsRead();
@@ -655,6 +668,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
655
668
  pending: action.pending || queue.length,
656
669
  assignments: queue,
657
670
  }));
671
+ realActionCount++;
658
672
  break;
659
673
  }
660
674
 
@@ -676,6 +690,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
676
690
  count: action.count || tasks.length,
677
691
  tasks,
678
692
  }));
693
+ realActionCount++;
679
694
  break;
680
695
  }
681
696
 
@@ -695,6 +710,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
695
710
  pending: action.pending || requests.length,
696
711
  requests,
697
712
  }));
713
+ realActionCount++;
698
714
  break;
699
715
  }
700
716
 
@@ -713,6 +729,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
713
729
  count: action.count || steps.length,
714
730
  steps,
715
731
  }));
732
+ realActionCount++;
716
733
  break;
717
734
  }
718
735
 
@@ -751,16 +768,20 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
751
768
  reply_endpoint: action.reply || `POST ${BASE_URL}/chat/${chatId}/agent-reply`,
752
769
  messages,
753
770
  }));
771
+ realActionCount++;
754
772
  break;
755
773
  }
756
774
 
757
775
  default: {
758
776
  log(` >> ${type}: (passthrough)`);
759
777
  console.log('ACTION:' + JSON.stringify(action));
778
+ realActionCount++;
760
779
  break;
761
780
  }
762
781
  }
763
782
  }
783
+
784
+ return realActionCount;
764
785
  }
765
786
 
766
787
  // ============================================================
@@ -2378,6 +2399,23 @@ async function heartbeatLoop(args, savedConfig) {
2378
2399
  }
2379
2400
  retries = 0;
2380
2401
 
2402
+ // ── CLI version gate — server enforces minimum version ──
2403
+ if (data.update_required) {
2404
+ const u = data.update_required;
2405
+ log('');
2406
+ log('╔══════════════════════════════════════════════════════════════╗');
2407
+ log('║ UPDATE REQUIRED ║');
2408
+ log('╚══════════════════════════════════════════════════════════════╝');
2409
+ log(` Your version: v${u.current}`);
2410
+ log(` Required: v${u.minimum}`);
2411
+ log(` Run this command: ${u.command}`);
2412
+ log(` Then restart: moltedopus --start`);
2413
+ log('');
2414
+ log(u.message || 'Update required before continuing.');
2415
+ log('');
2416
+ process.exit(2); // Exit code 2 = update required
2417
+ }
2418
+
2381
2419
  // Extract heartbeat fields
2382
2420
  const statusMode = data.status_mode || 'available';
2383
2421
  const statusText = data.status_text || '';
@@ -2601,7 +2639,18 @@ async function heartbeatLoop(args, savedConfig) {
2601
2639
 
2602
2640
  log(`BREAK | ${allToProcess.length} action(s) [${types.join(', ')}]${hasBoss ? ' [BOSS]' : ''} (triggered by: ${breakingActions.map(a => a.type).join(', ')})`);
2603
2641
 
2604
- await processActions(allToProcess, data, args, roomsFilter);
2642
+ const realCount = await processActions(allToProcess, data, args, roomsFilter);
2643
+
2644
+ // If all actions were phantom (e.g. stale mentions with empty fetch),
2645
+ // don't break — continue polling. The stale state has been cleared.
2646
+ if (realCount === 0) {
2647
+ log(' All actions were phantom/stale — continuing poll...');
2648
+ if (!noAutoStatus) {
2649
+ await setStatus('available', '');
2650
+ log('Auto-status: available');
2651
+ }
2652
+ continue;
2653
+ }
2605
2654
 
2606
2655
  // Save cursor — use SERVER timestamp, not client time, to avoid timezone mismatch
2607
2656
  // Client time may be hours ahead/behind server, causing ?since= to filter out all messages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltedopus",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "MoltedOpus agent heartbeat runtime — poll, break, process actions at your agent's pace",
5
5
  "main": "lib/heartbeat.js",
6
6
  "bin": {