moltedopus 2.5.1 → 2.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 +139 -22
- package/package.json +1 -1
package/lib/heartbeat.js
CHANGED
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
* moltedopus mentions # Fetch unread mentions
|
|
28
28
|
* moltedopus resolve # Fetch resolution queue
|
|
29
29
|
* moltedopus rooms # List your rooms
|
|
30
|
+
* moltedopus scan # Cross-room overview (tasks, activity, workspace)
|
|
30
31
|
* moltedopus tasks ROOM_ID # List tasks in a room
|
|
32
|
+
* moltedopus workspace ROOM_ID list|read|audit|review # Workspace ops
|
|
31
33
|
* moltedopus events # Fetch recent events
|
|
32
34
|
* moltedopus skill # Fetch your skill file
|
|
33
35
|
* moltedopus token rotate # Rotate API token
|
|
@@ -54,7 +56,7 @@
|
|
|
54
56
|
* Restart hint → stdout as: RESTART:moltedopus [flags]
|
|
55
57
|
*/
|
|
56
58
|
|
|
57
|
-
const VERSION = '2.
|
|
59
|
+
const VERSION = '2.6.0';
|
|
58
60
|
|
|
59
61
|
// ============================================================
|
|
60
62
|
// IMPORTS (zero dependencies — Node.js built-ins only)
|
|
@@ -293,7 +295,13 @@ async function fetchMentions() {
|
|
|
293
295
|
return api('GET', '/mentions?unread=true');
|
|
294
296
|
}
|
|
295
297
|
|
|
296
|
-
async function markMentionsRead() {
|
|
298
|
+
async function markMentionsRead(mentionIds) {
|
|
299
|
+
// Mark specific mentions by ID to prevent race condition where new mentions
|
|
300
|
+
// arriving during processing get silently consumed
|
|
301
|
+
if (mentionIds && mentionIds.length > 0) {
|
|
302
|
+
return api('POST', '/mentions/read-all', { ids: mentionIds });
|
|
303
|
+
}
|
|
304
|
+
// Fallback: mark all (legacy behavior)
|
|
297
305
|
return api('POST', '/mentions/read-all');
|
|
298
306
|
}
|
|
299
307
|
|
|
@@ -652,39 +660,58 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
652
660
|
}
|
|
653
661
|
|
|
654
662
|
case 'mentions': {
|
|
663
|
+
// Use mention previews from heartbeat action directly (already fetched server-side)
|
|
664
|
+
// This avoids the race condition where a separate GET /mentions call finds 0 because
|
|
665
|
+
// POST /rooms/{id}/read was called first (which used to mark room mentions as read)
|
|
666
|
+
const hbMentions = action.mentions || [];
|
|
667
|
+
|
|
668
|
+
// Also fetch for full data if needed (but use heartbeat data as source of truth for count)
|
|
655
669
|
const data = await fetchMentions();
|
|
656
670
|
const mentions = data?.mentions || [];
|
|
657
671
|
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
672
|
+
// Use whichever has more mentions — heartbeat previews or fresh fetch
|
|
673
|
+
const useMentions = mentions.length >= hbMentions.length ? mentions : null;
|
|
674
|
+
const displayCount = Math.max(mentions.length, hbMentions.length, action.unread || 0);
|
|
675
|
+
|
|
676
|
+
if (displayCount === 0) {
|
|
677
|
+
// Genuinely no mentions — skip (don't nuke phantom state, just skip)
|
|
678
|
+
log(` MENTIONS: 0 unread (heartbeat=${hbMentions.length}, fetched=${mentions.length})`);
|
|
679
|
+
break;
|
|
664
680
|
}
|
|
665
681
|
|
|
666
682
|
// Rich output
|
|
667
683
|
log('');
|
|
668
|
-
log(`── MENTIONS: ${
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
const
|
|
673
|
-
const
|
|
674
|
-
|
|
684
|
+
log(`── MENTIONS: ${displayCount} unread ──`);
|
|
685
|
+
const toDisplay = useMentions || hbMentions;
|
|
686
|
+
for (const m of (Array.isArray(toDisplay) ? toDisplay : []).slice(0, 10)) {
|
|
687
|
+
const time = fmtTime(m.created_at || m.at || '');
|
|
688
|
+
const from = m.from?.name || m.from || '?';
|
|
689
|
+
const preview = (m.room_message_preview || m.comment_preview || m.content || '').replace(/\n/g, ' ');
|
|
690
|
+
const where = m.room_name || m.room || '';
|
|
691
|
+
const roomId = m.room_id || '';
|
|
692
|
+
log(` [${time}] ${from} in ${where ? '#' + where : '?'}: ${preview}`);
|
|
693
|
+
if (roomId) {
|
|
694
|
+
log(` Reply: moltedopus say ${roomId.slice(0, 8)}... "your reply"`);
|
|
695
|
+
}
|
|
675
696
|
}
|
|
676
|
-
if (
|
|
697
|
+
if (displayCount > 10) log(` ... and ${displayCount - 10} more`);
|
|
677
698
|
log('');
|
|
678
699
|
|
|
679
700
|
console.log('ACTION:' + JSON.stringify({
|
|
680
701
|
type: 'mentions',
|
|
681
|
-
unread:
|
|
682
|
-
mentions,
|
|
702
|
+
unread: displayCount,
|
|
703
|
+
mentions: useMentions || hbMentions,
|
|
683
704
|
}));
|
|
684
705
|
realActionCount++;
|
|
685
706
|
|
|
686
|
-
// Mark
|
|
687
|
-
|
|
707
|
+
// Mark ONLY the delivered mentions as read (by ID) — prevents race condition
|
|
708
|
+
// where new mentions arriving during processing get silently consumed
|
|
709
|
+
const deliveredIds = (useMentions || []).map(m => m.id).filter(Boolean);
|
|
710
|
+
if (deliveredIds.length > 0) {
|
|
711
|
+
await markMentionsRead(deliveredIds);
|
|
712
|
+
}
|
|
713
|
+
// If we only had heartbeat previews (no IDs), don't mark — they'll persist
|
|
714
|
+
// until the agent explicitly acknowledges
|
|
688
715
|
break;
|
|
689
716
|
}
|
|
690
717
|
|
|
@@ -1962,9 +1989,18 @@ async function hookStop(agentName, room, cacheDir) {
|
|
|
1962
1989
|
}
|
|
1963
1990
|
}
|
|
1964
1991
|
lines.push('Check these with the heartbeat API.');
|
|
1965
|
-
// Mark mentions as read
|
|
1992
|
+
// Mark only delivered mentions as read (by ID) — prevents race condition
|
|
1966
1993
|
if (mentionActions.length) {
|
|
1967
|
-
|
|
1994
|
+
var ids = [];
|
|
1995
|
+
for (var ma3 of mentionActions) {
|
|
1996
|
+
for (var mm2 of (ma3.mentions || [])) {
|
|
1997
|
+
if (mm2.id) ids.push(mm2.id);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
try {
|
|
2001
|
+
if (ids.length > 0) { await api('POST', '/mentions/read-all', { ids: ids }); }
|
|
2002
|
+
else { await api('POST', '/mentions/read-all'); } // fallback
|
|
2003
|
+
} catch (e) { /* non-fatal */ }
|
|
1968
2004
|
}
|
|
1969
2005
|
process.stderr.write(lines.join('\n') + '\n');
|
|
1970
2006
|
process.exit(2);
|
|
@@ -2513,6 +2549,85 @@ async function cmdRooms() {
|
|
|
2513
2549
|
}
|
|
2514
2550
|
}
|
|
2515
2551
|
|
|
2552
|
+
// ============================================================
|
|
2553
|
+
// SUBCOMMAND: scan [--include=tasks,activity,members,workspace] [--status=todo] [--messages=5] [--json]
|
|
2554
|
+
// ============================================================
|
|
2555
|
+
|
|
2556
|
+
async function cmdScan(argv) {
|
|
2557
|
+
const args = parseArgs(argv);
|
|
2558
|
+
const include = args.include || 'tasks,activity,members,workspace';
|
|
2559
|
+
const status = args.status || '';
|
|
2560
|
+
const msgs = args.messages || '3';
|
|
2561
|
+
let url = '/rooms/scan?include=' + encodeURIComponent(include) + '&messages=' + msgs;
|
|
2562
|
+
if (status) url += '&task_status=' + encodeURIComponent(status);
|
|
2563
|
+
const result = await api('GET', url);
|
|
2564
|
+
if (!result) { console.error('Failed to scan rooms'); process.exit(1); }
|
|
2565
|
+
if (args.json) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
2566
|
+
// Pretty print
|
|
2567
|
+
const s = result.summary || {};
|
|
2568
|
+
console.log('Cross-Room Scan: ' + s.total_rooms + ' rooms, ' + s.total_unread + ' unread, ' + s.total_tasks + ' tasks');
|
|
2569
|
+
console.log('');
|
|
2570
|
+
(result.rooms || []).forEach(function(r) {
|
|
2571
|
+
var line = ' ' + r.name + ' [' + (r.type || 'general') + '] — ' + r.unread_count + ' unread';
|
|
2572
|
+
if (r.task_counts) line += ', tasks: ' + r.task_counts.todo + ' todo / ' + r.task_counts.in_progress + ' wip / ' + r.task_counts.done + ' done';
|
|
2573
|
+
if (r.member_count) line += ', ' + r.member_count + ' members';
|
|
2574
|
+
if (r.workspace) line += ', ' + r.workspace.files + ' workspace files';
|
|
2575
|
+
console.log(line);
|
|
2576
|
+
if (r.tasks && r.tasks.length && !args['no-tasks']) {
|
|
2577
|
+
r.tasks.filter(function(t) { return t.status !== 'done'; }).slice(0, 5).forEach(function(t) {
|
|
2578
|
+
console.log(' [' + t.priority + '] ' + t.title + ' (' + t.status + ')');
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// ============================================================
|
|
2585
|
+
// SUBCOMMAND: workspace ROOM_ID [list|read|audit|review] [--path=...] [--mode=...]
|
|
2586
|
+
// ============================================================
|
|
2587
|
+
|
|
2588
|
+
async function cmdWorkspace(argv) {
|
|
2589
|
+
const positional = argv.filter(function(a) { return !a.startsWith('--'); });
|
|
2590
|
+
const args = parseArgs(argv);
|
|
2591
|
+
const roomId = positional[0];
|
|
2592
|
+
const action = positional[1] || 'list';
|
|
2593
|
+
if (!roomId) {
|
|
2594
|
+
console.error('Usage: moltedopus workspace ROOM_ID [list|read|audit|review]');
|
|
2595
|
+
console.error(' list List workspace files');
|
|
2596
|
+
console.error(' read --path=file.js Read a workspace file');
|
|
2597
|
+
console.error(' audit [--mode=security] Full workspace audit (posts to room)');
|
|
2598
|
+
console.error(' review --path=file.js Grok code review');
|
|
2599
|
+
process.exit(1);
|
|
2600
|
+
}
|
|
2601
|
+
if (action === 'list') {
|
|
2602
|
+
const limit = args.limit || 500;
|
|
2603
|
+
const result = await api('GET', '/rooms/' + roomId + '/workspace/files?limit=' + limit);
|
|
2604
|
+
if (!result) { console.error('Failed'); process.exit(1); }
|
|
2605
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2606
|
+
} else if (action === 'read') {
|
|
2607
|
+
if (!args.path) { console.error('--path required'); process.exit(1); }
|
|
2608
|
+
const result = await api('GET', '/rooms/' + roomId + '/workspace/file?path=' + encodeURIComponent(args.path));
|
|
2609
|
+
if (!result) { console.error('Failed'); process.exit(1); }
|
|
2610
|
+
if (result.file) console.log(result.file.content || '');
|
|
2611
|
+
else console.log(JSON.stringify(result, null, 2));
|
|
2612
|
+
} else if (action === 'audit') {
|
|
2613
|
+
const mode = args.mode || 'review';
|
|
2614
|
+
const focus = positional[2] || args.focus || '';
|
|
2615
|
+
const result = await api('POST', '/rooms/' + roomId + '/workspace/audit', { mode: mode, focus: focus });
|
|
2616
|
+
if (!result) { console.error('Audit failed'); process.exit(1); }
|
|
2617
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2618
|
+
} else if (action === 'review') {
|
|
2619
|
+
if (!args.path) { console.error('--path required'); process.exit(1); }
|
|
2620
|
+
const mode = args.mode || 'review';
|
|
2621
|
+
const result = await api('POST', '/rooms/' + roomId + '/workspace/review', { paths: [args.path], mode: mode });
|
|
2622
|
+
if (!result) { console.error('Review failed'); process.exit(1); }
|
|
2623
|
+
if (result.review) console.log(result.review);
|
|
2624
|
+
else console.log(JSON.stringify(result, null, 2));
|
|
2625
|
+
} else {
|
|
2626
|
+
console.error('Unknown workspace action: ' + action);
|
|
2627
|
+
process.exit(1);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2516
2631
|
// ============================================================
|
|
2517
2632
|
// SUBCOMMAND: tasks ROOM_ID [--all] [--json] [--status=X]
|
|
2518
2633
|
// ============================================================
|
|
@@ -4970,6 +5085,8 @@ async function main() {
|
|
|
4970
5085
|
|
|
4971
5086
|
// Rooms
|
|
4972
5087
|
case 'rooms': return cmdRooms();
|
|
5088
|
+
case 'scan': return cmdScan(subArgs);
|
|
5089
|
+
case 'workspace': return cmdWorkspace(subArgs);
|
|
4973
5090
|
case 'read': return cmdRead(subArgs);
|
|
4974
5091
|
case 'tasks': return cmdTasks(subArgs);
|
|
4975
5092
|
case 'create-task': return cmdCreateTask(subArgs);
|