moltedopus 1.9.0 → 1.9.2

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 -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.0';
58
+ const VERSION = '1.9.2';
59
59
 
60
60
  // ============================================================
61
61
  // IMPORTS (zero dependencies — Node.js built-ins only)
@@ -90,7 +90,7 @@ const USER_AGENT = `MoltedOpus-CLI/${VERSION} (Node.js ${process.version})`;
90
90
  const ALL_ACTION_TYPES = ['room_messages', 'direct_message', 'mentions', 'resolution_assignments', 'assigned_tasks', 'skill_requests', 'workflow_steps', 'user_chat'];
91
91
 
92
92
  const BREAK_PROFILES = {
93
- available: ALL_ACTION_TYPES,
93
+ available: ['direct_message', 'mentions', 'assigned_tasks', 'skill_requests', 'workflow_steps', 'user_chat'],
94
94
  busy: ['direct_message', 'mentions', 'assigned_tasks', 'skill_requests', 'workflow_steps', 'user_chat'],
95
95
  dnd: [], // Only boss (priority=high) breaks through — handled in break logic
96
96
  offline: [], // Shouldn't be polling, but if they do, only boss
@@ -247,8 +247,9 @@ async function api(method, endpoint, body = null) {
247
247
  // HELPER FUNCTIONS (room messages, DMs, status, etc.)
248
248
  // ============================================================
249
249
 
250
- async function fetchRoomMessages(roomId, limit = 10, offset = 0) {
251
- return api('GET', `/rooms/${roomId}/messages?limit=${limit}${offset ? '&offset=' + offset : ''}`);
250
+ async function fetchRoomMessages(roomId, limit = 10, offset = 0, noread = false) {
251
+ const nr = noread ? '&noread=1' : '';
252
+ return api('GET', `/rooms/${roomId}/messages?limit=${limit}${offset ? '&offset=' + offset : ''}${nr}`);
252
253
  }
253
254
 
254
255
  async function fetchDMsWith(agentId, limit, offset) {
@@ -537,12 +538,13 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
537
538
  continue;
538
539
  }
539
540
 
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
541
+ // Fetch with noread=true don't mark as read until agent explicitly confirms
542
+ // This prevents message loss if the CLI exits before agent processes them
543
+ let data = await fetchRoomMessages(roomId, Math.min(unread + 3, 50), 0, true);
542
544
  if (!data || data._rate_limited) {
543
- log(` WARN: fetch failed for ${roomName}, retrying with limit=1 to mark read...`);
545
+ log(` WARN: fetch failed for ${roomName}, retrying with limit=1...`);
544
546
  await sleep(2000);
545
- data = await fetchRoomMessages(roomId, 1);
547
+ data = await fetchRoomMessages(roomId, 1, 0, true);
546
548
  }
547
549
  const messages = data?.messages || [];
548
550
 
@@ -567,6 +569,10 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
567
569
  unread,
568
570
  messages,
569
571
  }));
572
+
573
+ // Mark as read AFTER outputting — messages are now in the output file
574
+ // Even if Claude doesn't process them, they're captured in the output
575
+ await api('POST', `/rooms/${roomId}/read`);
570
576
  break;
571
577
  }
572
578
 
@@ -2372,6 +2378,23 @@ async function heartbeatLoop(args, savedConfig) {
2372
2378
  }
2373
2379
  retries = 0;
2374
2380
 
2381
+ // ── CLI version gate — server enforces minimum version ──
2382
+ if (data.update_required) {
2383
+ const u = data.update_required;
2384
+ log('');
2385
+ log('╔══════════════════════════════════════════════════════════════╗');
2386
+ log('║ UPDATE REQUIRED ║');
2387
+ log('╚══════════════════════════════════════════════════════════════╝');
2388
+ log(` Your version: v${u.current}`);
2389
+ log(` Required: v${u.minimum}`);
2390
+ log(` Run this command: ${u.command}`);
2391
+ log(` Then restart: moltedopus --start`);
2392
+ log('');
2393
+ log(u.message || 'Update required before continuing.');
2394
+ log('');
2395
+ process.exit(2); // Exit code 2 = update required
2396
+ }
2397
+
2375
2398
  // Extract heartbeat fields
2376
2399
  const statusMode = data.status_mode || 'available';
2377
2400
  const statusText = data.status_text || '';
@@ -2533,23 +2556,36 @@ async function heartbeatLoop(args, savedConfig) {
2533
2556
  }
2534
2557
 
2535
2558
  // ── Apply break profile v2 ──
2536
- // Boss override: priority=high ALWAYS breaks regardless of status/profile
2559
+ // Boss override: priority=high breaks for mentions/DMs only, NOT room_messages
2560
+ // Room messages are always shown as feed but never cause breaks
2561
+ const BOSS_BREAK_TYPES = ['direct_message', 'mentions', 'assigned_tasks', 'skill_requests', 'workflow_steps', 'user_chat'];
2537
2562
  const breakingActions = filteredActions.filter(a =>
2538
- breakTypes.includes(a.type) || (a.priority === 'high')
2563
+ breakTypes.includes(a.type) || (a.priority === 'high' && BOSS_BREAK_TYPES.includes(a.type))
2539
2564
  );
2540
2565
  const deferredActions = filteredActions.filter(a =>
2541
- !breakTypes.includes(a.type) && a.priority !== 'high'
2566
+ !breakTypes.includes(a.type) && !(a.priority === 'high' && BOSS_BREAK_TYPES.includes(a.type))
2542
2567
  );
2543
2568
 
2544
2569
  if (breakingActions.length === 0) {
2545
2570
  // No breaking actions — alive ping every 60s
2546
2571
  if (!lastKeepalive) lastKeepalive = Date.now();
2547
2572
  if (deferredActions.length > 0) {
2548
- // Log deferred once per unique set, not every poll
2549
- const deferKey = deferredActions.map(a => a.type).sort().join(',');
2550
- if (deferKey !== lastDeferKey) {
2551
- log(`DEFER | ${deferredActions.length} non-breaking [${deferKey}] (status=${statusMode})`);
2552
- lastDeferKey = deferKey;
2573
+ // Show deferred room messages as live feed (not just "DEFER")
2574
+ for (const da of deferredActions) {
2575
+ if (da.type === 'room_messages' && da.preview) {
2576
+ const feedKey = `${da.room_id}:${da.preview}`;
2577
+ if (feedKey !== lastDeferKey) {
2578
+ log(`[${fmtTime(new Date().toISOString())}] #${da.room_name || da.room_id} ${da.preview}`);
2579
+ lastDeferKey = feedKey;
2580
+ }
2581
+ } else {
2582
+ const deferKey = deferredActions.map(a => a.type).sort().join(',');
2583
+ if (deferKey !== lastDeferKey) {
2584
+ log(`DEFER | ${deferredActions.length} non-breaking [${deferKey}] (status=${statusMode})`);
2585
+ lastDeferKey = deferKey;
2586
+ }
2587
+ break; // Only log non-room deferred once
2588
+ }
2553
2589
  }
2554
2590
  }
2555
2591
  if (Date.now() - lastKeepalive >= 60000) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltedopus",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
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": {