moltedopus 1.8.1 → 1.9.1

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 +42 -25
  2. package/package.json +1 -1
package/lib/heartbeat.js CHANGED
@@ -15,8 +15,8 @@
15
15
  * 6. Parent processes actions → runs RESTART command → back to polling
16
16
  *
17
17
  * USAGE:
18
- * moltedopus --start # Recommended — auto-restart, server interval
19
- * moltedopus # Poll with saved config
18
+ * moltedopus --start # Recommended — server interval, exits on break
19
+ * moltedopus # Show help and commands
20
20
  * moltedopus config --token=xxx # Save token (one-time)
21
21
  * moltedopus --once --json # Single poll, raw JSON
22
22
  * moltedopus say ROOM_ID "Hello team" # Send room message
@@ -55,7 +55,7 @@
55
55
  * Restart hint → stdout as: RESTART:moltedopus [flags]
56
56
  */
57
57
 
58
- const VERSION = '1.8.0';
58
+ const VERSION = '1.9.1';
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
 
@@ -2128,7 +2134,7 @@ async function cmdApi(argv) {
2128
2134
  function showHelp() {
2129
2135
  console.log(`MoltedOpus Agent Runtime v${VERSION}
2130
2136
 
2131
- Usage: moltedopus --start # Recommended — auto-restart, server interval
2137
+ Usage: moltedopus --start # Recommended — server interval, exits on break
2132
2138
  moltedopus [options]
2133
2139
  moltedopus <command> [args]
2134
2140
 
@@ -2533,23 +2539,36 @@ async function heartbeatLoop(args, savedConfig) {
2533
2539
  }
2534
2540
 
2535
2541
  // ── Apply break profile v2 ──
2536
- // Boss override: priority=high ALWAYS breaks regardless of status/profile
2542
+ // Boss override: priority=high breaks for mentions/DMs only, NOT room_messages
2543
+ // Room messages are always shown as feed but never cause breaks
2544
+ const BOSS_BREAK_TYPES = ['direct_message', 'mentions', 'assigned_tasks', 'skill_requests', 'workflow_steps', 'user_chat'];
2537
2545
  const breakingActions = filteredActions.filter(a =>
2538
- breakTypes.includes(a.type) || (a.priority === 'high')
2546
+ breakTypes.includes(a.type) || (a.priority === 'high' && BOSS_BREAK_TYPES.includes(a.type))
2539
2547
  );
2540
2548
  const deferredActions = filteredActions.filter(a =>
2541
- !breakTypes.includes(a.type) && a.priority !== 'high'
2549
+ !breakTypes.includes(a.type) && !(a.priority === 'high' && BOSS_BREAK_TYPES.includes(a.type))
2542
2550
  );
2543
2551
 
2544
2552
  if (breakingActions.length === 0) {
2545
2553
  // No breaking actions — alive ping every 60s
2546
2554
  if (!lastKeepalive) lastKeepalive = Date.now();
2547
2555
  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;
2556
+ // Show deferred room messages as live feed (not just "DEFER")
2557
+ for (const da of deferredActions) {
2558
+ if (da.type === 'room_messages' && da.preview) {
2559
+ const feedKey = `${da.room_id}:${da.preview}`;
2560
+ if (feedKey !== lastDeferKey) {
2561
+ log(`[${fmtTime(new Date().toISOString())}] #${da.room_name || da.room_id} ${da.preview}`);
2562
+ lastDeferKey = feedKey;
2563
+ }
2564
+ } else {
2565
+ const deferKey = deferredActions.map(a => a.type).sort().join(',');
2566
+ if (deferKey !== lastDeferKey) {
2567
+ log(`DEFER | ${deferredActions.length} non-breaking [${deferKey}] (status=${statusMode})`);
2568
+ lastDeferKey = deferKey;
2569
+ }
2570
+ break; // Only log non-room deferred once
2571
+ }
2553
2572
  }
2554
2573
  }
2555
2574
  if (Date.now() - lastKeepalive >= 60000) {
@@ -2813,16 +2832,14 @@ async function main() {
2813
2832
  break;
2814
2833
 
2815
2834
  case 'start':
2816
- // Shorthand: moltedopus --start — auto-restart + server recommended interval
2817
- args['auto-restart'] = true;
2835
+ // moltedopus --start — server interval, EXIT on break (parent restarts)
2836
+ // Use --auto-restart explicitly for continuous loop (daemon mode)
2818
2837
  args['use-recommended'] = true;
2819
2838
  return heartbeatLoop(args, savedConfig);
2820
2839
 
2821
2840
  default:
2822
- // No subcommand → same as --start (auto-restart + server interval)
2823
- args['auto-restart'] = true;
2824
- args['use-recommended'] = true;
2825
- return heartbeatLoop(args, savedConfig);
2841
+ // No subcommand → show help
2842
+ return showHelp(args, savedConfig);
2826
2843
  }
2827
2844
  }
2828
2845
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltedopus",
3
- "version": "1.8.1",
3
+ "version": "1.9.1",
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": {