feat: add heartbeat idle override and preserve session freshness
This commit is contained in:
@@ -13,6 +13,7 @@ export type SessionConfig = {
|
|||||||
scope?: SessionScope;
|
scope?: SessionScope;
|
||||||
resetTriggers?: string[];
|
resetTriggers?: string[];
|
||||||
idleMinutes?: number;
|
idleMinutes?: number;
|
||||||
|
heartbeatIdleMinutes?: number;
|
||||||
store?: string;
|
store?: string;
|
||||||
sessionArgNew?: string[];
|
sessionArgNew?: string[];
|
||||||
sessionArgResume?: string[];
|
sessionArgResume?: string[];
|
||||||
@@ -89,6 +90,7 @@ const ReplySchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
resetTriggers: z.array(z.string()).optional(),
|
resetTriggers: z.array(z.string()).optional(),
|
||||||
idleMinutes: z.number().int().positive().optional(),
|
idleMinutes: z.number().int().positive().optional(),
|
||||||
|
heartbeatIdleMinutes: z.number().int().positive().optional(),
|
||||||
store: z.string().optional(),
|
store: z.string().optional(),
|
||||||
sessionArgNew: z.array(z.string()).optional(),
|
sessionArgNew: z.array(z.string()).optional(),
|
||||||
sessionArgResume: z.array(z.string()).optional(),
|
sessionArgResume: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
deriveSessionKey,
|
deriveSessionKey,
|
||||||
loadSessionStore,
|
loadSessionStore,
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
|
saveSessionStore,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { danger, isVerbose, logVerbose, success } from "../globals.js";
|
import { danger, isVerbose, logVerbose, success } from "../globals.js";
|
||||||
import { logInfo } from "../logger.js";
|
import { logInfo } from "../logger.js";
|
||||||
@@ -92,7 +93,7 @@ export async function runWebHeartbeatOnce(opts: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const sessionSnapshot = getSessionSnapshot(cfg, to);
|
const sessionSnapshot = getSessionSnapshot(cfg, to, true);
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
heartbeatLogger.info(
|
heartbeatLogger.info(
|
||||||
{
|
{
|
||||||
@@ -112,7 +113,7 @@ export async function runWebHeartbeatOnce(opts: {
|
|||||||
Body: HEARTBEAT_PROMPT,
|
Body: HEARTBEAT_PROMPT,
|
||||||
From: to,
|
From: to,
|
||||||
To: to,
|
To: to,
|
||||||
MessageSid: undefined,
|
MessageSid: sessionSnapshot.entry?.sessionId,
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
cfg,
|
cfg,
|
||||||
@@ -139,6 +140,15 @@ export async function runWebHeartbeatOnce(opts: {
|
|||||||
(replyResult.mediaUrl ?? replyResult.mediaUrls?.length ?? 0) > 0;
|
(replyResult.mediaUrl ?? replyResult.mediaUrls?.length ?? 0) > 0;
|
||||||
const stripped = stripHeartbeatToken(replyResult.text);
|
const stripped = stripHeartbeatToken(replyResult.text);
|
||||||
if (stripped.shouldSkip && !hasMedia) {
|
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(
|
heartbeatLogger.info(
|
||||||
{ to, reason: "heartbeat-token", rawLength: replyResult.text?.length },
|
{ to, reason: "heartbeat-token", rawLength: replyResult.text?.length },
|
||||||
"heartbeat skipped",
|
"heartbeat skipped",
|
||||||
@@ -185,14 +195,20 @@ function getFallbackRecipient(cfg: ReturnType<typeof loadConfig>) {
|
|||||||
return mostRecent ? normalizeE164(mostRecent[0]) : null;
|
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 sessionCfg = cfg.inbound?.reply?.session;
|
||||||
const scope = sessionCfg?.scope ?? "per-sender";
|
const scope = sessionCfg?.scope ?? "per-sender";
|
||||||
const key = deriveSessionKey(scope, { From: from, To: "", Body: "" });
|
const key = deriveSessionKey(scope, { From: from, To: "", Body: "" });
|
||||||
const store = loadSessionStore(resolveStorePath(sessionCfg?.store));
|
const store = loadSessionStore(resolveStorePath(sessionCfg?.store));
|
||||||
const entry = store[key];
|
const entry = store[key];
|
||||||
const idleMinutes = Math.max(
|
const idleMinutes = Math.max(
|
||||||
sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES,
|
(isHeartbeat
|
||||||
|
? (sessionCfg?.heartbeatIdleMinutes ?? sessionCfg?.idleMinutes)
|
||||||
|
: sessionCfg?.idleMinutes) ?? DEFAULT_IDLE_MINUTES,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
const fresh = !!(
|
const fresh = !!(
|
||||||
|
|||||||
Reference in New Issue
Block a user