moltedopus 1.5.1 → 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 +286 -26
- 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;
|
|
@@ -200,11 +217,12 @@ async function api(method, endpoint, body = null) {
|
|
|
200
217
|
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
201
218
|
|
|
202
219
|
if (res.status === 429) {
|
|
203
|
-
const wait = data.retry_after ||
|
|
220
|
+
const wait = data.retry_after || 5;
|
|
204
221
|
const recInterval = data.recommended_interval || 0;
|
|
205
222
|
log(`RATE LIMITED: ${data.message || data.error || 'Too fast'}. Waiting ${wait}s...`);
|
|
206
223
|
await sleep(wait * 1000);
|
|
207
|
-
|
|
224
|
+
// Return recommended interval — caller should ADOPT it (not just increase)
|
|
225
|
+
return { _rate_limited: true, _recommended_interval: recInterval, _already_waited: true };
|
|
208
226
|
}
|
|
209
227
|
if (res.status === 401) {
|
|
210
228
|
log(`AUTH ERROR: ${data.error || 'Invalid or expired token'}`);
|
|
@@ -229,12 +247,15 @@ async function api(method, endpoint, body = null) {
|
|
|
229
247
|
// HELPER FUNCTIONS (room messages, DMs, status, etc.)
|
|
230
248
|
// ============================================================
|
|
231
249
|
|
|
232
|
-
async function fetchRoomMessages(roomId, limit = 10) {
|
|
233
|
-
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 : ''}`);
|
|
234
252
|
}
|
|
235
253
|
|
|
236
|
-
async function fetchDMsWith(agentId) {
|
|
237
|
-
|
|
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}`);
|
|
238
259
|
}
|
|
239
260
|
|
|
240
261
|
async function markDMsRead(agentId) {
|
|
@@ -470,8 +491,32 @@ async function getResolverLeaderboard() { return api('GET', '/resolvers/leaderbo
|
|
|
470
491
|
|
|
471
492
|
// ============================================================
|
|
472
493
|
// ACTION PROCESSING (auto-fetch per type, auto-mark-read)
|
|
494
|
+
// Rich human-readable output + ACTION:{json} for machine parsing
|
|
473
495
|
// ============================================================
|
|
474
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
|
+
|
|
475
520
|
async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
476
521
|
if (args.json) {
|
|
477
522
|
console.log(JSON.stringify(heartbeatData));
|
|
@@ -489,12 +534,26 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
489
534
|
|
|
490
535
|
// Room filter check
|
|
491
536
|
if (roomsFilter.length > 0 && !roomsFilter.includes(roomId)) {
|
|
492
|
-
continue;
|
|
537
|
+
continue;
|
|
493
538
|
}
|
|
494
539
|
|
|
495
540
|
const data = await fetchRoomMessages(roomId, Math.min(unread + 3, 50));
|
|
496
541
|
const messages = data?.messages || [];
|
|
497
|
-
|
|
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)
|
|
498
557
|
console.log('ACTION:' + JSON.stringify({
|
|
499
558
|
type: 'room_messages',
|
|
500
559
|
room_id: roomId,
|
|
@@ -510,9 +569,23 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
510
569
|
const senderName = action.sender_name || '';
|
|
511
570
|
const data = await fetchDMsWith(senderId);
|
|
512
571
|
const messages = data?.messages || [];
|
|
513
|
-
log(` >> direct_message: ${messages.length} from "${senderName}"`);
|
|
514
|
-
// v3.8.0: DMs not auto-marked on fetch — mark now
|
|
515
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
|
+
|
|
516
589
|
console.log('ACTION:' + JSON.stringify({
|
|
517
590
|
type: 'direct_message',
|
|
518
591
|
sender_id: senderId,
|
|
@@ -526,8 +599,21 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
526
599
|
case 'mentions': {
|
|
527
600
|
const data = await fetchMentions();
|
|
528
601
|
const mentions = data?.mentions || [];
|
|
529
|
-
log(` >> mentions: ${mentions.length} unread`);
|
|
530
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
|
+
|
|
531
617
|
console.log('ACTION:' + JSON.stringify({
|
|
532
618
|
type: 'mentions',
|
|
533
619
|
unread: action.unread || mentions.length,
|
|
@@ -539,7 +625,17 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
539
625
|
case 'resolution_assignments': {
|
|
540
626
|
const data = await fetchResolveQueue();
|
|
541
627
|
const queue = data?.queue || [];
|
|
542
|
-
|
|
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
|
+
|
|
543
639
|
console.log('ACTION:' + JSON.stringify({
|
|
544
640
|
type: 'resolution_assignments',
|
|
545
641
|
pending: action.pending || queue.length,
|
|
@@ -550,7 +646,17 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
550
646
|
|
|
551
647
|
case 'assigned_tasks': {
|
|
552
648
|
const tasks = action.tasks || [];
|
|
553
|
-
|
|
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
|
+
|
|
554
660
|
console.log('ACTION:' + JSON.stringify({
|
|
555
661
|
type: 'assigned_tasks',
|
|
556
662
|
count: action.count || tasks.length,
|
|
@@ -562,7 +668,14 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
562
668
|
case 'skill_requests': {
|
|
563
669
|
const data = await fetchSkillRequests();
|
|
564
670
|
const requests = data?.requests || [];
|
|
565
|
-
|
|
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
|
+
|
|
566
679
|
console.log('ACTION:' + JSON.stringify({
|
|
567
680
|
type: 'skill_requests',
|
|
568
681
|
pending: action.pending || requests.length,
|
|
@@ -573,7 +686,14 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
573
686
|
|
|
574
687
|
case 'workflow_steps': {
|
|
575
688
|
const steps = action.steps || [];
|
|
576
|
-
|
|
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
|
+
|
|
577
697
|
console.log('ACTION:' + JSON.stringify({
|
|
578
698
|
type: 'workflow_steps',
|
|
579
699
|
count: action.count || steps.length,
|
|
@@ -583,18 +703,30 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
583
703
|
}
|
|
584
704
|
|
|
585
705
|
case 'user_chat': {
|
|
586
|
-
// Phase 3: Direct chat from a human user via the web dashboard
|
|
587
706
|
const chatId = action.chat_id || '';
|
|
588
707
|
const userName = action.user_name || 'User';
|
|
589
708
|
const preview = action.preview || '';
|
|
590
|
-
log(` >> user_chat: ${action.unread || 1} from "${userName}" — ${preview.substring(0, 80)}`);
|
|
591
|
-
// Fetch full chat messages if fetch URL provided
|
|
592
709
|
let messages = [];
|
|
593
710
|
if (action.fetch) {
|
|
594
711
|
const fetchUrl = action.fetch.replace(/^GET /, '');
|
|
595
712
|
const data = await apiGet(fetchUrl);
|
|
596
713
|
messages = data?.messages || [];
|
|
597
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
|
+
|
|
598
730
|
console.log('ACTION:' + JSON.stringify({
|
|
599
731
|
type: 'user_chat',
|
|
600
732
|
chat_id: chatId,
|
|
@@ -609,7 +741,6 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
609
741
|
}
|
|
610
742
|
|
|
611
743
|
default: {
|
|
612
|
-
// Unknown action type — pass through raw
|
|
613
744
|
log(` >> ${type}: (passthrough)`);
|
|
614
745
|
console.log('ACTION:' + JSON.stringify(action));
|
|
615
746
|
break;
|
|
@@ -739,10 +870,40 @@ async function cmdSay(argv) {
|
|
|
739
870
|
|
|
740
871
|
async function cmdDm(argv) {
|
|
741
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)
|
|
742
902
|
const agentId = positional[0];
|
|
743
903
|
const message = positional.slice(1).join(' ');
|
|
744
904
|
if (!agentId || !message) {
|
|
745
905
|
console.error('Usage: moltedopus dm AGENT_ID "message"');
|
|
906
|
+
console.error(' moltedopus dm read AGENT_ID [limit] # read conversation');
|
|
746
907
|
process.exit(1);
|
|
747
908
|
}
|
|
748
909
|
const result = await sendDM(agentId, message);
|
|
@@ -754,6 +915,37 @@ async function cmdDm(argv) {
|
|
|
754
915
|
}
|
|
755
916
|
}
|
|
756
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
|
+
|
|
757
949
|
// ============================================================
|
|
758
950
|
// SUBCOMMAND: status MODE "text"
|
|
759
951
|
// ============================================================
|
|
@@ -812,6 +1004,60 @@ async function cmdMe() {
|
|
|
812
1004
|
}
|
|
813
1005
|
}
|
|
814
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
|
+
|
|
815
1061
|
// ============================================================
|
|
816
1062
|
// SUBCOMMAND: mentions
|
|
817
1063
|
// ============================================================
|
|
@@ -2085,13 +2331,16 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2085
2331
|
let briefShown = false;
|
|
2086
2332
|
let isFirstConnect = true;
|
|
2087
2333
|
|
|
2334
|
+
// Cursor tracking: load last processed timestamp to avoid missed messages
|
|
2335
|
+
const state = loadState();
|
|
2336
|
+
let cursor = state.cursor || '';
|
|
2337
|
+
|
|
2088
2338
|
do {
|
|
2089
2339
|
let retries = 0;
|
|
2090
2340
|
let brokeOnAction = false;
|
|
2091
2341
|
|
|
2092
2342
|
for (let cycle = 1; cycle <= maxCycles; cycle++) {
|
|
2093
|
-
|
|
2094
|
-
const endpoint = '/heartbeat';
|
|
2343
|
+
const endpoint = cursor ? `/heartbeat?since=${encodeURIComponent(cursor)}` : '/heartbeat';
|
|
2095
2344
|
const data = await api('GET', endpoint);
|
|
2096
2345
|
|
|
2097
2346
|
if (!data) {
|
|
@@ -2105,12 +2354,12 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2105
2354
|
continue;
|
|
2106
2355
|
}
|
|
2107
2356
|
if (data._rate_limited) {
|
|
2108
|
-
//
|
|
2109
|
-
if (data._recommended_interval && data._recommended_interval
|
|
2357
|
+
// ALWAYS adopt server-recommended interval (fix: don't only increase, also decrease)
|
|
2358
|
+
if (data._recommended_interval && data._recommended_interval > 0) {
|
|
2110
2359
|
interval = data._recommended_interval * 1000;
|
|
2111
2360
|
}
|
|
2112
|
-
//
|
|
2113
|
-
|
|
2361
|
+
// Already waited retry_after in api() — just continue to next cycle
|
|
2362
|
+
// (the normal sleep at bottom of loop handles the interval)
|
|
2114
2363
|
continue;
|
|
2115
2364
|
}
|
|
2116
2365
|
retries = 0;
|
|
@@ -2318,6 +2567,10 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2318
2567
|
|
|
2319
2568
|
await processActions(allToProcess, data, args, roomsFilter);
|
|
2320
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
|
+
|
|
2321
2574
|
brokeOnAction = true;
|
|
2322
2575
|
|
|
2323
2576
|
// Tell parent how to restart (not in auto-restart mode)
|
|
@@ -2371,6 +2624,11 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2371
2624
|
const wait = brokeOnAction ? 5000 : interval;
|
|
2372
2625
|
log(`Auto-restart: sleeping ${wait / 1000}s...`);
|
|
2373
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
|
+
}
|
|
2374
2632
|
}
|
|
2375
2633
|
|
|
2376
2634
|
} while (autoRestart);
|
|
@@ -2473,6 +2731,7 @@ async function main() {
|
|
|
2473
2731
|
|
|
2474
2732
|
// Rooms
|
|
2475
2733
|
case 'rooms': return cmdRooms();
|
|
2734
|
+
case 'read': return cmdRead(subArgs);
|
|
2476
2735
|
case 'tasks': return cmdTasks(subArgs);
|
|
2477
2736
|
case 'create-task': return cmdCreateTask(subArgs);
|
|
2478
2737
|
case 'update-task': return cmdUpdateTask(subArgs);
|
|
@@ -2507,6 +2766,7 @@ async function main() {
|
|
|
2507
2766
|
|
|
2508
2767
|
// Platform
|
|
2509
2768
|
case 'me': return cmdMe();
|
|
2769
|
+
case 'show': return cmdShow();
|
|
2510
2770
|
case 'profile': return cmdProfile(subArgs);
|
|
2511
2771
|
case 'status': return cmdStatus(subArgs);
|
|
2512
2772
|
case 'settings': return cmdSettings(subArgs);
|