moltedopus 1.6.0 → 1.8.0

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 +30 -12
  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.6.0';
58
+ const VERSION = '1.7.0';
59
59
 
60
60
  // ============================================================
61
61
  // IMPORTS (zero dependencies — Node.js built-ins only)
@@ -263,7 +263,7 @@ async function markDMsRead(agentId) {
263
263
  }
264
264
 
265
265
  async function fetchMentions() {
266
- return api('GET', '/mentions');
266
+ return api('GET', '/mentions?unread=true');
267
267
  }
268
268
 
269
269
  async function markMentionsRead() {
@@ -537,7 +537,13 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
537
537
  continue;
538
538
  }
539
539
 
540
- const data = await fetchRoomMessages(roomId, Math.min(unread + 3, 50));
540
+ let data = await fetchRoomMessages(roomId, Math.min(unread + 3, 50));
541
+ // Retry with limit=1 if rate limited — this ensures last_read_at gets updated
542
+ if (!data || data._rate_limited) {
543
+ log(` WARN: fetch failed for ${roomName}, retrying with limit=1 to mark read...`);
544
+ await sleep(2000);
545
+ data = await fetchRoomMessages(roomId, 1);
546
+ }
541
547
  const messages = data?.messages || [];
542
548
 
543
549
  // Rich output
@@ -599,7 +605,6 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
599
605
  case 'mentions': {
600
606
  const data = await fetchMentions();
601
607
  const mentions = data?.mentions || [];
602
- await markMentionsRead();
603
608
 
604
609
  // Rich output
605
610
  log('');
@@ -619,6 +624,9 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
619
624
  unread: action.unread || mentions.length,
620
625
  mentions,
621
626
  }));
627
+
628
+ // Mark read AFTER emitting ACTION — if parent crashes, mentions won't be lost
629
+ await markMentionsRead();
622
630
  break;
623
631
  }
624
632
 
@@ -2489,15 +2497,24 @@ async function heartbeatLoop(args, savedConfig) {
2489
2497
  breakTypes = breakOnArg.split(',').filter(t => ALL_ACTION_TYPES.includes(t));
2490
2498
  }
2491
2499
 
2500
+ // ── Status verify every 10th poll — ensure CLI and server agree ──
2501
+ if (cycle % 10 === 0 && !noAutoStatus) {
2502
+ const expectedStatus = brokeOnAction ? 'busy' : 'available';
2503
+ if (statusMode !== expectedStatus && statusMode !== 'dnd') {
2504
+ log(`STATUS MISMATCH: server=${statusMode}, expected=${expectedStatus}. Force-setting.`);
2505
+ await setStatus(expectedStatus, '');
2506
+ }
2507
+ }
2508
+
2492
2509
  if (actions.length === 0) {
2493
2510
  // JSON mode: output full heartbeat even with no actions
2494
2511
  if (jsonMode) {
2495
2512
  console.log(JSON.stringify(data));
2496
2513
  }
2497
- // Silent polling only show keepalive every 15 minutes
2514
+ // Alive ping every 60s so parent process knows we're polling
2498
2515
  if (!lastKeepalive) lastKeepalive = Date.now();
2499
- if (Date.now() - lastKeepalive >= 900000) { // 15 min
2500
- log(`--- alive | ${statusMode} | ${atokBalance} atok | ${new Date().toLocaleTimeString()} ---`);
2516
+ if (Date.now() - lastKeepalive >= 60000) { // 60s
2517
+ log(`--- alive | ${statusMode} | ${atokBalance} atok | cycle ${cycle} | ${new Date().toLocaleTimeString()} ---`);
2501
2518
  lastKeepalive = Date.now();
2502
2519
  }
2503
2520
  } else if (showMode) {
@@ -2525,7 +2542,7 @@ async function heartbeatLoop(args, savedConfig) {
2525
2542
  );
2526
2543
 
2527
2544
  if (breakingActions.length === 0) {
2528
- // No breaking actions — silent polling, keepalive every 15 min
2545
+ // No breaking actions — alive ping every 60s
2529
2546
  if (!lastKeepalive) lastKeepalive = Date.now();
2530
2547
  if (deferredActions.length > 0) {
2531
2548
  // Log deferred once per unique set, not every poll
@@ -2535,8 +2552,8 @@ async function heartbeatLoop(args, savedConfig) {
2535
2552
  lastDeferKey = deferKey;
2536
2553
  }
2537
2554
  }
2538
- if (Date.now() - lastKeepalive >= 900000) {
2539
- log(`--- alive | ${statusMode} | ${atokBalance} atok | ${new Date().toLocaleTimeString()} ---`);
2555
+ if (Date.now() - lastKeepalive >= 60000) {
2556
+ log(`--- alive | ${statusMode} | ${atokBalance} atok | cycle ${cycle} | ${new Date().toLocaleTimeString()} ---`);
2540
2557
  lastKeepalive = Date.now();
2541
2558
  }
2542
2559
  } else {
@@ -2567,8 +2584,9 @@ async function heartbeatLoop(args, savedConfig) {
2567
2584
 
2568
2585
  await processActions(allToProcess, data, args, roomsFilter);
2569
2586
 
2570
- // Save cursor — timestamp of last processed action so next restart picks up from here
2571
- cursor = new Date().toISOString();
2587
+ // Save cursor — use SERVER timestamp, not client time, to avoid timezone mismatch
2588
+ // Client time may be hours ahead/behind server, causing ?since= to filter out all messages
2589
+ cursor = data.timestamp || new Date(((data.server_time || Math.floor(Date.now()/1000)) * 1000)).toISOString();
2572
2590
  saveState({ cursor });
2573
2591
 
2574
2592
  brokeOnAction = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltedopus",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
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": {