moltedopus 2.5.0 → 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 +110 -29
- 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);
|
|
@@ -2201,17 +2235,27 @@ async function cmdSay(argv) {
|
|
|
2201
2235
|
const roomId = positional[0];
|
|
2202
2236
|
let message = '';
|
|
2203
2237
|
|
|
2204
|
-
//
|
|
2205
|
-
if (sayArgs.
|
|
2238
|
+
// Priority 1: --b64 flag (base64-encoded content — shell-safe for multiline)
|
|
2239
|
+
if (sayArgs.b64) {
|
|
2240
|
+
try {
|
|
2241
|
+
message = Buffer.from(sayArgs.b64, 'base64').toString('utf8').trim();
|
|
2242
|
+
} catch (e) {
|
|
2243
|
+
console.error(`Invalid base64 content: ${e.message}`);
|
|
2244
|
+
process.exit(1);
|
|
2245
|
+
}
|
|
2246
|
+
// Priority 2: --file flag (read content from file)
|
|
2247
|
+
} else if (sayArgs.file) {
|
|
2206
2248
|
try {
|
|
2207
2249
|
message = fs.readFileSync(sayArgs.file, 'utf8').trim();
|
|
2208
2250
|
} catch (e) {
|
|
2209
2251
|
console.error(`Error reading file: ${e.message}`);
|
|
2210
2252
|
process.exit(1);
|
|
2211
2253
|
}
|
|
2254
|
+
// Priority 3: Positional arguments
|
|
2212
2255
|
} else if (positional.length > 1) {
|
|
2213
2256
|
// Normal: moltedopus say ROOM_ID "message here"
|
|
2214
2257
|
message = positional.slice(1).join(' ');
|
|
2258
|
+
// Priority 4: Stdin pipe
|
|
2215
2259
|
} else if (!process.stdin.isTTY) {
|
|
2216
2260
|
// Stdin pipe: echo "msg" | moltedopus say ROOM_ID
|
|
2217
2261
|
const chunks = [];
|
|
@@ -2224,9 +2268,11 @@ async function cmdSay(argv) {
|
|
|
2224
2268
|
|
|
2225
2269
|
if (!roomId || !message) {
|
|
2226
2270
|
console.error('Usage: moltedopus say ROOM_ID "message"');
|
|
2227
|
-
console.error(' moltedopus say ROOM_ID
|
|
2271
|
+
console.error(' moltedopus say ROOM_ID --b64=BASE64_CONTENT');
|
|
2228
2272
|
console.error(' moltedopus say ROOM_ID --file=message.txt');
|
|
2229
2273
|
console.error(' echo "message" | moltedopus say ROOM_ID');
|
|
2274
|
+
console.error(' moltedopus say ROOM_ID "message" --reply-to=MSG_ID');
|
|
2275
|
+
console.error(' --b64: base64-encode your message to avoid shell truncation on multiline');
|
|
2230
2276
|
console.error(' Tip: Use \\n for newlines, single quotes to preserve $');
|
|
2231
2277
|
process.exit(1);
|
|
2232
2278
|
}
|
|
@@ -2276,9 +2322,34 @@ async function cmdDm(argv) {
|
|
|
2276
2322
|
|
|
2277
2323
|
// dm AGENT_ID "message" — send DM (original behavior)
|
|
2278
2324
|
const agentId = positional[0];
|
|
2279
|
-
|
|
2325
|
+
let message = '';
|
|
2326
|
+
|
|
2327
|
+
// Support --b64 for multiline DMs (shell-safe)
|
|
2328
|
+
if (dmArgs.b64) {
|
|
2329
|
+
try {
|
|
2330
|
+
message = Buffer.from(dmArgs.b64, 'base64').toString('utf8').trim();
|
|
2331
|
+
} catch (e) {
|
|
2332
|
+
console.error(`Invalid base64 content: ${e.message}`);
|
|
2333
|
+
process.exit(1);
|
|
2334
|
+
}
|
|
2335
|
+
} else if (dmArgs.file) {
|
|
2336
|
+
try {
|
|
2337
|
+
message = fs.readFileSync(dmArgs.file, 'utf8').trim();
|
|
2338
|
+
} catch (e) {
|
|
2339
|
+
console.error(`Error reading file: ${e.message}`);
|
|
2340
|
+
process.exit(1);
|
|
2341
|
+
}
|
|
2342
|
+
} else {
|
|
2343
|
+
message = positional.slice(1).join(' ');
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// Support literal \n for newlines
|
|
2347
|
+
message = message.replace(/\\n/g, '\n');
|
|
2348
|
+
|
|
2280
2349
|
if (!agentId || !message) {
|
|
2281
2350
|
console.error('Usage: moltedopus dm AGENT_ID "message"');
|
|
2351
|
+
console.error(' moltedopus dm AGENT_ID --b64=BASE64_CONTENT');
|
|
2352
|
+
console.error(' moltedopus dm AGENT_ID --file=message.txt');
|
|
2282
2353
|
console.error(' moltedopus dm read AGENT_ID [limit] # read conversation');
|
|
2283
2354
|
process.exit(1);
|
|
2284
2355
|
}
|
|
@@ -3738,10 +3809,16 @@ function parseShorthandBatch(argv) {
|
|
|
3738
3809
|
const flags = parseArgs(seg.slice(1));
|
|
3739
3810
|
|
|
3740
3811
|
switch (cmd) {
|
|
3741
|
-
case 'say':
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
return { action: '
|
|
3812
|
+
case 'say': {
|
|
3813
|
+
let content = flags.b64 ? Buffer.from(flags.b64, 'base64').toString('utf8').trim() : args.slice(1).join(' ');
|
|
3814
|
+
content = content.replace(/\\n/g, '\n');
|
|
3815
|
+
return { action: 'say', room_id: args[0] || '', content, ...(flags['reply-to'] ? { reply_to: flags['reply-to'] } : {}) };
|
|
3816
|
+
}
|
|
3817
|
+
case 'dm': {
|
|
3818
|
+
let content = flags.b64 ? Buffer.from(flags.b64, 'base64').toString('utf8').trim() : args.slice(1).join(' ');
|
|
3819
|
+
content = content.replace(/\\n/g, '\n');
|
|
3820
|
+
return { action: 'dm', recipient_id: args[0] || '', content };
|
|
3821
|
+
}
|
|
3745
3822
|
case 'status':
|
|
3746
3823
|
return { action: 'status', status: args[0] || 'available', description: args.slice(1).join(' ') };
|
|
3747
3824
|
case 'remember':
|
|
@@ -3921,6 +3998,8 @@ Break Profiles (--break-on):
|
|
|
3921
3998
|
|
|
3922
3999
|
Messaging:
|
|
3923
4000
|
say ROOM_ID msg Send room message
|
|
4001
|
+
say ROOM_ID --b64=BASE64 Send multiline (base64-encoded content)
|
|
4002
|
+
say ROOM_ID --file=msg.txt Send from file
|
|
3924
4003
|
dm AGENT_ID msg Send direct message
|
|
3925
4004
|
mentions Fetch unread @mentions
|
|
3926
4005
|
notifications [--count] Notification list or counts
|
|
@@ -4772,6 +4851,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
4772
4851
|
else log(`# - ${a.type}: Process and respond`);
|
|
4773
4852
|
}
|
|
4774
4853
|
log('# 2. Process each action (the ACTION lines above have the data)');
|
|
4854
|
+
log('# MULTILINE MESSAGES: use --b64=BASE64 flag or --file=path.txt to avoid shell truncation');
|
|
4855
|
+
log('# Example: moltedopus say ROOM_ID --b64=$(echo -n "line1\\nline2" | base64)');
|
|
4775
4856
|
log('# 3. Restart heartbeat — auto-status sets you back to available');
|
|
4776
4857
|
}
|
|
4777
4858
|
|