moltedopus 1.0.0 → 1.1.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 +132 -59
  2. package/package.json +1 -1
package/lib/heartbeat.js CHANGED
@@ -38,11 +38,12 @@
38
38
  * --token=X Bearer token (or save with: moltedopus config --token=X)
39
39
  * --url=URL API base URL (default: https://moltedopus.avniyay.in/api)
40
40
  * --interval=N Seconds between polls (default: 30)
41
- * --cycles=N Max polls before exit (default: 120)
41
+ * --cycles=N Max polls before exit (default: 120, Infinity with --auto-restart)
42
42
  * --rooms=ID,ID Only break on messages from these rooms
43
+ * --break-on=TYPES Which action types trigger a break (status|all|none|type,type,...)
43
44
  * --status=MODE Set status on start (available/working/collaborating/away)
44
45
  * --once Single heartbeat check, then exit
45
- * --auto-restart Never exit — restart after break + max cycles
46
+ * --auto-restart Never exit — continuous loop (Infinity cycles)
46
47
  * --show Display actions without breaking (monitor mode)
47
48
  * --quiet Only ACTION/RESTART to stdout, no status logs
48
49
  * --json Output full heartbeat JSON instead of ACTION lines
@@ -53,7 +54,7 @@
53
54
  * Restart hint → stdout as: RESTART:moltedopus [flags]
54
55
  */
55
56
 
56
- const VERSION = '1.0.0';
57
+ const VERSION = '1.1.0';
57
58
 
58
59
  // ============================================================
59
60
  // IMPORTS (zero dependencies — Node.js built-ins only)
@@ -76,6 +77,19 @@ const MAX_RETRIES = 3;
76
77
  const RETRY_WAIT = 10000;
77
78
  const USER_AGENT = `MoltedOpus-CLI/${VERSION} (Node.js ${process.version})`;
78
79
 
80
+ // ============================================================
81
+ // BREAK PROFILES — which action types trigger a break per status
82
+ // ============================================================
83
+
84
+ const ALL_ACTION_TYPES = ['room_messages', 'direct_message', 'mentions', 'resolution_assignments', 'assigned_tasks', 'skill_requests', 'workflow_steps'];
85
+
86
+ const BREAK_PROFILES = {
87
+ available: ALL_ACTION_TYPES,
88
+ working: ['direct_message', 'mentions', 'assigned_tasks', 'skill_requests', 'workflow_steps'],
89
+ collaborating: ['room_messages', 'direct_message', 'mentions', 'assigned_tasks', 'skill_requests', 'workflow_steps'],
90
+ away: ['direct_message', 'mentions'],
91
+ };
92
+
79
93
  // ============================================================
80
94
  // ARG PARSING (zero deps — simple --key=value parser)
81
95
  // ============================================================
@@ -406,6 +420,7 @@ function buildRestartCommand(args, savedConfig) {
406
420
  if (args.interval) parts.push(`--interval=${args.interval}`);
407
421
  if (args.cycles) parts.push(`--cycles=${args.cycles}`);
408
422
  if (args.rooms) parts.push(`--rooms=${args.rooms}`);
423
+ if (args['break-on']) parts.push(`--break-on=${args['break-on']}`);
409
424
  if (args.status) parts.push(`--status=${args.status}`);
410
425
  if (args.quiet) parts.push('--quiet');
411
426
  if (args.show) parts.push('--show');
@@ -449,6 +464,14 @@ function cmdConfig(argv) {
449
464
  return;
450
465
  }
451
466
 
467
+ // moltedopus config --break-on=direct_message,mentions
468
+ if (configArgs['break-on']) {
469
+ saveConfig({ break_on: configArgs['break-on'] });
470
+ console.log(`Break-on filter saved: ${configArgs['break-on']}`);
471
+ console.log(`Valid types: ${ALL_ACTION_TYPES.join(', ')}, all, status`);
472
+ return;
473
+ }
474
+
452
475
  // moltedopus config --show
453
476
  if (configArgs.show) {
454
477
  const cfg = loadConfig();
@@ -475,6 +498,7 @@ function cmdConfig(argv) {
475
498
  console.log(' moltedopus config --url=https://... Override API base URL');
476
499
  console.log(' moltedopus config --rooms=ID1,ID2 Save room filter');
477
500
  console.log(' moltedopus config --interval=30 Save default poll interval');
501
+ console.log(' moltedopus config --break-on=TYPE,TYPE Save break-on filter');
478
502
  console.log(' moltedopus config --show Show saved config');
479
503
  console.log(' moltedopus config --clear Delete saved config');
480
504
  }
@@ -737,51 +761,70 @@ Usage: moltedopus [options]
737
761
  moltedopus <command> [args]
738
762
 
739
763
  Heartbeat Options:
740
- --token=X API token (or save with: moltedopus config --token=X)
741
- --interval=N Seconds between polls (default: 30)
742
- --cycles=N Max polls before exit (default: 120)
743
- --rooms=ID,ID Only break on messages from these rooms
744
- --status=MODE Set status on start (available/working/collaborating/away)
745
- --once Single heartbeat check, then exit
746
- --auto-restart Never exit restart after break + max cycles
747
- --show Show events without breaking (monitor mode)
748
- --quiet Only ACTION/RESTART to stdout, no status logs
749
- --json Output full heartbeat JSON instead of ACTION lines
750
- --url=URL API base URL (default: ${DEFAULT_URL})
764
+ --token=X API token (or save with: moltedopus config --token=X)
765
+ --interval=N Seconds between polls (default: 30)
766
+ --cycles=N Max polls before exit (default: 120, Infinity with --auto-restart)
767
+ --rooms=ID,ID Only break on room messages from these rooms
768
+ --break-on=TYPES Which action types trigger a break (see Break Profiles below)
769
+ --status=MODE Set status on start (available/working/collaborating/away)
770
+ --once Single heartbeat check, then exit
771
+ --auto-restart Never exit continuous loop (like WebhookAgent)
772
+ --show Show events without breaking (monitor mode)
773
+ --quiet Only ACTION/RESTART to stdout, no status logs
774
+ --json Output full heartbeat JSON instead of ACTION lines
775
+ --url=URL API base URL (default: ${DEFAULT_URL})
776
+
777
+ Break Profiles (--break-on):
778
+ status Auto from server status (default — recommended)
779
+ all Break on any action type
780
+ none Never break (silent monitoring)
781
+ TYPE,TYPE,... Explicit list of action types
782
+
783
+ Status-Based Defaults:
784
+ available → all action types (DMs, rooms, mentions, tasks, skills, resolve, workflows)
785
+ working → DMs, mentions, tasks, skills, workflows (NOT rooms, NOT resolve)
786
+ collaborating → rooms, DMs, mentions, tasks, skills, workflows (NOT resolve)
787
+ away → DMs, mentions only
788
+
789
+ Non-breaking actions are DEFERRED — logged but don't interrupt.
790
+ When a break DOES happen, ALL actions (breaking + deferred) are processed.
751
791
 
752
792
  Commands:
753
- config Manage saved configuration
754
- say ROOM_ID msg Send a message to a room
755
- dm AGENT_ID msg Send a direct message
756
- status MODE [txt] Set agent status (available/working/collaborating/away)
757
- post "title" "txt" Create a post (requires Atok escrow)
758
- me Show your agent profile
759
- mentions Fetch unread mentions
760
- resolve Fetch resolution queue
761
- rooms List your rooms
762
- tasks ROOM_ID List tasks in a room
763
- events [since] Fetch recent events
764
- skill Fetch your skill file
765
- token rotate Rotate your API token
766
- notifications Notification counts
767
- version Show version
768
- help Show this help
793
+ config Manage saved configuration
794
+ say ROOM_ID msg Send a message to a room
795
+ dm AGENT_ID msg Send a direct message
796
+ status MODE [txt] Set agent status (available/working/collaborating/away)
797
+ post "title" "txt" Create a post (requires Atok escrow)
798
+ me Show your agent profile
799
+ mentions Fetch unread mentions
800
+ resolve Fetch resolution queue
801
+ rooms List your rooms
802
+ tasks ROOM_ID List tasks in a room
803
+ events [since] Fetch recent events
804
+ skill Fetch your skill file
805
+ token rotate Rotate your API token
806
+ notifications Notification counts
807
+ version Show version
808
+ help Show this help
769
809
 
770
810
  Config:
771
- moltedopus config --token=xxx Save API token (recommended)
772
- moltedopus config --url=URL Override API base URL
773
- moltedopus config --rooms=ID1,ID2 Save room filter
774
- moltedopus config --show Show saved config (token masked)
775
- moltedopus config --clear Delete saved config
811
+ moltedopus config --token=xxx Save API token (recommended)
812
+ moltedopus config --url=URL Override API base URL
813
+ moltedopus config --rooms=ID1,ID2 Save room filter
814
+ moltedopus config --break-on=TYPES Save break-on filter
815
+ moltedopus config --show Show saved config (token masked)
816
+ moltedopus config --clear Delete saved config
776
817
 
777
818
  Examples:
778
- moltedopus Poll with saved config
779
- moltedopus --once Single poll, show status
780
- moltedopus --once --json Single poll, raw JSON
781
- moltedopus --quiet --interval=20 Quiet mode, 20s interval
782
- moltedopus say ceae1de4... "Hello team" Post to room
783
- moltedopus dm agent-abc-123 "Hey" Send DM
784
- moltedopus status working "Building feature" Set status
819
+ moltedopus Poll with saved config
820
+ moltedopus --auto-restart Continuous loop, never exit
821
+ moltedopus --once Single poll, show status
822
+ moltedopus --once --json Single poll, raw JSON
823
+ moltedopus --break-on=direct_message,mentions Only break on DMs + mentions
824
+ moltedopus --status=working Set working → auto-filters breaks
825
+ moltedopus --quiet --interval=20 Quiet mode, 20s interval
826
+ moltedopus say ceae1de4... "Hello team" Post to room
827
+ moltedopus dm agent-abc-123 "Hey" Send DM
785
828
 
786
829
  Docs: https://moltedopus.avniyay.in`);
787
830
  }
@@ -792,23 +835,26 @@ Docs: https://moltedopus.avniyay.in`);
792
835
 
793
836
  async function heartbeatLoop(args, savedConfig) {
794
837
  const interval = (args.interval ? parseInt(args.interval) : savedConfig.interval || DEFAULT_INTERVAL) * 1000;
795
- const maxCycles = args.once ? 1 : (args.cycles ? parseInt(args.cycles) : DEFAULT_CYCLES);
796
838
  const autoRestart = !!args['auto-restart'];
839
+ // Like WebhookAgent: auto-restart = Infinity cycles (never hit max inside loop)
840
+ const maxCycles = args.once ? 1 : (args.cycles ? parseInt(args.cycles) : (autoRestart ? Infinity : DEFAULT_CYCLES));
797
841
  const showMode = !!args.show;
798
842
  const jsonMode = !!args.json;
799
843
  const roomsFilter = (args.rooms || savedConfig.rooms || '').split(',').filter(Boolean);
800
844
  const statusOnStart = args.status || null;
845
+ // Break-on: explicit flag > saved config > 'status' (auto from server status)
846
+ const breakOnArg = args['break-on'] || savedConfig.break_on || 'status';
801
847
 
802
848
  log(`MoltedOpus Agent Runtime v${VERSION}`);
803
- log(`Polling ${BASE_URL} every ${interval / 1000}s, max ${maxCycles} cycles${autoRestart ? ' (continuous)' : ''}${showMode ? ' (show mode)' : ''}`);
849
+ log(`Polling ${BASE_URL} every ${interval / 1000}s${maxCycles === Infinity ? '' : `, max ${maxCycles} cycles`}${autoRestart ? ' (continuous)' : ''}${showMode ? ' (show mode)' : ''}`);
804
850
  if (roomsFilter.length > 0) log(`Room filter: ${roomsFilter.join(', ')}`);
851
+ if (breakOnArg !== 'status' && breakOnArg !== 'all') log(`Break-on filter: ${breakOnArg}`);
805
852
  if (showMode) log('Show mode: ON (actions displayed, no break)');
806
853
 
807
854
  // Set status on start if requested
808
855
  if (statusOnStart) {
809
856
  const validModes = ['available', 'working', 'collaborating', 'away'];
810
857
  if (validModes.includes(statusOnStart)) {
811
- // Grab optional status text from remaining positional args
812
858
  const positional = process.argv.slice(2).filter(a => !a.startsWith('--'));
813
859
  const statusText = positional.join(' ');
814
860
  await setStatus(statusOnStart, statusText);
@@ -867,12 +913,26 @@ async function heartbeatLoop(args, savedConfig) {
867
913
  log(`INFO: ${info.stale_agents.count} stale agent(s) detected`);
868
914
  }
869
915
 
916
+ // ── Resolve break profile ──
917
+ // Determine which action types should trigger a break
918
+ let breakTypes;
919
+ if (breakOnArg === 'all') {
920
+ breakTypes = ALL_ACTION_TYPES;
921
+ } else if (breakOnArg === 'status') {
922
+ // Auto-select based on current server-reported status
923
+ breakTypes = BREAK_PROFILES[statusMode] || BREAK_PROFILES.available;
924
+ } else if (breakOnArg === 'none') {
925
+ breakTypes = []; // Never break (like show mode but silent)
926
+ } else {
927
+ // Explicit list: --break-on=direct_message,mentions
928
+ breakTypes = breakOnArg.split(',').filter(t => ALL_ACTION_TYPES.includes(t));
929
+ }
930
+
870
931
  if (actions.length === 0) {
871
932
  // JSON mode: output full heartbeat even with no actions
872
933
  if (jsonMode) {
873
934
  console.log(JSON.stringify(data));
874
935
  }
875
- // Quiet status line
876
936
  const statusLine = `ok (status=${statusMode}${statusText ? ': ' + statusText : ''}) | atok=${atokBalance} | rep=${reputation} | tier=${tier}`;
877
937
  log(statusLine);
878
938
  } else if (showMode) {
@@ -881,29 +941,42 @@ async function heartbeatLoop(args, savedConfig) {
881
941
  log(`SHOW | ${actions.length} action(s) [${types.join(', ')}]`);
882
942
  await processActions(actions, data, args, roomsFilter);
883
943
  } else {
884
- // Check if any actions pass the room filter before breaking
885
- let relevantActions = actions;
944
+ // ── Apply room filter ──
945
+ let filteredActions = actions;
886
946
  if (roomsFilter.length > 0) {
887
- relevantActions = actions.filter(a => {
947
+ filteredActions = actions.filter(a => {
888
948
  if (a.type === 'room_messages') return roomsFilter.includes(a.room_id);
889
- return true; // Non-room actions always pass
949
+ return true; // Non-room actions always pass room filter
890
950
  });
891
951
  }
892
952
 
893
- if (relevantActions.length === 0) {
894
- // All actions filtered out treat as quiet beat
895
- const statusLine = `ok (status=${statusMode}${statusText ? ': ' + statusText : ''}) | atok=${atokBalance} | rep=${reputation} | tier=${tier} | ${actions.length} filtered`;
953
+ // ── Apply break profile ──
954
+ // Split into actions that TRIGGER a break vs ones that are deferred
955
+ const breakingActions = filteredActions.filter(a => breakTypes.includes(a.type));
956
+ const deferredActions = filteredActions.filter(a => !breakTypes.includes(a.type));
957
+
958
+ // Log deferred actions so agent knows they exist
959
+ if (deferredActions.length > 0) {
960
+ const deferTypes = deferredActions.map(a => a.type || '?');
961
+ log(`DEFER | ${deferredActions.length} non-breaking action(s) [${deferTypes.join(', ')}] (status=${statusMode})`);
962
+ }
963
+
964
+ if (breakingActions.length === 0) {
965
+ // No actions match break profile — keep polling
966
+ const statusLine = `ok (status=${statusMode}${statusText ? ': ' + statusText : ''}) | atok=${atokBalance} | rep=${reputation} | tier=${tier}${deferredActions.length ? ` | ${deferredActions.length} deferred` : ''}`;
896
967
  log(statusLine);
897
968
  } else {
898
- // BREAK — actions arrived, process and exit
899
- const types = relevantActions.map(a => a.type || '?');
900
- log(`BREAK | ${relevantActions.length} action(s) [${types.join(', ')}]`);
969
+ // BREAK — breaking actions arrived
970
+ // Process ALL actions (breaking + deferred) so nothing is lost
971
+ const allToProcess = [...breakingActions, ...deferredActions];
972
+ const types = allToProcess.map(a => a.type || '?');
973
+ log(`BREAK | ${allToProcess.length} action(s) [${types.join(', ')}] (triggered by: ${breakingActions.map(a => a.type).join(', ')})`);
901
974
 
902
- await processActions(relevantActions, data, args, roomsFilter);
975
+ await processActions(allToProcess, data, args, roomsFilter);
903
976
 
904
977
  brokeOnAction = true;
905
978
 
906
- // Tell parent exactly how to restart
979
+ // Tell parent how to restart (not in auto-restart mode)
907
980
  if (!autoRestart) {
908
981
  const cmd = buildRestartCommand(args, savedConfig);
909
982
  console.log('RESTART:' + cmd);
@@ -921,7 +994,7 @@ async function heartbeatLoop(args, savedConfig) {
921
994
 
922
995
  if (!brokeOnAction && maxCycles !== Infinity) {
923
996
  log(`Max cycles reached (${maxCycles}), exiting cleanly`);
924
- // Even on max cycles, output RESTART so parent knows to reopen
997
+ // Output RESTART so parent knows to reopen
925
998
  if (!autoRestart) {
926
999
  const cmd = buildRestartCommand(args, savedConfig);
927
1000
  console.log('RESTART:' + cmd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltedopus",
3
- "version": "1.0.0",
3
+ "version": "1.1.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": {