feat: add heartbeat idle override and preserve session freshness

This commit is contained in:
Peter Steinberger
2025-11-26 17:26:17 +01:00
parent e6c78df975
commit 135d930c99
2 changed files with 22 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ export type SessionConfig = {
scope?: SessionScope;
resetTriggers?: string[];
idleMinutes?: number;
heartbeatIdleMinutes?: number;
store?: string;
sessionArgNew?: string[];
sessionArgResume?: string[];
@@ -89,6 +90,7 @@ const ReplySchema = z
.optional(),
resetTriggers: z.array(z.string()).optional(),
idleMinutes: z.number().int().positive().optional(),
heartbeatIdleMinutes: z.number().int().positive().optional(),
store: z.string().optional(),
sessionArgNew: z.array(z.string()).optional(),
sessionArgResume: z.array(z.string()).optional(),

View File

@@ -7,6 +7,7 @@ import {
deriveSessionKey,
loadSessionStore,
resolveStorePath,
saveSessionStore,
} from "../config/sessions.js";
import { danger, isVerbose, logVerbose, success } from "../globals.js";
import { logInfo } from "../logger.js";
@@ -92,7 +93,7 @@ export async function runWebHeartbeatOnce(opts: {
});
const cfg = loadConfig();
const sessionSnapshot = getSessionSnapshot(cfg, to);
const sessionSnapshot = getSessionSnapshot(cfg, to, true);
if (verbose) {
heartbeatLogger.info(
{
@@ -112,7 +113,7 @@ export async function runWebHeartbeatOnce(opts: {
Body: HEARTBEAT_PROMPT,
From: to,
To: to,
MessageSid: undefined,
MessageSid: sessionSnapshot.entry?.sessionId,
},
undefined,
cfg,
@@ -139,6 +140,15 @@ export async function runWebHeartbeatOnce(opts: {
(replyResult.mediaUrl ?? replyResult.mediaUrls?.length ?? 0) > 0;
const stripped = stripHeartbeatToken(replyResult.text);
if (stripped.shouldSkip && !hasMedia) {
// Don't let heartbeats keep sessions alive: restore previous updatedAt so idle expiry still works.
const sessionCfg = cfg.inbound?.reply?.session;
const storePath = resolveStorePath(sessionCfg?.store);
const store = loadSessionStore(storePath);
if (sessionSnapshot.entry && store[sessionSnapshot.key]) {
store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt;
await saveSessionStore(storePath, store);
}
heartbeatLogger.info(
{ to, reason: "heartbeat-token", rawLength: replyResult.text?.length },
"heartbeat skipped",
@@ -185,14 +195,20 @@ function getFallbackRecipient(cfg: ReturnType<typeof loadConfig>) {
return mostRecent ? normalizeE164(mostRecent[0]) : null;
}
function getSessionSnapshot(cfg: ReturnType<typeof loadConfig>, from: string) {
function getSessionSnapshot(
cfg: ReturnType<typeof loadConfig>,
from: string,
isHeartbeat = false,
) {
const sessionCfg = cfg.inbound?.reply?.session;
const scope = sessionCfg?.scope ?? "per-sender";
const key = deriveSessionKey(scope, { From: from, To: "", Body: "" });
const store = loadSessionStore(resolveStorePath(sessionCfg?.store));
const entry = store[key];
const idleMinutes = Math.max(
sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES,
(isHeartbeat
? (sessionCfg?.heartbeatIdleMinutes ?? sessionCfg?.idleMinutes)
: sessionCfg?.idleMinutes) ?? DEFAULT_IDLE_MINUTES,
1,
);
const fresh = !!(