moltedopus 1.9.2 → 1.9.4

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 +47 -16
  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.2';
58
+ const VERSION = '1.9.4';
59
59
 
60
60
  // ============================================================
61
61
  // IMPORTS (zero dependencies — Node.js built-ins only)
@@ -510,18 +510,19 @@ function fmtTime(dateStr) {
510
510
  return `${hh}:${mm} · ${ago}`;
511
511
  }
512
512
 
513
- function fmtMsg(msg, maxLen) {
513
+ function fmtMsg(msg) {
514
514
  const content = (msg.content || '').replace(/\n/g, ' ');
515
515
  const name = msg.sender_name || msg.from?.name || '?';
516
516
  const time = fmtTime(msg.created_at);
517
- const truncated = content.length > (maxLen || 120) ? content.slice(0, maxLen || 120) + '...' : content;
518
- return ` [${time}] ${name}: ${truncated}`;
517
+ return ` [${time}] ${name}: ${content}`;
519
518
  }
520
519
 
521
520
  async function processActions(actions, heartbeatData, args, roomsFilter) {
521
+ let realActionCount = 0;
522
+
522
523
  if (args.json) {
523
524
  console.log(JSON.stringify(heartbeatData));
524
- return;
525
+ return actions.length;
525
526
  }
526
527
 
527
528
  for (const action of actions) {
@@ -569,6 +570,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
569
570
  unread,
570
571
  messages,
571
572
  }));
573
+ realActionCount++;
572
574
 
573
575
  // Mark as read AFTER outputting — messages are now in the output file
574
576
  // Even if Claude doesn't process them, they're captured in the output
@@ -605,6 +607,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
605
607
  unread: action.unread || messages.length,
606
608
  messages,
607
609
  }));
610
+ realActionCount++;
608
611
  break;
609
612
  }
610
613
 
@@ -612,15 +615,23 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
612
615
  const data = await fetchMentions();
613
616
  const mentions = data?.mentions || [];
614
617
 
618
+ // Phantom mentions: server counted unread but fetch returned empty
619
+ // Force mark-read to clear stale state and skip ACTION output
620
+ if (mentions.length === 0) {
621
+ log(` WARN: Phantom mentions detected (server unread=${action.unread || '?'}, fetched=0). Clearing stale state.`);
622
+ await markMentionsRead();
623
+ break; // Don't count as real action — continue polling
624
+ }
625
+
615
626
  // Rich output
616
627
  log('');
617
628
  log(`── MENTIONS: ${mentions.length} unread ──`);
618
629
  for (const m of mentions.slice(0, 10)) {
619
630
  const time = fmtTime(m.created_at);
620
631
  const from = m.from?.name || '?';
621
- const preview = (m.room_message_preview || m.comment_preview || '').replace(/\n/g, ' ').slice(0, 120);
632
+ const preview = (m.room_message_preview || m.comment_preview || '').replace(/\n/g, ' ');
622
633
  const where = m.room_name ? `#${m.room_name}` : (m.post_title ? `post: ${m.post_title}` : '');
623
- log(` [${time}] ${from} in ${where}: ${preview}${preview.length >= 120 ? '...' : ''}`);
634
+ log(` [${time}] ${from} in ${where}: ${preview}`);
624
635
  }
625
636
  if (mentions.length > 10) log(` ... and ${mentions.length - 10} more`);
626
637
  log('');
@@ -630,6 +641,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
630
641
  unread: action.unread || mentions.length,
631
642
  mentions,
632
643
  }));
644
+ realActionCount++;
633
645
 
634
646
  // Mark read AFTER emitting ACTION — if parent crashes, mentions won't be lost
635
647
  await markMentionsRead();
@@ -643,7 +655,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
643
655
  log('');
644
656
  log(`── RESOLUTIONS: ${queue.length} pending ──`);
645
657
  for (const r of queue.slice(0, 5)) {
646
- log(` [${r.id?.slice(0, 8)}...] "${(r.title || r.content || '').slice(0, 80)}" by ${r.author_name || '?'}`);
658
+ log(` [${r.id?.slice(0, 8)}...] "${r.title || r.content || ''}" by ${r.author_name || '?'}`);
647
659
  }
648
660
  log('');
649
661
  log(` Commands:`);
@@ -655,6 +667,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
655
667
  pending: action.pending || queue.length,
656
668
  assignments: queue,
657
669
  }));
670
+ realActionCount++;
658
671
  break;
659
672
  }
660
673
 
@@ -676,6 +689,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
676
689
  count: action.count || tasks.length,
677
690
  tasks,
678
691
  }));
692
+ realActionCount++;
679
693
  break;
680
694
  }
681
695
 
@@ -686,7 +700,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
686
700
  log('');
687
701
  log(`── SKILL REQUESTS: ${requests.length} pending ──`);
688
702
  for (const r of requests.slice(0, 5)) {
689
- log(` ${r.skill_name || '?'} from ${r.requester_name || '?'}: ${(r.description || '').slice(0, 80)}`);
703
+ log(` ${r.skill_name || '?'} from ${r.requester_name || '?'}: ${r.description || ''}`);
690
704
  }
691
705
  log('');
692
706
 
@@ -695,6 +709,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
695
709
  pending: action.pending || requests.length,
696
710
  requests,
697
711
  }));
712
+ realActionCount++;
698
713
  break;
699
714
  }
700
715
 
@@ -713,6 +728,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
713
728
  count: action.count || steps.length,
714
729
  steps,
715
730
  }));
731
+ realActionCount++;
716
732
  break;
717
733
  }
718
734
 
@@ -734,7 +750,7 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
734
750
  log(fmtMsg(m));
735
751
  }
736
752
  } else {
737
- log(` ${preview.slice(0, 120)}`);
753
+ log(` ${preview}`);
738
754
  }
739
755
  log('');
740
756
  log(` Commands:`);
@@ -751,16 +767,20 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
751
767
  reply_endpoint: action.reply || `POST ${BASE_URL}/chat/${chatId}/agent-reply`,
752
768
  messages,
753
769
  }));
770
+ realActionCount++;
754
771
  break;
755
772
  }
756
773
 
757
774
  default: {
758
775
  log(` >> ${type}: (passthrough)`);
759
776
  console.log('ACTION:' + JSON.stringify(action));
777
+ realActionCount++;
760
778
  break;
761
779
  }
762
780
  }
763
781
  }
782
+
783
+ return realActionCount;
764
784
  }
765
785
 
766
786
  // ============================================================
@@ -1052,7 +1072,7 @@ async function cmdShow() {
1052
1072
  for (const r of rooms) {
1053
1073
  const role = r.role || 'member';
1054
1074
  console.log(` ${r.name} (${role}) — ${r.id}`);
1055
- if (r.description) console.log(` ${r.description.slice(0, 80)}`);
1075
+ if (r.description) console.log(` ${r.description}`);
1056
1076
  }
1057
1077
  console.log('');
1058
1078
  }
@@ -2436,7 +2456,7 @@ async function heartbeatLoop(args, savedConfig) {
2436
2456
  for (const r of b.rooms) {
2437
2457
  log(` ${r.name} (${r.role}) — ${r.id}`);
2438
2458
  if (r.description) log(` ${r.description}`);
2439
- if (r.skill) log(` skill: ${r.skill.replace(/\n/g, ' ').slice(0, 200)}${r.skill.length > 200 ? '...' : ''}`);
2459
+ if (r.skill) log(` skill: ${r.skill.replace(/\n/g, ' ')}`);
2440
2460
  }
2441
2461
  }
2442
2462
  if (b.orders && b.orders.length > 0) {
@@ -2450,7 +2470,7 @@ async function heartbeatLoop(args, savedConfig) {
2450
2470
  log('');
2451
2471
  log('Config:');
2452
2472
  for (const [k, v] of Object.entries(b.config)) {
2453
- log(` ${k}: ${String(v).slice(0, 100)}`);
2473
+ log(` ${k}: ${String(v)}`);
2454
2474
  }
2455
2475
  }
2456
2476
  if (b.commands) {
@@ -2537,7 +2557,7 @@ async function heartbeatLoop(args, savedConfig) {
2537
2557
  // Alive ping every 60s so parent process knows we're polling
2538
2558
  if (!lastKeepalive) lastKeepalive = Date.now();
2539
2559
  if (Date.now() - lastKeepalive >= 60000) { // 60s
2540
- log(`--- alive | ${statusMode} | ${atokBalance} atok | cycle ${cycle} | ${new Date().toLocaleTimeString()} ---`);
2560
+ log(`--- ${statusMode} | #${cycle} | ${new Date().toLocaleTimeString()} ---`);
2541
2561
  lastKeepalive = Date.now();
2542
2562
  }
2543
2563
  } else if (showMode) {
@@ -2589,7 +2609,7 @@ async function heartbeatLoop(args, savedConfig) {
2589
2609
  }
2590
2610
  }
2591
2611
  if (Date.now() - lastKeepalive >= 60000) {
2592
- log(`--- alive | ${statusMode} | ${atokBalance} atok | cycle ${cycle} | ${new Date().toLocaleTimeString()} ---`);
2612
+ log(`--- ${statusMode} | #${cycle} | ${new Date().toLocaleTimeString()} ---`);
2593
2613
  lastKeepalive = Date.now();
2594
2614
  }
2595
2615
  } else {
@@ -2618,7 +2638,18 @@ async function heartbeatLoop(args, savedConfig) {
2618
2638
 
2619
2639
  log(`BREAK | ${allToProcess.length} action(s) [${types.join(', ')}]${hasBoss ? ' [BOSS]' : ''} (triggered by: ${breakingActions.map(a => a.type).join(', ')})`);
2620
2640
 
2621
- await processActions(allToProcess, data, args, roomsFilter);
2641
+ const realCount = await processActions(allToProcess, data, args, roomsFilter);
2642
+
2643
+ // If all actions were phantom (e.g. stale mentions with empty fetch),
2644
+ // don't break — continue polling. The stale state has been cleared.
2645
+ if (realCount === 0) {
2646
+ log(' All actions were phantom/stale — continuing poll...');
2647
+ if (!noAutoStatus) {
2648
+ await setStatus('available', '');
2649
+ log('Auto-status: available');
2650
+ }
2651
+ continue;
2652
+ }
2622
2653
 
2623
2654
  // Save cursor — use SERVER timestamp, not client time, to avoid timezone mismatch
2624
2655
  // 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.2",
3
+ "version": "1.9.4",
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": {