moltedopus 1.5.2 → 1.6.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 +279 -20
- 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.
|
|
58
|
+
const VERSION = '1.6.0';
|
|
59
59
|
|
|
60
60
|
// ============================================================
|
|
61
61
|
// IMPORTS (zero dependencies — Node.js built-ins only)
|
|
@@ -138,6 +138,23 @@ function loadConfig() {
|
|
|
138
138
|
return {};
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// State file — separate from config, stores cursor/runtime data
|
|
142
|
+
const STATE_FILE = path.join(CONFIG_DIR, 'state.json');
|
|
143
|
+
|
|
144
|
+
function loadState() {
|
|
145
|
+
try {
|
|
146
|
+
if (fs.existsSync(STATE_FILE)) return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
147
|
+
} catch (e) { /* ignore */ }
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function saveState(data) {
|
|
152
|
+
ensureConfigDir();
|
|
153
|
+
let existing = loadState();
|
|
154
|
+
const merged = { ...existing, ...data };
|
|
155
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
function saveConfig(data) {
|
|
142
159
|
// Save to local config (project directory) by default
|
|
143
160
|
const targetFile = LOCAL_CONFIG_FILE;
|
|
@@ -230,12 +247,15 @@ async function api(method, endpoint, body = null) {
|
|
|
230
247
|
// HELPER FUNCTIONS (room messages, DMs, status, etc.)
|
|
231
248
|
// ============================================================
|
|
232
249
|
|
|
233
|
-
async function fetchRoomMessages(roomId, limit = 10) {
|
|
234
|
-
return api('GET', `/rooms/${roomId}/messages?limit=${limit}`);
|
|
250
|
+
async function fetchRoomMessages(roomId, limit = 10, offset = 0) {
|
|
251
|
+
return api('GET', `/rooms/${roomId}/messages?limit=${limit}${offset ? '&offset=' + offset : ''}`);
|
|
235
252
|
}
|
|
236
253
|
|
|
237
|
-
async function fetchDMsWith(agentId) {
|
|
238
|
-
|
|
254
|
+
async function fetchDMsWith(agentId, limit, offset) {
|
|
255
|
+
let qs = '';
|
|
256
|
+
if (limit) qs += `?limit=${limit}`;
|
|
257
|
+
if (offset) qs += `${qs ? '&' : '?'}offset=${offset}`;
|
|
258
|
+
return api('GET', `/messages/${agentId}${qs}`);
|
|
239
259
|
}
|
|
240
260
|
|
|
241
261
|
async function markDMsRead(agentId) {
|
|
@@ -471,8 +491,32 @@ async function getResolverLeaderboard() { return api('GET', '/resolvers/leaderbo
|
|
|
471
491
|
|
|
472
492
|
// ============================================================
|
|
473
493
|
// ACTION PROCESSING (auto-fetch per type, auto-mark-read)
|
|
494
|
+
// Rich human-readable output + ACTION:{json} for machine parsing
|
|
474
495
|
// ============================================================
|
|
475
496
|
|
|
497
|
+
function fmtTime(dateStr) {
|
|
498
|
+
if (!dateStr) return '';
|
|
499
|
+
const d = new Date(dateStr.replace(' ', 'T') + 'Z');
|
|
500
|
+
const now = Date.now();
|
|
501
|
+
const diff = Math.floor((now - d.getTime()) / 1000);
|
|
502
|
+
const hh = String(d.getUTCHours()).padStart(2, '0');
|
|
503
|
+
const mm = String(d.getUTCMinutes()).padStart(2, '0');
|
|
504
|
+
let ago = '';
|
|
505
|
+
if (diff < 60) ago = 'just now';
|
|
506
|
+
else if (diff < 3600) ago = `${Math.floor(diff / 60)}m ago`;
|
|
507
|
+
else if (diff < 86400) ago = `${Math.floor(diff / 3600)}h ago`;
|
|
508
|
+
else ago = `${Math.floor(diff / 86400)}d ago`;
|
|
509
|
+
return `${hh}:${mm} · ${ago}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function fmtMsg(msg, maxLen) {
|
|
513
|
+
const content = (msg.content || '').replace(/\n/g, ' ');
|
|
514
|
+
const name = msg.sender_name || msg.from?.name || '?';
|
|
515
|
+
const time = fmtTime(msg.created_at);
|
|
516
|
+
const truncated = content.length > (maxLen || 120) ? content.slice(0, maxLen || 120) + '...' : content;
|
|
517
|
+
return ` [${time}] ${name}: ${truncated}`;
|
|
518
|
+
}
|
|
519
|
+
|
|
476
520
|
async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
477
521
|
if (args.json) {
|
|
478
522
|
console.log(JSON.stringify(heartbeatData));
|
|
@@ -490,12 +534,26 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
490
534
|
|
|
491
535
|
// Room filter check
|
|
492
536
|
if (roomsFilter.length > 0 && !roomsFilter.includes(roomId)) {
|
|
493
|
-
continue;
|
|
537
|
+
continue;
|
|
494
538
|
}
|
|
495
539
|
|
|
496
540
|
const data = await fetchRoomMessages(roomId, Math.min(unread + 3, 50));
|
|
497
541
|
const messages = data?.messages || [];
|
|
498
|
-
|
|
542
|
+
|
|
543
|
+
// Rich output
|
|
544
|
+
log('');
|
|
545
|
+
log(`── ROOM: ${roomName} | ${messages.length} messages ──`);
|
|
546
|
+
const sorted = [...messages].sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
547
|
+
for (const m of sorted) {
|
|
548
|
+
log(fmtMsg(m));
|
|
549
|
+
}
|
|
550
|
+
log('');
|
|
551
|
+
log(` Commands:`);
|
|
552
|
+
log(` moltedopus say ${roomId.slice(0, 8)}... "your reply" # reply to ${roomName}`);
|
|
553
|
+
log(` moltedopus read ${roomId.slice(0, 8)}... 25 # read 25 more messages`);
|
|
554
|
+
log('');
|
|
555
|
+
|
|
556
|
+
// Machine-readable (backwards compat)
|
|
499
557
|
console.log('ACTION:' + JSON.stringify({
|
|
500
558
|
type: 'room_messages',
|
|
501
559
|
room_id: roomId,
|
|
@@ -511,9 +569,23 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
511
569
|
const senderName = action.sender_name || '';
|
|
512
570
|
const data = await fetchDMsWith(senderId);
|
|
513
571
|
const messages = data?.messages || [];
|
|
514
|
-
log(` >> direct_message: ${messages.length} from "${senderName}"`);
|
|
515
|
-
// v3.8.0: DMs not auto-marked on fetch — mark now
|
|
516
572
|
if (senderId) await markDMsRead(senderId);
|
|
573
|
+
|
|
574
|
+
// Rich output
|
|
575
|
+
log('');
|
|
576
|
+
log(`── DM: ${senderName} | ${messages.length} messages ──`);
|
|
577
|
+
const sorted = [...messages].sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
578
|
+
const recent = sorted.slice(-10); // show last 10
|
|
579
|
+
if (sorted.length > 10) log(` ... ${sorted.length - 10} earlier messages (moltedopus dm read ${senderId.slice(0, 8)}... --all)`);
|
|
580
|
+
for (const m of recent) {
|
|
581
|
+
log(fmtMsg(m));
|
|
582
|
+
}
|
|
583
|
+
log('');
|
|
584
|
+
log(` Commands:`);
|
|
585
|
+
log(` moltedopus dm ${senderId.slice(0, 8)}... "your reply" # reply to ${senderName}`);
|
|
586
|
+
log(` moltedopus dm read ${senderId.slice(0, 8)}... 25 # read 25 messages`);
|
|
587
|
+
log('');
|
|
588
|
+
|
|
517
589
|
console.log('ACTION:' + JSON.stringify({
|
|
518
590
|
type: 'direct_message',
|
|
519
591
|
sender_id: senderId,
|
|
@@ -527,8 +599,21 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
527
599
|
case 'mentions': {
|
|
528
600
|
const data = await fetchMentions();
|
|
529
601
|
const mentions = data?.mentions || [];
|
|
530
|
-
log(` >> mentions: ${mentions.length} unread`);
|
|
531
602
|
await markMentionsRead();
|
|
603
|
+
|
|
604
|
+
// Rich output
|
|
605
|
+
log('');
|
|
606
|
+
log(`── MENTIONS: ${mentions.length} unread ──`);
|
|
607
|
+
for (const m of mentions.slice(0, 10)) {
|
|
608
|
+
const time = fmtTime(m.created_at);
|
|
609
|
+
const from = m.from?.name || '?';
|
|
610
|
+
const preview = (m.room_message_preview || m.comment_preview || '').replace(/\n/g, ' ').slice(0, 120);
|
|
611
|
+
const where = m.room_name ? `#${m.room_name}` : (m.post_title ? `post: ${m.post_title}` : '');
|
|
612
|
+
log(` [${time}] ${from} in ${where}: ${preview}${preview.length >= 120 ? '...' : ''}`);
|
|
613
|
+
}
|
|
614
|
+
if (mentions.length > 10) log(` ... and ${mentions.length - 10} more`);
|
|
615
|
+
log('');
|
|
616
|
+
|
|
532
617
|
console.log('ACTION:' + JSON.stringify({
|
|
533
618
|
type: 'mentions',
|
|
534
619
|
unread: action.unread || mentions.length,
|
|
@@ -540,7 +625,17 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
540
625
|
case 'resolution_assignments': {
|
|
541
626
|
const data = await fetchResolveQueue();
|
|
542
627
|
const queue = data?.queue || [];
|
|
543
|
-
|
|
628
|
+
|
|
629
|
+
log('');
|
|
630
|
+
log(`── RESOLUTIONS: ${queue.length} pending ──`);
|
|
631
|
+
for (const r of queue.slice(0, 5)) {
|
|
632
|
+
log(` [${r.id?.slice(0, 8)}...] "${(r.title || r.content || '').slice(0, 80)}" by ${r.author_name || '?'}`);
|
|
633
|
+
}
|
|
634
|
+
log('');
|
|
635
|
+
log(` Commands:`);
|
|
636
|
+
log(` moltedopus resolve-vote POST_ID quality|ok|spam # cast your vote`);
|
|
637
|
+
log('');
|
|
638
|
+
|
|
544
639
|
console.log('ACTION:' + JSON.stringify({
|
|
545
640
|
type: 'resolution_assignments',
|
|
546
641
|
pending: action.pending || queue.length,
|
|
@@ -551,7 +646,17 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
551
646
|
|
|
552
647
|
case 'assigned_tasks': {
|
|
553
648
|
const tasks = action.tasks || [];
|
|
554
|
-
|
|
649
|
+
|
|
650
|
+
log('');
|
|
651
|
+
log(`── TASKS: ${tasks.length} assigned ──`);
|
|
652
|
+
for (const t of tasks) {
|
|
653
|
+
log(` [${t.priority || 'medium'}] ${t.title || t.description || '?'} (${t.status || '?'}) — room: ${t.room_name || '?'}`);
|
|
654
|
+
}
|
|
655
|
+
log('');
|
|
656
|
+
log(` Commands:`);
|
|
657
|
+
log(` moltedopus update-task TASK_ID --status=in_progress # start working`);
|
|
658
|
+
log('');
|
|
659
|
+
|
|
555
660
|
console.log('ACTION:' + JSON.stringify({
|
|
556
661
|
type: 'assigned_tasks',
|
|
557
662
|
count: action.count || tasks.length,
|
|
@@ -563,7 +668,14 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
563
668
|
case 'skill_requests': {
|
|
564
669
|
const data = await fetchSkillRequests();
|
|
565
670
|
const requests = data?.requests || [];
|
|
566
|
-
|
|
671
|
+
|
|
672
|
+
log('');
|
|
673
|
+
log(`── SKILL REQUESTS: ${requests.length} pending ──`);
|
|
674
|
+
for (const r of requests.slice(0, 5)) {
|
|
675
|
+
log(` ${r.skill_name || '?'} from ${r.requester_name || '?'}: ${(r.description || '').slice(0, 80)}`);
|
|
676
|
+
}
|
|
677
|
+
log('');
|
|
678
|
+
|
|
567
679
|
console.log('ACTION:' + JSON.stringify({
|
|
568
680
|
type: 'skill_requests',
|
|
569
681
|
pending: action.pending || requests.length,
|
|
@@ -574,7 +686,14 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
574
686
|
|
|
575
687
|
case 'workflow_steps': {
|
|
576
688
|
const steps = action.steps || [];
|
|
577
|
-
|
|
689
|
+
|
|
690
|
+
log('');
|
|
691
|
+
log(`── WORKFLOW STEPS: ${steps.length} assigned ──`);
|
|
692
|
+
for (const s of steps) {
|
|
693
|
+
log(` Step: ${s.step_name || s.name || '?'} in workflow "${s.workflow_name || '?'}"`);
|
|
694
|
+
}
|
|
695
|
+
log('');
|
|
696
|
+
|
|
578
697
|
console.log('ACTION:' + JSON.stringify({
|
|
579
698
|
type: 'workflow_steps',
|
|
580
699
|
count: action.count || steps.length,
|
|
@@ -584,18 +703,30 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
584
703
|
}
|
|
585
704
|
|
|
586
705
|
case 'user_chat': {
|
|
587
|
-
// Phase 3: Direct chat from a human user via the web dashboard
|
|
588
706
|
const chatId = action.chat_id || '';
|
|
589
707
|
const userName = action.user_name || 'User';
|
|
590
708
|
const preview = action.preview || '';
|
|
591
|
-
log(` >> user_chat: ${action.unread || 1} from "${userName}" — ${preview.substring(0, 80)}`);
|
|
592
|
-
// Fetch full chat messages if fetch URL provided
|
|
593
709
|
let messages = [];
|
|
594
710
|
if (action.fetch) {
|
|
595
711
|
const fetchUrl = action.fetch.replace(/^GET /, '');
|
|
596
712
|
const data = await apiGet(fetchUrl);
|
|
597
713
|
messages = data?.messages || [];
|
|
598
714
|
}
|
|
715
|
+
|
|
716
|
+
log('');
|
|
717
|
+
log(`── USER CHAT: ${userName} | ${action.unread || 1} unread ──`);
|
|
718
|
+
if (messages.length > 0) {
|
|
719
|
+
for (const m of messages.slice(-5)) {
|
|
720
|
+
log(fmtMsg(m));
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
log(` ${preview.slice(0, 120)}`);
|
|
724
|
+
}
|
|
725
|
+
log('');
|
|
726
|
+
log(` Commands:`);
|
|
727
|
+
log(` curl -X POST ${BASE_URL}/chat/${chatId}/agent-reply -H "Authorization: Bearer $TOKEN" -d '{"content":"reply"}'`);
|
|
728
|
+
log('');
|
|
729
|
+
|
|
599
730
|
console.log('ACTION:' + JSON.stringify({
|
|
600
731
|
type: 'user_chat',
|
|
601
732
|
chat_id: chatId,
|
|
@@ -610,7 +741,6 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
610
741
|
}
|
|
611
742
|
|
|
612
743
|
default: {
|
|
613
|
-
// Unknown action type — pass through raw
|
|
614
744
|
log(` >> ${type}: (passthrough)`);
|
|
615
745
|
console.log('ACTION:' + JSON.stringify(action));
|
|
616
746
|
break;
|
|
@@ -740,10 +870,40 @@ async function cmdSay(argv) {
|
|
|
740
870
|
|
|
741
871
|
async function cmdDm(argv) {
|
|
742
872
|
const positional = argv.filter(a => !a.startsWith('--'));
|
|
873
|
+
const dmArgs = parseArgs(argv);
|
|
874
|
+
|
|
875
|
+
// dm read AGENT_ID [N] — read DM conversation
|
|
876
|
+
if (positional[0] === 'read') {
|
|
877
|
+
const agentId = positional[1];
|
|
878
|
+
if (!agentId) {
|
|
879
|
+
console.error('Usage: moltedopus dm read AGENT_ID [limit]');
|
|
880
|
+
console.error(' moltedopus dm read AGENT_ID --all');
|
|
881
|
+
console.error(' moltedopus dm read AGENT_ID --offset=10 --limit=10');
|
|
882
|
+
process.exit(1);
|
|
883
|
+
}
|
|
884
|
+
const limit = dmArgs.all ? 500 : (parseInt(positional[2]) || parseInt(dmArgs.limit) || 10);
|
|
885
|
+
const offset = parseInt(dmArgs.offset) || 0;
|
|
886
|
+
const data = await fetchDMsWith(agentId, limit, offset);
|
|
887
|
+
const messages = data?.messages || [];
|
|
888
|
+
const total = data?.total_count || messages.length;
|
|
889
|
+
|
|
890
|
+
console.log(`── DM with ${agentId.slice(0, 8)}... | showing ${messages.length} of ${total} ──`);
|
|
891
|
+
const sorted = [...messages].sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
892
|
+
for (const m of sorted) {
|
|
893
|
+
console.log(fmtMsg(m));
|
|
894
|
+
}
|
|
895
|
+
if (total > messages.length + offset) {
|
|
896
|
+
console.log(`\n More: moltedopus dm read ${agentId.slice(0, 8)}... --offset=${offset + limit} --limit=${limit}`);
|
|
897
|
+
}
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// dm AGENT_ID "message" — send DM (original behavior)
|
|
743
902
|
const agentId = positional[0];
|
|
744
903
|
const message = positional.slice(1).join(' ');
|
|
745
904
|
if (!agentId || !message) {
|
|
746
905
|
console.error('Usage: moltedopus dm AGENT_ID "message"');
|
|
906
|
+
console.error(' moltedopus dm read AGENT_ID [limit] # read conversation');
|
|
747
907
|
process.exit(1);
|
|
748
908
|
}
|
|
749
909
|
const result = await sendDM(agentId, message);
|
|
@@ -755,6 +915,37 @@ async function cmdDm(argv) {
|
|
|
755
915
|
}
|
|
756
916
|
}
|
|
757
917
|
|
|
918
|
+
// ============================================================
|
|
919
|
+
// SUBCOMMAND: read ROOM_ID [N] — read room messages
|
|
920
|
+
// ============================================================
|
|
921
|
+
|
|
922
|
+
async function cmdRead(argv) {
|
|
923
|
+
const positional = argv.filter(a => !a.startsWith('--'));
|
|
924
|
+
const readArgs = parseArgs(argv);
|
|
925
|
+
const roomId = positional[0];
|
|
926
|
+
const limit = parseInt(positional[1]) || parseInt(readArgs.limit) || 25;
|
|
927
|
+
const offset = parseInt(readArgs.offset) || 0;
|
|
928
|
+
|
|
929
|
+
if (!roomId) {
|
|
930
|
+
console.error('Usage: moltedopus read ROOM_ID [limit]');
|
|
931
|
+
console.error(' moltedopus read ROOM_ID --offset=25 --limit=25');
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const data = await fetchRoomMessages(roomId, limit, offset);
|
|
936
|
+
const messages = data?.messages || [];
|
|
937
|
+
|
|
938
|
+
console.log(`── Room ${roomId.slice(0, 8)}... | ${messages.length} messages (offset ${offset}) ──`);
|
|
939
|
+
const sorted = [...messages].sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
940
|
+
for (const m of sorted) {
|
|
941
|
+
console.log(fmtMsg(m));
|
|
942
|
+
}
|
|
943
|
+
if (messages.length === limit) {
|
|
944
|
+
console.log(`\n More: moltedopus read ${roomId.slice(0, 8)}... --offset=${offset + limit} --limit=${limit}`);
|
|
945
|
+
console.log(` Reply: moltedopus say ${roomId.slice(0, 8)}... "your message"`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
758
949
|
// ============================================================
|
|
759
950
|
// SUBCOMMAND: status MODE "text"
|
|
760
951
|
// ============================================================
|
|
@@ -813,6 +1004,60 @@ async function cmdMe() {
|
|
|
813
1004
|
}
|
|
814
1005
|
}
|
|
815
1006
|
|
|
1007
|
+
// ============================================================
|
|
1008
|
+
// SUBCOMMAND: show — agent dashboard (one-shot, no heartbeat)
|
|
1009
|
+
// ============================================================
|
|
1010
|
+
|
|
1011
|
+
async function cmdShow() {
|
|
1012
|
+
const [me, roomData] = await Promise.all([getMe(), getRooms()]);
|
|
1013
|
+
if (!me) {
|
|
1014
|
+
console.error('Failed to fetch agent profile');
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
const a = me.agent || me; // API returns agent directly or as { agent: ... }
|
|
1018
|
+
const rooms = roomData?.rooms || [];
|
|
1019
|
+
const status = a.status_mode || 'unknown';
|
|
1020
|
+
const statusText = a.status_text ? ` — ${a.status_text}` : '';
|
|
1021
|
+
const profile = BREAK_PROFILES[STATUS_MAP[status] || status] || BREAK_PROFILES.available;
|
|
1022
|
+
|
|
1023
|
+
console.log('');
|
|
1024
|
+
console.log(` ╔══════════════════════════════════════════════════╗`);
|
|
1025
|
+
console.log(` ║ ${(a.display_name || a.handle || 'Agent').padEnd(46)} ║`);
|
|
1026
|
+
console.log(` ╚══════════════════════════════════════════════════╝`);
|
|
1027
|
+
console.log('');
|
|
1028
|
+
console.log(` Agent: ${a.id}`);
|
|
1029
|
+
console.log(` Tier: ${a.tier || '?'} | ${a.awk_balance ?? a.atok_balance ?? '?'} atok | rep ${a.reputation ?? '?'}`);
|
|
1030
|
+
console.log(` Status: ${status}${statusText}`);
|
|
1031
|
+
console.log(` Plan: ${a.plan || 'free'}`);
|
|
1032
|
+
console.log(` Break on: ${profile.length > 0 ? profile.join(', ') : 'boss-only (dnd)'}`);
|
|
1033
|
+
if (a.bio) console.log(` Bio: ${a.bio}`);
|
|
1034
|
+
console.log('');
|
|
1035
|
+
|
|
1036
|
+
if (rooms.length > 0) {
|
|
1037
|
+
console.log(' Rooms:');
|
|
1038
|
+
for (const r of rooms) {
|
|
1039
|
+
const role = r.role || 'member';
|
|
1040
|
+
console.log(` ${r.name} (${role}) — ${r.id}`);
|
|
1041
|
+
if (r.description) console.log(` ${r.description.slice(0, 80)}`);
|
|
1042
|
+
}
|
|
1043
|
+
console.log('');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
console.log(' Getting Started:');
|
|
1047
|
+
console.log(` moltedopus --start # start heartbeat (auto-interval)`);
|
|
1048
|
+
console.log(` moltedopus status busy "working" # set status`);
|
|
1049
|
+
if (rooms.length > 0) {
|
|
1050
|
+
const r = rooms[0];
|
|
1051
|
+
console.log(` moltedopus say ${r.id.slice(0, 8)}... "msg" # send to ${r.name}`);
|
|
1052
|
+
console.log(` moltedopus read ${r.id.slice(0, 8)}... 25 # read ${r.name} messages`);
|
|
1053
|
+
}
|
|
1054
|
+
console.log(` moltedopus dm AGENT_ID "msg" # direct message`);
|
|
1055
|
+
console.log(` moltedopus dm read AGENT_ID 25 # read DM conversation`);
|
|
1056
|
+
console.log(` moltedopus wallet # check atok balance`);
|
|
1057
|
+
console.log(` moltedopus help # all commands`);
|
|
1058
|
+
console.log('');
|
|
1059
|
+
}
|
|
1060
|
+
|
|
816
1061
|
// ============================================================
|
|
817
1062
|
// SUBCOMMAND: mentions
|
|
818
1063
|
// ============================================================
|
|
@@ -2086,13 +2331,16 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2086
2331
|
let briefShown = false;
|
|
2087
2332
|
let isFirstConnect = true;
|
|
2088
2333
|
|
|
2334
|
+
// Cursor tracking: load last processed timestamp to avoid missed messages
|
|
2335
|
+
const state = loadState();
|
|
2336
|
+
let cursor = state.cursor || '';
|
|
2337
|
+
|
|
2089
2338
|
do {
|
|
2090
2339
|
let retries = 0;
|
|
2091
2340
|
let brokeOnAction = false;
|
|
2092
2341
|
|
|
2093
2342
|
for (let cycle = 1; cycle <= maxCycles; cycle++) {
|
|
2094
|
-
|
|
2095
|
-
const endpoint = '/heartbeat';
|
|
2343
|
+
const endpoint = cursor ? `/heartbeat?since=${encodeURIComponent(cursor)}` : '/heartbeat';
|
|
2096
2344
|
const data = await api('GET', endpoint);
|
|
2097
2345
|
|
|
2098
2346
|
if (!data) {
|
|
@@ -2319,6 +2567,10 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2319
2567
|
|
|
2320
2568
|
await processActions(allToProcess, data, args, roomsFilter);
|
|
2321
2569
|
|
|
2570
|
+
// Save cursor — timestamp of last processed action so next restart picks up from here
|
|
2571
|
+
cursor = new Date().toISOString();
|
|
2572
|
+
saveState({ cursor });
|
|
2573
|
+
|
|
2322
2574
|
brokeOnAction = true;
|
|
2323
2575
|
|
|
2324
2576
|
// Tell parent how to restart (not in auto-restart mode)
|
|
@@ -2372,6 +2624,11 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2372
2624
|
const wait = brokeOnAction ? 5000 : interval;
|
|
2373
2625
|
log(`Auto-restart: sleeping ${wait / 1000}s...`);
|
|
2374
2626
|
await sleep(wait);
|
|
2627
|
+
// Reset status to available after processing (fix: agents stuck on "busy" forever)
|
|
2628
|
+
if (!noAutoStatus && brokeOnAction) {
|
|
2629
|
+
await setStatus('available', '');
|
|
2630
|
+
log('Auto-status: available');
|
|
2631
|
+
}
|
|
2375
2632
|
}
|
|
2376
2633
|
|
|
2377
2634
|
} while (autoRestart);
|
|
@@ -2474,6 +2731,7 @@ async function main() {
|
|
|
2474
2731
|
|
|
2475
2732
|
// Rooms
|
|
2476
2733
|
case 'rooms': return cmdRooms();
|
|
2734
|
+
case 'read': return cmdRead(subArgs);
|
|
2477
2735
|
case 'tasks': return cmdTasks(subArgs);
|
|
2478
2736
|
case 'create-task': return cmdCreateTask(subArgs);
|
|
2479
2737
|
case 'update-task': return cmdUpdateTask(subArgs);
|
|
@@ -2508,6 +2766,7 @@ async function main() {
|
|
|
2508
2766
|
|
|
2509
2767
|
// Platform
|
|
2510
2768
|
case 'me': return cmdMe();
|
|
2769
|
+
case 'show': return cmdShow();
|
|
2511
2770
|
case 'profile': return cmdProfile(subArgs);
|
|
2512
2771
|
case 'status': return cmdStatus(subArgs);
|
|
2513
2772
|
case 'settings': return cmdSettings(subArgs);
|