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

@@ -436,3 +436,42 @@ describe("initSessionState reset policy", () => {
}
});
});
describe("initSessionState channel reset overrides", () => {
it("uses channel-specific reset policy when configured", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-channel-idle-"));
const storePath = path.join(root, "sessions.json");
const sessionKey = "agent:main:discord:dm:123";
const sessionId = "session-override";
const updatedAt = Date.now() - (10080 - 1) * 60_000;
await saveSessionStore(storePath, {
[sessionKey]: {
sessionId,
updatedAt,
},
});
const cfg = {
session: {
store: storePath,
idleMinutes: 60,
resetByType: { dm: { mode: "idle", idleMinutes: 10 } },
resetByChannel: { discord: { mode: "idle", idleMinutes: 10080 } },
},
} as ClawdbotConfig;
const result = await initSessionState({
ctx: {
Body: "Hello",
SessionKey: sessionKey,
Provider: "discord",
},
cfg,
commandAuthorized: true,
});
expect(result.isNewSession).toBe(false);
expect(result.sessionEntry.sessionId).toBe(sessionId);
});
});

View File

@@ -11,6 +11,7 @@ import {
evaluateSessionFreshness,
type GroupKeyResolution,
loadSessionStore,
resolveChannelResetConfig,
resolveThreadFlag,
resolveSessionResetPolicy,
resolveSessionResetType,
@@ -106,6 +107,7 @@ export async function initSessionState(params: {
sessionKey: sessionCtxForState.SessionKey,
config: cfg,
});
const groupResolution = resolveGroupSessionKey(sessionCtxForState) ?? undefined;
const resetTriggers = sessionCfg?.resetTriggers?.length
? sessionCfg.resetTriggers
: DEFAULT_RESET_TRIGGERS;
@@ -129,7 +131,6 @@ export async function initSessionState(params: {
let persistedModelOverride: string | undefined;
let persistedProviderOverride: string | undefined;
const groupResolution = resolveGroupSessionKey(sessionCtxForState) ?? undefined;
const normalizedChatType = normalizeChatType(ctx.ChatType);
const isGroup =
normalizedChatType != null && normalizedChatType !== "direct" ? true : Boolean(groupResolution);
@@ -195,7 +196,19 @@ export async function initSessionState(params: {
parentSessionKey: ctx.ParentSessionKey,
});
const resetType = resolveSessionResetType({ sessionKey, isGroup, isThread });
const resetPolicy = resolveSessionResetPolicy({ sessionCfg, resetType });
const channelReset = resolveChannelResetConfig({
sessionCfg,
channel:
groupResolution?.channel ??
(ctx.OriginatingChannel as string | undefined) ??
ctx.Surface ??
ctx.Provider,
});
const resetPolicy = resolveSessionResetPolicy({
sessionCfg,
resetType,
resetOverride: channelReset,
});
const freshEntry = entry
? evaluateSessionFreshness({ updatedAt: entry.updatedAt, now, policy: resetPolicy }).fresh
: false;