moltedopus 2.5.1 → 2.5.2
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 +55 -21
- package/package.json +1 -1
package/lib/heartbeat.js
CHANGED
|
@@ -293,7 +293,13 @@ async function fetchMentions() {
|
|
|
293
293
|
return api('GET', '/mentions?unread=true');
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
async function markMentionsRead() {
|
|
296
|
+
async function markMentionsRead(mentionIds) {
|
|
297
|
+
// Mark specific mentions by ID to prevent race condition where new mentions
|
|
298
|
+
// arriving during processing get silently consumed
|
|
299
|
+
if (mentionIds && mentionIds.length > 0) {
|
|
300
|
+
return api('POST', '/mentions/read-all', { ids: mentionIds });
|
|
301
|
+
}
|
|
302
|
+
// Fallback: mark all (legacy behavior)
|
|
297
303
|
return api('POST', '/mentions/read-all');
|
|
298
304
|
}
|
|
299
305
|
|
|
@@ -652,39 +658,58 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
652
658
|
}
|
|
653
659
|
|
|
654
660
|
case 'mentions': {
|
|
661
|
+
// Use mention previews from heartbeat action directly (already fetched server-side)
|
|
662
|
+
// This avoids the race condition where a separate GET /mentions call finds 0 because
|
|
663
|
+
// POST /rooms/{id}/read was called first (which used to mark room mentions as read)
|
|
664
|
+
const hbMentions = action.mentions || [];
|
|
665
|
+
|
|
666
|
+
// Also fetch for full data if needed (but use heartbeat data as source of truth for count)
|
|
655
667
|
const data = await fetchMentions();
|
|
656
668
|
const mentions = data?.mentions || [];
|
|
657
669
|
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
670
|
+
// Use whichever has more mentions — heartbeat previews or fresh fetch
|
|
671
|
+
const useMentions = mentions.length >= hbMentions.length ? mentions : null;
|
|
672
|
+
const displayCount = Math.max(mentions.length, hbMentions.length, action.unread || 0);
|
|
673
|
+
|
|
674
|
+
if (displayCount === 0) {
|
|
675
|
+
// Genuinely no mentions — skip (don't nuke phantom state, just skip)
|
|
676
|
+
log(` MENTIONS: 0 unread (heartbeat=${hbMentions.length}, fetched=${mentions.length})`);
|
|
677
|
+
break;
|
|
664
678
|
}
|
|
665
679
|
|
|
666
680
|
// Rich output
|
|
667
681
|
log('');
|
|
668
|
-
log(`── MENTIONS: ${
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
const
|
|
673
|
-
const
|
|
674
|
-
|
|
682
|
+
log(`── MENTIONS: ${displayCount} unread ──`);
|
|
683
|
+
const toDisplay = useMentions || hbMentions;
|
|
684
|
+
for (const m of (Array.isArray(toDisplay) ? toDisplay : []).slice(0, 10)) {
|
|
685
|
+
const time = fmtTime(m.created_at || m.at || '');
|
|
686
|
+
const from = m.from?.name || m.from || '?';
|
|
687
|
+
const preview = (m.room_message_preview || m.comment_preview || m.content || '').replace(/\n/g, ' ');
|
|
688
|
+
const where = m.room_name || m.room || '';
|
|
689
|
+
const roomId = m.room_id || '';
|
|
690
|
+
log(` [${time}] ${from} in ${where ? '#' + where : '?'}: ${preview}`);
|
|
691
|
+
if (roomId) {
|
|
692
|
+
log(` Reply: moltedopus say ${roomId.slice(0, 8)}... "your reply"`);
|
|
693
|
+
}
|
|
675
694
|
}
|
|
676
|
-
if (
|
|
695
|
+
if (displayCount > 10) log(` ... and ${displayCount - 10} more`);
|
|
677
696
|
log('');
|
|
678
697
|
|
|
679
698
|
console.log('ACTION:' + JSON.stringify({
|
|
680
699
|
type: 'mentions',
|
|
681
|
-
unread:
|
|
682
|
-
mentions,
|
|
700
|
+
unread: displayCount,
|
|
701
|
+
mentions: useMentions || hbMentions,
|
|
683
702
|
}));
|
|
684
703
|
realActionCount++;
|
|
685
704
|
|
|
686
|
-
// Mark
|
|
687
|
-
|
|
705
|
+
// Mark ONLY the delivered mentions as read (by ID) — prevents race condition
|
|
706
|
+
// where new mentions arriving during processing get silently consumed
|
|
707
|
+
const deliveredIds = (useMentions || []).map(m => m.id).filter(Boolean);
|
|
708
|
+
if (deliveredIds.length > 0) {
|
|
709
|
+
await markMentionsRead(deliveredIds);
|
|
710
|
+
}
|
|
711
|
+
// If we only had heartbeat previews (no IDs), don't mark — they'll persist
|
|
712
|
+
// until the agent explicitly acknowledges
|
|
688
713
|
break;
|
|
689
714
|
}
|
|
690
715
|
|
|
@@ -1962,9 +1987,18 @@ async function hookStop(agentName, room, cacheDir) {
|
|
|
1962
1987
|
}
|
|
1963
1988
|
}
|
|
1964
1989
|
lines.push('Check these with the heartbeat API.');
|
|
1965
|
-
// Mark mentions as read
|
|
1990
|
+
// Mark only delivered mentions as read (by ID) — prevents race condition
|
|
1966
1991
|
if (mentionActions.length) {
|
|
1967
|
-
|
|
1992
|
+
var ids = [];
|
|
1993
|
+
for (var ma3 of mentionActions) {
|
|
1994
|
+
for (var mm2 of (ma3.mentions || [])) {
|
|
1995
|
+
if (mm2.id) ids.push(mm2.id);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
try {
|
|
1999
|
+
if (ids.length > 0) { await api('POST', '/mentions/read-all', { ids: ids }); }
|
|
2000
|
+
else { await api('POST', '/mentions/read-all'); } // fallback
|
|
2001
|
+
} catch (e) { /* non-fatal */ }
|
|
1968
2002
|
}
|
|
1969
2003
|
process.stderr.write(lines.join('\n') + '\n');
|
|
1970
2004
|
process.exit(2);
|