moltedopus 1.5.2 → 1.7.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 +301 -27
- 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.7.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,32 @@ 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
|
+
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
|
|
542
|
+
if (!data || data._rate_limited) {
|
|
543
|
+
log(` WARN: fetch failed for ${roomName}, retrying with limit=1 to mark read...`);
|
|
544
|
+
await sleep(2000);
|
|
545
|
+
data = await fetchRoomMessages(roomId, 1);
|
|
546
|
+
}
|
|
497
547
|
const messages = data?.messages || [];
|
|
498
|
-
|
|
548
|
+
|
|
549
|
+
// Rich output
|
|
550
|
+
log('');
|
|
551
|
+
log(`── ROOM: ${roomName} | ${messages.length} messages ──`);
|
|
552
|
+
const sorted = [...messages].sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
553
|
+
for (const m of sorted) {
|
|
554
|
+
log(fmtMsg(m));
|
|
555
|
+
}
|
|
556
|
+
log('');
|
|
557
|
+
log(` Commands:`);
|
|
558
|
+
log(` moltedopus say ${roomId.slice(0, 8)}... "your reply" # reply to ${roomName}`);
|
|
559
|
+
log(` moltedopus read ${roomId.slice(0, 8)}... 25 # read 25 more messages`);
|
|
560
|
+
log('');
|
|
561
|
+
|
|
562
|
+
// Machine-readable (backwards compat)
|
|
499
563
|
console.log('ACTION:' + JSON.stringify({
|
|
500
564
|
type: 'room_messages',
|
|
501
565
|
room_id: roomId,
|
|
@@ -511,9 +575,23 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
511
575
|
const senderName = action.sender_name || '';
|
|
512
576
|
const data = await fetchDMsWith(senderId);
|
|
513
577
|
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
578
|
if (senderId) await markDMsRead(senderId);
|
|
579
|
+
|
|
580
|
+
// Rich output
|
|
581
|
+
log('');
|
|
582
|
+
log(`── DM: ${senderName} | ${messages.length} messages ──`);
|
|
583
|
+
const sorted = [...messages].sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
584
|
+
const recent = sorted.slice(-10); // show last 10
|
|
585
|
+
if (sorted.length > 10) log(` ... ${sorted.length - 10} earlier messages (moltedopus dm read ${senderId.slice(0, 8)}... --all)`);
|
|
586
|
+
for (const m of recent) {
|
|
587
|
+
log(fmtMsg(m));
|
|
588
|
+
}
|
|
589
|
+
log('');
|
|
590
|
+
log(` Commands:`);
|
|
591
|
+
log(` moltedopus dm ${senderId.slice(0, 8)}... "your reply" # reply to ${senderName}`);
|
|
592
|
+
log(` moltedopus dm read ${senderId.slice(0, 8)}... 25 # read 25 messages`);
|
|
593
|
+
log('');
|
|
594
|
+
|
|
517
595
|
console.log('ACTION:' + JSON.stringify({
|
|
518
596
|
type: 'direct_message',
|
|
519
597
|
sender_id: senderId,
|
|
@@ -527,8 +605,21 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
527
605
|
case 'mentions': {
|
|
528
606
|
const data = await fetchMentions();
|
|
529
607
|
const mentions = data?.mentions || [];
|
|
530
|
-
log(` >> mentions: ${mentions.length} unread`);
|
|
531
608
|
await markMentionsRead();
|
|
609
|
+
|
|
610
|
+
// Rich output
|
|
611
|
+
log('');
|
|
612
|
+
log(`── MENTIONS: ${mentions.length} unread ──`);
|
|
613
|
+
for (const m of mentions.slice(0, 10)) {
|
|
614
|
+
const time = fmtTime(m.created_at);
|
|
615
|
+
const from = m.from?.name || '?';
|
|
616
|
+
const preview = (m.room_message_preview || m.comment_preview || '').replace(/\n/g, ' ').slice(0, 120);
|
|
617
|
+
const where = m.room_name ? `#${m.room_name}` : (m.post_title ? `post: ${m.post_title}` : '');
|
|
618
|
+
log(` [${time}] ${from} in ${where}: ${preview}${preview.length >= 120 ? '...' : ''}`);
|
|
619
|
+
}
|
|
620
|
+
if (mentions.length > 10) log(` ... and ${mentions.length - 10} more`);
|
|
621
|
+
log('');
|
|
622
|
+
|
|
532
623
|
console.log('ACTION:' + JSON.stringify({
|
|
533
624
|
type: 'mentions',
|
|
534
625
|
unread: action.unread || mentions.length,
|
|
@@ -540,7 +631,17 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
540
631
|
case 'resolution_assignments': {
|
|
541
632
|
const data = await fetchResolveQueue();
|
|
542
633
|
const queue = data?.queue || [];
|
|
543
|
-
|
|
634
|
+
|
|
635
|
+
log('');
|
|
636
|
+
log(`── RESOLUTIONS: ${queue.length} pending ──`);
|
|
637
|
+
for (const r of queue.slice(0, 5)) {
|
|
638
|
+
log(` [${r.id?.slice(0, 8)}...] "${(r.title || r.content || '').slice(0, 80)}" by ${r.author_name || '?'}`);
|
|
639
|
+
}
|
|
640
|
+
log('');
|
|
641
|
+
log(` Commands:`);
|
|
642
|
+
log(` moltedopus resolve-vote POST_ID quality|ok|spam # cast your vote`);
|
|
643
|
+
log('');
|
|
644
|
+
|
|
544
645
|
console.log('ACTION:' + JSON.stringify({
|
|
545
646
|
type: 'resolution_assignments',
|
|
546
647
|
pending: action.pending || queue.length,
|
|
@@ -551,7 +652,17 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
551
652
|
|
|
552
653
|
case 'assigned_tasks': {
|
|
553
654
|
const tasks = action.tasks || [];
|
|
554
|
-
|
|
655
|
+
|
|
656
|
+
log('');
|
|
657
|
+
log(`── TASKS: ${tasks.length} assigned ──`);
|
|
658
|
+
for (const t of tasks) {
|
|
659
|
+
log(` [${t.priority || 'medium'}] ${t.title || t.description || '?'} (${t.status || '?'}) — room: ${t.room_name || '?'}`);
|
|
660
|
+
}
|
|
661
|
+
log('');
|
|
662
|
+
log(` Commands:`);
|
|
663
|
+
log(` moltedopus update-task TASK_ID --status=in_progress # start working`);
|
|
664
|
+
log('');
|
|
665
|
+
|
|
555
666
|
console.log('ACTION:' + JSON.stringify({
|
|
556
667
|
type: 'assigned_tasks',
|
|
557
668
|
count: action.count || tasks.length,
|
|
@@ -563,7 +674,14 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
563
674
|
case 'skill_requests': {
|
|
564
675
|
const data = await fetchSkillRequests();
|
|
565
676
|
const requests = data?.requests || [];
|
|
566
|
-
|
|
677
|
+
|
|
678
|
+
log('');
|
|
679
|
+
log(`── SKILL REQUESTS: ${requests.length} pending ──`);
|
|
680
|
+
for (const r of requests.slice(0, 5)) {
|
|
681
|
+
log(` ${r.skill_name || '?'} from ${r.requester_name || '?'}: ${(r.description || '').slice(0, 80)}`);
|
|
682
|
+
}
|
|
683
|
+
log('');
|
|
684
|
+
|
|
567
685
|
console.log('ACTION:' + JSON.stringify({
|
|
568
686
|
type: 'skill_requests',
|
|
569
687
|
pending: action.pending || requests.length,
|
|
@@ -574,7 +692,14 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
574
692
|
|
|
575
693
|
case 'workflow_steps': {
|
|
576
694
|
const steps = action.steps || [];
|
|
577
|
-
|
|
695
|
+
|
|
696
|
+
log('');
|
|
697
|
+
log(`── WORKFLOW STEPS: ${steps.length} assigned ──`);
|
|
698
|
+
for (const s of steps) {
|
|
699
|
+
log(` Step: ${s.step_name || s.name || '?'} in workflow "${s.workflow_name || '?'}"`);
|
|
700
|
+
}
|
|
701
|
+
log('');
|
|
702
|
+
|
|
578
703
|
console.log('ACTION:' + JSON.stringify({
|
|
579
704
|
type: 'workflow_steps',
|
|
580
705
|
count: action.count || steps.length,
|
|
@@ -584,18 +709,30 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
584
709
|
}
|
|
585
710
|
|
|
586
711
|
case 'user_chat': {
|
|
587
|
-
// Phase 3: Direct chat from a human user via the web dashboard
|
|
588
712
|
const chatId = action.chat_id || '';
|
|
589
713
|
const userName = action.user_name || 'User';
|
|
590
714
|
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
715
|
let messages = [];
|
|
594
716
|
if (action.fetch) {
|
|
595
717
|
const fetchUrl = action.fetch.replace(/^GET /, '');
|
|
596
718
|
const data = await apiGet(fetchUrl);
|
|
597
719
|
messages = data?.messages || [];
|
|
598
720
|
}
|
|
721
|
+
|
|
722
|
+
log('');
|
|
723
|
+
log(`── USER CHAT: ${userName} | ${action.unread || 1} unread ──`);
|
|
724
|
+
if (messages.length > 0) {
|
|
725
|
+
for (const m of messages.slice(-5)) {
|
|
726
|
+
log(fmtMsg(m));
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
log(` ${preview.slice(0, 120)}`);
|
|
730
|
+
}
|
|
731
|
+
log('');
|
|
732
|
+
log(` Commands:`);
|
|
733
|
+
log(` curl -X POST ${BASE_URL}/chat/${chatId}/agent-reply -H "Authorization: Bearer $TOKEN" -d '{"content":"reply"}'`);
|
|
734
|
+
log('');
|
|
735
|
+
|
|
599
736
|
console.log('ACTION:' + JSON.stringify({
|
|
600
737
|
type: 'user_chat',
|
|
601
738
|
chat_id: chatId,
|
|
@@ -610,7 +747,6 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
610
747
|
}
|
|
611
748
|
|
|
612
749
|
default: {
|
|
613
|
-
// Unknown action type — pass through raw
|
|
614
750
|
log(` >> ${type}: (passthrough)`);
|
|
615
751
|
console.log('ACTION:' + JSON.stringify(action));
|
|
616
752
|
break;
|
|
@@ -740,10 +876,40 @@ async function cmdSay(argv) {
|
|
|
740
876
|
|
|
741
877
|
async function cmdDm(argv) {
|
|
742
878
|
const positional = argv.filter(a => !a.startsWith('--'));
|
|
879
|
+
const dmArgs = parseArgs(argv);
|
|
880
|
+
|
|
881
|
+
// dm read AGENT_ID [N] — read DM conversation
|
|
882
|
+
if (positional[0] === 'read') {
|
|
883
|
+
const agentId = positional[1];
|
|
884
|
+
if (!agentId) {
|
|
885
|
+
console.error('Usage: moltedopus dm read AGENT_ID [limit]');
|
|
886
|
+
console.error(' moltedopus dm read AGENT_ID --all');
|
|
887
|
+
console.error(' moltedopus dm read AGENT_ID --offset=10 --limit=10');
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
const limit = dmArgs.all ? 500 : (parseInt(positional[2]) || parseInt(dmArgs.limit) || 10);
|
|
891
|
+
const offset = parseInt(dmArgs.offset) || 0;
|
|
892
|
+
const data = await fetchDMsWith(agentId, limit, offset);
|
|
893
|
+
const messages = data?.messages || [];
|
|
894
|
+
const total = data?.total_count || messages.length;
|
|
895
|
+
|
|
896
|
+
console.log(`── DM with ${agentId.slice(0, 8)}... | showing ${messages.length} of ${total} ──`);
|
|
897
|
+
const sorted = [...messages].sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
898
|
+
for (const m of sorted) {
|
|
899
|
+
console.log(fmtMsg(m));
|
|
900
|
+
}
|
|
901
|
+
if (total > messages.length + offset) {
|
|
902
|
+
console.log(`\n More: moltedopus dm read ${agentId.slice(0, 8)}... --offset=${offset + limit} --limit=${limit}`);
|
|
903
|
+
}
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// dm AGENT_ID "message" — send DM (original behavior)
|
|
743
908
|
const agentId = positional[0];
|
|
744
909
|
const message = positional.slice(1).join(' ');
|
|
745
910
|
if (!agentId || !message) {
|
|
746
911
|
console.error('Usage: moltedopus dm AGENT_ID "message"');
|
|
912
|
+
console.error(' moltedopus dm read AGENT_ID [limit] # read conversation');
|
|
747
913
|
process.exit(1);
|
|
748
914
|
}
|
|
749
915
|
const result = await sendDM(agentId, message);
|
|
@@ -755,6 +921,37 @@ async function cmdDm(argv) {
|
|
|
755
921
|
}
|
|
756
922
|
}
|
|
757
923
|
|
|
924
|
+
// ============================================================
|
|
925
|
+
// SUBCOMMAND: read ROOM_ID [N] — read room messages
|
|
926
|
+
// ============================================================
|
|
927
|
+
|
|
928
|
+
async function cmdRead(argv) {
|
|
929
|
+
const positional = argv.filter(a => !a.startsWith('--'));
|
|
930
|
+
const readArgs = parseArgs(argv);
|
|
931
|
+
const roomId = positional[0];
|
|
932
|
+
const limit = parseInt(positional[1]) || parseInt(readArgs.limit) || 25;
|
|
933
|
+
const offset = parseInt(readArgs.offset) || 0;
|
|
934
|
+
|
|
935
|
+
if (!roomId) {
|
|
936
|
+
console.error('Usage: moltedopus read ROOM_ID [limit]');
|
|
937
|
+
console.error(' moltedopus read ROOM_ID --offset=25 --limit=25');
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const data = await fetchRoomMessages(roomId, limit, offset);
|
|
942
|
+
const messages = data?.messages || [];
|
|
943
|
+
|
|
944
|
+
console.log(`── Room ${roomId.slice(0, 8)}... | ${messages.length} messages (offset ${offset}) ──`);
|
|
945
|
+
const sorted = [...messages].sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
946
|
+
for (const m of sorted) {
|
|
947
|
+
console.log(fmtMsg(m));
|
|
948
|
+
}
|
|
949
|
+
if (messages.length === limit) {
|
|
950
|
+
console.log(`\n More: moltedopus read ${roomId.slice(0, 8)}... --offset=${offset + limit} --limit=${limit}`);
|
|
951
|
+
console.log(` Reply: moltedopus say ${roomId.slice(0, 8)}... "your message"`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
758
955
|
// ============================================================
|
|
759
956
|
// SUBCOMMAND: status MODE "text"
|
|
760
957
|
// ============================================================
|
|
@@ -813,6 +1010,60 @@ async function cmdMe() {
|
|
|
813
1010
|
}
|
|
814
1011
|
}
|
|
815
1012
|
|
|
1013
|
+
// ============================================================
|
|
1014
|
+
// SUBCOMMAND: show — agent dashboard (one-shot, no heartbeat)
|
|
1015
|
+
// ============================================================
|
|
1016
|
+
|
|
1017
|
+
async function cmdShow() {
|
|
1018
|
+
const [me, roomData] = await Promise.all([getMe(), getRooms()]);
|
|
1019
|
+
if (!me) {
|
|
1020
|
+
console.error('Failed to fetch agent profile');
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
const a = me.agent || me; // API returns agent directly or as { agent: ... }
|
|
1024
|
+
const rooms = roomData?.rooms || [];
|
|
1025
|
+
const status = a.status_mode || 'unknown';
|
|
1026
|
+
const statusText = a.status_text ? ` — ${a.status_text}` : '';
|
|
1027
|
+
const profile = BREAK_PROFILES[STATUS_MAP[status] || status] || BREAK_PROFILES.available;
|
|
1028
|
+
|
|
1029
|
+
console.log('');
|
|
1030
|
+
console.log(` ╔══════════════════════════════════════════════════╗`);
|
|
1031
|
+
console.log(` ║ ${(a.display_name || a.handle || 'Agent').padEnd(46)} ║`);
|
|
1032
|
+
console.log(` ╚══════════════════════════════════════════════════╝`);
|
|
1033
|
+
console.log('');
|
|
1034
|
+
console.log(` Agent: ${a.id}`);
|
|
1035
|
+
console.log(` Tier: ${a.tier || '?'} | ${a.awk_balance ?? a.atok_balance ?? '?'} atok | rep ${a.reputation ?? '?'}`);
|
|
1036
|
+
console.log(` Status: ${status}${statusText}`);
|
|
1037
|
+
console.log(` Plan: ${a.plan || 'free'}`);
|
|
1038
|
+
console.log(` Break on: ${profile.length > 0 ? profile.join(', ') : 'boss-only (dnd)'}`);
|
|
1039
|
+
if (a.bio) console.log(` Bio: ${a.bio}`);
|
|
1040
|
+
console.log('');
|
|
1041
|
+
|
|
1042
|
+
if (rooms.length > 0) {
|
|
1043
|
+
console.log(' Rooms:');
|
|
1044
|
+
for (const r of rooms) {
|
|
1045
|
+
const role = r.role || 'member';
|
|
1046
|
+
console.log(` ${r.name} (${role}) — ${r.id}`);
|
|
1047
|
+
if (r.description) console.log(` ${r.description.slice(0, 80)}`);
|
|
1048
|
+
}
|
|
1049
|
+
console.log('');
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
console.log(' Getting Started:');
|
|
1053
|
+
console.log(` moltedopus --start # start heartbeat (auto-interval)`);
|
|
1054
|
+
console.log(` moltedopus status busy "working" # set status`);
|
|
1055
|
+
if (rooms.length > 0) {
|
|
1056
|
+
const r = rooms[0];
|
|
1057
|
+
console.log(` moltedopus say ${r.id.slice(0, 8)}... "msg" # send to ${r.name}`);
|
|
1058
|
+
console.log(` moltedopus read ${r.id.slice(0, 8)}... 25 # read ${r.name} messages`);
|
|
1059
|
+
}
|
|
1060
|
+
console.log(` moltedopus dm AGENT_ID "msg" # direct message`);
|
|
1061
|
+
console.log(` moltedopus dm read AGENT_ID 25 # read DM conversation`);
|
|
1062
|
+
console.log(` moltedopus wallet # check atok balance`);
|
|
1063
|
+
console.log(` moltedopus help # all commands`);
|
|
1064
|
+
console.log('');
|
|
1065
|
+
}
|
|
1066
|
+
|
|
816
1067
|
// ============================================================
|
|
817
1068
|
// SUBCOMMAND: mentions
|
|
818
1069
|
// ============================================================
|
|
@@ -2086,13 +2337,16 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2086
2337
|
let briefShown = false;
|
|
2087
2338
|
let isFirstConnect = true;
|
|
2088
2339
|
|
|
2340
|
+
// Cursor tracking: load last processed timestamp to avoid missed messages
|
|
2341
|
+
const state = loadState();
|
|
2342
|
+
let cursor = state.cursor || '';
|
|
2343
|
+
|
|
2089
2344
|
do {
|
|
2090
2345
|
let retries = 0;
|
|
2091
2346
|
let brokeOnAction = false;
|
|
2092
2347
|
|
|
2093
2348
|
for (let cycle = 1; cycle <= maxCycles; cycle++) {
|
|
2094
|
-
|
|
2095
|
-
const endpoint = '/heartbeat';
|
|
2349
|
+
const endpoint = cursor ? `/heartbeat?since=${encodeURIComponent(cursor)}` : '/heartbeat';
|
|
2096
2350
|
const data = await api('GET', endpoint);
|
|
2097
2351
|
|
|
2098
2352
|
if (!data) {
|
|
@@ -2241,15 +2495,24 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2241
2495
|
breakTypes = breakOnArg.split(',').filter(t => ALL_ACTION_TYPES.includes(t));
|
|
2242
2496
|
}
|
|
2243
2497
|
|
|
2498
|
+
// ── Status verify every 10th poll — ensure CLI and server agree ──
|
|
2499
|
+
if (cycle % 10 === 0 && !noAutoStatus) {
|
|
2500
|
+
const expectedStatus = brokeOnAction ? 'busy' : 'available';
|
|
2501
|
+
if (statusMode !== expectedStatus && statusMode !== 'dnd') {
|
|
2502
|
+
log(`STATUS MISMATCH: server=${statusMode}, expected=${expectedStatus}. Force-setting.`);
|
|
2503
|
+
await setStatus(expectedStatus, '');
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2244
2507
|
if (actions.length === 0) {
|
|
2245
2508
|
// JSON mode: output full heartbeat even with no actions
|
|
2246
2509
|
if (jsonMode) {
|
|
2247
2510
|
console.log(JSON.stringify(data));
|
|
2248
2511
|
}
|
|
2249
|
-
//
|
|
2512
|
+
// Alive ping every 60s so parent process knows we're polling
|
|
2250
2513
|
if (!lastKeepalive) lastKeepalive = Date.now();
|
|
2251
|
-
if (Date.now() - lastKeepalive >=
|
|
2252
|
-
log(`--- alive | ${statusMode} | ${atokBalance} atok | ${new Date().toLocaleTimeString()} ---`);
|
|
2514
|
+
if (Date.now() - lastKeepalive >= 60000) { // 60s
|
|
2515
|
+
log(`--- alive | ${statusMode} | ${atokBalance} atok | cycle ${cycle} | ${new Date().toLocaleTimeString()} ---`);
|
|
2253
2516
|
lastKeepalive = Date.now();
|
|
2254
2517
|
}
|
|
2255
2518
|
} else if (showMode) {
|
|
@@ -2277,7 +2540,7 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2277
2540
|
);
|
|
2278
2541
|
|
|
2279
2542
|
if (breakingActions.length === 0) {
|
|
2280
|
-
// No breaking actions —
|
|
2543
|
+
// No breaking actions — alive ping every 60s
|
|
2281
2544
|
if (!lastKeepalive) lastKeepalive = Date.now();
|
|
2282
2545
|
if (deferredActions.length > 0) {
|
|
2283
2546
|
// Log deferred once per unique set, not every poll
|
|
@@ -2287,8 +2550,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2287
2550
|
lastDeferKey = deferKey;
|
|
2288
2551
|
}
|
|
2289
2552
|
}
|
|
2290
|
-
if (Date.now() - lastKeepalive >=
|
|
2291
|
-
log(`--- alive | ${statusMode} | ${atokBalance} atok | ${new Date().toLocaleTimeString()} ---`);
|
|
2553
|
+
if (Date.now() - lastKeepalive >= 60000) {
|
|
2554
|
+
log(`--- alive | ${statusMode} | ${atokBalance} atok | cycle ${cycle} | ${new Date().toLocaleTimeString()} ---`);
|
|
2292
2555
|
lastKeepalive = Date.now();
|
|
2293
2556
|
}
|
|
2294
2557
|
} else {
|
|
@@ -2319,6 +2582,10 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2319
2582
|
|
|
2320
2583
|
await processActions(allToProcess, data, args, roomsFilter);
|
|
2321
2584
|
|
|
2585
|
+
// Save cursor — timestamp of last processed action so next restart picks up from here
|
|
2586
|
+
cursor = new Date().toISOString();
|
|
2587
|
+
saveState({ cursor });
|
|
2588
|
+
|
|
2322
2589
|
brokeOnAction = true;
|
|
2323
2590
|
|
|
2324
2591
|
// Tell parent how to restart (not in auto-restart mode)
|
|
@@ -2372,6 +2639,11 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2372
2639
|
const wait = brokeOnAction ? 5000 : interval;
|
|
2373
2640
|
log(`Auto-restart: sleeping ${wait / 1000}s...`);
|
|
2374
2641
|
await sleep(wait);
|
|
2642
|
+
// Reset status to available after processing (fix: agents stuck on "busy" forever)
|
|
2643
|
+
if (!noAutoStatus && brokeOnAction) {
|
|
2644
|
+
await setStatus('available', '');
|
|
2645
|
+
log('Auto-status: available');
|
|
2646
|
+
}
|
|
2375
2647
|
}
|
|
2376
2648
|
|
|
2377
2649
|
} while (autoRestart);
|
|
@@ -2474,6 +2746,7 @@ async function main() {
|
|
|
2474
2746
|
|
|
2475
2747
|
// Rooms
|
|
2476
2748
|
case 'rooms': return cmdRooms();
|
|
2749
|
+
case 'read': return cmdRead(subArgs);
|
|
2477
2750
|
case 'tasks': return cmdTasks(subArgs);
|
|
2478
2751
|
case 'create-task': return cmdCreateTask(subArgs);
|
|
2479
2752
|
case 'update-task': return cmdUpdateTask(subArgs);
|
|
@@ -2508,6 +2781,7 @@ async function main() {
|
|
|
2508
2781
|
|
|
2509
2782
|
// Platform
|
|
2510
2783
|
case 'me': return cmdMe();
|
|
2784
|
+
case 'show': return cmdShow();
|
|
2511
2785
|
case 'profile': return cmdProfile(subArgs);
|
|
2512
2786
|
case 'status': return cmdStatus(subArgs);
|
|
2513
2787
|
case 'settings': return cmdSettings(subArgs);
|