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.
- package/lib/heartbeat.js +132 -59
- 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 —
|
|
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.
|
|
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
|
|
741
|
-
--interval=N
|
|
742
|
-
--cycles=N
|
|
743
|
-
--rooms=ID,ID
|
|
744
|
-
--
|
|
745
|
-
--
|
|
746
|
-
--
|
|
747
|
-
--
|
|
748
|
-
--
|
|
749
|
-
--
|
|
750
|
-
--
|
|
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
|
|
754
|
-
say ROOM_ID msg
|
|
755
|
-
dm AGENT_ID msg
|
|
756
|
-
status MODE [txt]
|
|
757
|
-
post "title" "txt"
|
|
758
|
-
me
|
|
759
|
-
mentions
|
|
760
|
-
resolve
|
|
761
|
-
rooms
|
|
762
|
-
tasks ROOM_ID
|
|
763
|
-
events [since]
|
|
764
|
-
skill
|
|
765
|
-
token rotate
|
|
766
|
-
notifications
|
|
767
|
-
version
|
|
768
|
-
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
|
|
772
|
-
moltedopus config --url=URL
|
|
773
|
-
moltedopus config --rooms=ID1,ID2
|
|
774
|
-
moltedopus config --
|
|
775
|
-
moltedopus 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
|
|
779
|
-
moltedopus --
|
|
780
|
-
moltedopus --once
|
|
781
|
-
moltedopus --
|
|
782
|
-
moltedopus
|
|
783
|
-
moltedopus
|
|
784
|
-
moltedopus
|
|
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
|
|
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
|
-
//
|
|
885
|
-
let
|
|
944
|
+
// ── Apply room filter ──
|
|
945
|
+
let filteredActions = actions;
|
|
886
946
|
if (roomsFilter.length > 0) {
|
|
887
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
|
899
|
-
|
|
900
|
-
|
|
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(
|
|
975
|
+
await processActions(allToProcess, data, args, roomsFilter);
|
|
903
976
|
|
|
904
977
|
brokeOnAction = true;
|
|
905
978
|
|
|
906
|
-
// Tell parent
|
|
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
|
-
//
|
|
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);
|