feat(sessions): add channelIdleMinutes config for per-channel session idle durations (#1353)

* feat(sessions): add channelIdleMinutes config for per-channel session idle durations

Add new `channelIdleMinutes` config option to allow different session idle
timeouts per channel. For example, Discord sessions can now be configured
to last 7 days (10080 minutes) while other channels use shorter defaults.

Config example:
  sessions:
    channelIdleMinutes:
      discord: 10080  # 7 days

The channel-specific idle is passed as idleMinutesOverride to the existing
resolveSessionResetPolicy, integrating cleanly with the new reset policy
architecture.

* fix

* feat: add per-channel session reset overrides (#1353) (thanks @cash-echo-bot)

---------

Co-authored-by: Cash Williams <cashwilliams@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Echo
2026-01-21 13:10:31 -06:00
committed by GitHub
parent 403904ecd1
commit c415ccaed5
14 changed files with 123 additions and 28 deletions

View File

@@ -8,7 +8,7 @@ import { saveSessionStore } from "../../config/sessions.js";
import { getSessionSnapshot } from "./session-snapshot.js";
describe("getSessionSnapshot", () => {
it("uses heartbeat idle override while daily reset still applies", async () => {
it("uses channel reset overrides when configured", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 0, 18, 5, 0, 0));
try {
@@ -20,6 +20,7 @@ describe("getSessionSnapshot", () => {
[sessionKey]: {
sessionId: "snapshot-session",
updatedAt: new Date(2026, 0, 18, 3, 30, 0).getTime(),
lastChannel: "whatsapp",
},
});
@@ -27,7 +28,9 @@ describe("getSessionSnapshot", () => {
session: {
store: storePath,
reset: { mode: "daily", atHour: 4, idleMinutes: 240 },
heartbeatIdleMinutes: 30,
resetByChannel: {
whatsapp: { mode: "idle", idleMinutes: 360 },
},
},
} as Parameters<typeof getSessionSnapshot>[0];
@@ -35,9 +38,10 @@ describe("getSessionSnapshot", () => {
sessionKey,
});
expect(snapshot.resetPolicy.idleMinutes).toBe(30);
expect(snapshot.fresh).toBe(false);
expect(snapshot.dailyResetAt).toBe(new Date(2026, 0, 18, 4, 0, 0).getTime());
expect(snapshot.resetPolicy.mode).toBe("idle");
expect(snapshot.resetPolicy.idleMinutes).toBe(360);
expect(snapshot.fresh).toBe(true);
expect(snapshot.dailyResetAt).toBeUndefined();
} finally {
vi.useRealTimers();
}

View File

@@ -2,6 +2,7 @@ import type { loadConfig } from "../../config/config.js";
import {
evaluateSessionFreshness,
loadSessionStore,
resolveChannelResetConfig,
resolveThreadFlag,
resolveSessionResetPolicy,
resolveSessionResetType,
@@ -13,7 +14,7 @@ import { normalizeMainKey } from "../../routing/session-key.js";
export function getSessionSnapshot(
cfg: ReturnType<typeof loadConfig>,
from: string,
isHeartbeat = false,
_isHeartbeat = false,
ctx?: {
sessionKey?: string | null;
isGroup?: boolean;
@@ -34,6 +35,7 @@ export function getSessionSnapshot(
);
const store = loadSessionStore(resolveStorePath(sessionCfg?.store));
const entry = store[key];
const isThread = resolveThreadFlag({
sessionKey: key,
messageThreadId: ctx?.messageThreadId ?? null,
@@ -42,11 +44,14 @@ export function getSessionSnapshot(
parentSessionKey: ctx?.parentSessionKey ?? null,
});
const resetType = resolveSessionResetType({ sessionKey: key, isGroup: ctx?.isGroup, isThread });
const idleMinutesOverride = isHeartbeat ? sessionCfg?.heartbeatIdleMinutes : undefined;
const channelReset = resolveChannelResetConfig({
sessionCfg,
channel: entry?.lastChannel ?? entry?.channel,
});
const resetPolicy = resolveSessionResetPolicy({
sessionCfg,
resetType,
idleMinutesOverride,
resetOverride: channelReset,
});
const now = Date.now();
const freshness = entry