Fix heartbeat skip logic to only check user messages (#43)
- Previously heartbeats were skipped if agent had ANY recent activity (Gmail polling, cron jobs, other heartbeats, etc.) - Now only skips if user sent a message in the last 5 minutes - Added getLastUserMessageTime() to LettaBot to track user messages - Manual /heartbeat command bypasses the skip check - Discord /heartbeat now replies with confirmation (Telegram stays silent) 🐙 Generated with [Letta Code](https://letta.com) Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -178,7 +178,10 @@ Ask the bot owner to approve with:
|
||||
return;
|
||||
}
|
||||
if (command === 'heartbeat') {
|
||||
await this.onCommand('heartbeat');
|
||||
const result = await this.onCommand('heartbeat');
|
||||
if (result) {
|
||||
await message.channel.send(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +138,10 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle /heartbeat (silent - no reply)
|
||||
// Handle /heartbeat - trigger heartbeat manually (silent - no reply)
|
||||
this.bot.command('heartbeat', async (ctx) => {
|
||||
if (this.onCommand) {
|
||||
await this.onCommand('heartbeat');
|
||||
// No reply - heartbeat runs silently
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export class LettaBot {
|
||||
private config: BotConfig;
|
||||
private channels: Map<string, ChannelAdapter> = new Map();
|
||||
private messageQueue: Array<{ msg: InboundMessage; adapter: ChannelAdapter }> = [];
|
||||
private lastUserMessageTime: Date | null = null;
|
||||
|
||||
// Callback to trigger heartbeat (set by main.ts)
|
||||
public onTriggerHeartbeat?: () => Promise<void>;
|
||||
@@ -151,6 +152,8 @@ export class LettaBot {
|
||||
* Process a single message
|
||||
*/
|
||||
private async processMessage(msg: InboundMessage, adapter: ChannelAdapter): Promise<void> {
|
||||
// Track when user last sent a message (for heartbeat skip logic)
|
||||
this.lastUserMessageTime = new Date();
|
||||
|
||||
// Track last message target for heartbeat delivery
|
||||
this.store.lastMessageTarget = {
|
||||
@@ -436,4 +439,11 @@ export class LettaBot {
|
||||
getLastMessageTarget(): { channel: string; chatId: string } | null {
|
||||
return this.store.lastMessageTarget || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time of the last user message (for heartbeat skip logic)
|
||||
*/
|
||||
getLastUserMessageTime(): Date | null {
|
||||
return this.lastUserMessageTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { resolve } from 'node:path';
|
||||
import type { LettaBot } from '../core/bot.js';
|
||||
import type { TriggerContext } from '../core/types.js';
|
||||
import { buildHeartbeatPrompt } from '../core/prompts.js';
|
||||
import { getLastRunTime } from '../tools/letta-api.js';
|
||||
|
||||
|
||||
// Log file
|
||||
const LOG_PATH = resolve(process.cwd(), 'cron-log.jsonl');
|
||||
@@ -106,11 +106,11 @@ export class HeartbeatService {
|
||||
|
||||
/**
|
||||
* Manually trigger a heartbeat (for /heartbeat command)
|
||||
* Bypasses the "recently active" check since user explicitly requested it
|
||||
* Bypasses the "recently messaged" check since user explicitly requested it
|
||||
*/
|
||||
async trigger(): Promise<void> {
|
||||
console.log('[Heartbeat] Manual trigger requested');
|
||||
await this.runHeartbeat(true); // skipActiveCheck = true
|
||||
await this.runHeartbeat(true); // skipRecentCheck = true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,9 +119,9 @@ export class HeartbeatService {
|
||||
* SILENT MODE: Agent's text output is NOT auto-delivered.
|
||||
* The agent must use `lettabot-message` CLI via Bash to contact the user.
|
||||
*
|
||||
* @param skipActiveCheck - If true, bypass the "recently active" check (for manual triggers)
|
||||
* @param skipRecentCheck - If true, bypass the "recently messaged" check (for manual triggers)
|
||||
*/
|
||||
private async runHeartbeat(skipActiveCheck = false): Promise<void> {
|
||||
private async runHeartbeat(skipRecentCheck = false): Promise<void> {
|
||||
const now = new Date();
|
||||
const formattedTime = now.toLocaleString();
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
@@ -130,25 +130,21 @@ export class HeartbeatService {
|
||||
console.log(`[Heartbeat] ⏰ RUNNING at ${formattedTime} [SILENT MODE]`);
|
||||
console.log(`${'='.repeat(60)}\n`);
|
||||
|
||||
// Check if agent was active recently (skip heartbeat if so)
|
||||
// Skip this check for manual triggers (/heartbeat command)
|
||||
if (!skipActiveCheck) {
|
||||
const agentId = this.bot.getStatus().agentId;
|
||||
if (agentId) {
|
||||
const lastRunTime = await getLastRunTime(agentId);
|
||||
if (lastRunTime) {
|
||||
const msSinceLastRun = now.getTime() - lastRunTime.getTime();
|
||||
const intervalMs = this.config.intervalMinutes * 60 * 1000;
|
||||
|
||||
if (msSinceLastRun < intervalMs) {
|
||||
const minutesAgo = Math.round(msSinceLastRun / 60000);
|
||||
console.log(`[Heartbeat] Agent was active ${minutesAgo}m ago - skipping heartbeat`);
|
||||
logEvent('heartbeat_skipped_active', {
|
||||
lastRunTime: lastRunTime.toISOString(),
|
||||
minutesAgo,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Skip if user sent a message in the last 5 minutes (unless manual trigger)
|
||||
if (!skipRecentCheck) {
|
||||
const lastUserMessage = this.bot.getLastUserMessageTime();
|
||||
if (lastUserMessage) {
|
||||
const msSinceLastMessage = now.getTime() - lastUserMessage.getTime();
|
||||
const skipWindowMs = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
if (msSinceLastMessage < skipWindowMs) {
|
||||
const minutesAgo = Math.round(msSinceLastMessage / 60000);
|
||||
console.log(`[Heartbeat] User messaged ${minutesAgo}m ago - skipping heartbeat`);
|
||||
logEvent('heartbeat_skipped_recent_user', {
|
||||
lastUserMessage: lastUserMessage.toISOString(),
|
||||
minutesAgo,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user