From c70be4b4afd0e3c441d70a314c20416a8a797054 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 13 Apr 2026 18:28:53 +0100 Subject: [PATCH] perf(sessions): isolate reset policy helpers --- src/config/sessions/reset-policy.ts | 107 +++++++++++++++++++++ src/config/sessions/reset.ts | 118 +++--------------------- src/cron/isolated-agent/session.test.ts | 8 +- src/cron/isolated-agent/session.ts | 2 +- 4 files changed, 124 insertions(+), 111 deletions(-) create mode 100644 src/config/sessions/reset-policy.ts diff --git a/src/config/sessions/reset-policy.ts b/src/config/sessions/reset-policy.ts new file mode 100644 index 00000000000..ebf1aea5133 --- /dev/null +++ b/src/config/sessions/reset-policy.ts @@ -0,0 +1,107 @@ +import type { SessionConfig, SessionResetConfig } from "../types.base.js"; +import { DEFAULT_IDLE_MINUTES } from "./types.js"; + +export type SessionResetMode = "daily" | "idle"; +export type SessionResetType = "direct" | "group" | "thread"; + +export type SessionResetPolicy = { + mode: SessionResetMode; + atHour: number; + idleMinutes?: number; +}; + +export type SessionFreshness = { + fresh: boolean; + dailyResetAt?: number; + idleExpiresAt?: number; +}; + +export const DEFAULT_RESET_MODE: SessionResetMode = "daily"; +export const DEFAULT_RESET_AT_HOUR = 4; + +export function resolveDailyResetAtMs(now: number, atHour: number): number { + const normalizedAtHour = normalizeResetAtHour(atHour); + const resetAt = new Date(now); + resetAt.setHours(normalizedAtHour, 0, 0, 0); + if (now < resetAt.getTime()) { + resetAt.setDate(resetAt.getDate() - 1); + } + return resetAt.getTime(); +} + +export function resolveSessionResetPolicy(params: { + sessionCfg?: SessionConfig; + resetType: SessionResetType; + resetOverride?: SessionResetConfig; +}): SessionResetPolicy { + const sessionCfg = params.sessionCfg; + const baseReset = params.resetOverride ?? sessionCfg?.reset; + // Backward compat: accept legacy "dm" key as alias for "direct" + const typeReset = params.resetOverride + ? undefined + : (sessionCfg?.resetByType?.[params.resetType] ?? + (params.resetType === "direct" + ? (sessionCfg?.resetByType as { dm?: SessionResetConfig } | undefined)?.dm + : undefined)); + const hasExplicitReset = Boolean(baseReset || sessionCfg?.resetByType); + const legacyIdleMinutes = params.resetOverride ? undefined : sessionCfg?.idleMinutes; + const mode = + typeReset?.mode ?? + baseReset?.mode ?? + (!hasExplicitReset && legacyIdleMinutes != null ? "idle" : DEFAULT_RESET_MODE); + const atHour = normalizeResetAtHour( + typeReset?.atHour ?? baseReset?.atHour ?? DEFAULT_RESET_AT_HOUR, + ); + const idleMinutesRaw = typeReset?.idleMinutes ?? baseReset?.idleMinutes ?? legacyIdleMinutes; + + let idleMinutes: number | undefined; + if (idleMinutesRaw != null) { + const normalized = Math.floor(idleMinutesRaw); + if (Number.isFinite(normalized)) { + idleMinutes = Math.max(normalized, 0); + } + } else if (mode === "idle") { + idleMinutes = DEFAULT_IDLE_MINUTES; + } + + return { mode, atHour, idleMinutes }; +} + +export function evaluateSessionFreshness(params: { + updatedAt: number; + now: number; + policy: SessionResetPolicy; +}): SessionFreshness { + const dailyResetAt = + params.policy.mode === "daily" + ? resolveDailyResetAtMs(params.now, params.policy.atHour) + : undefined; + const idleExpiresAt = + params.policy.idleMinutes != null && params.policy.idleMinutes > 0 + ? params.updatedAt + params.policy.idleMinutes * 60_000 + : undefined; + const staleDaily = dailyResetAt != null && params.updatedAt < dailyResetAt; + const staleIdle = idleExpiresAt != null && params.now > idleExpiresAt; + return { + fresh: !(staleDaily || staleIdle), + dailyResetAt, + idleExpiresAt, + }; +} + +function normalizeResetAtHour(value: number | undefined): number { + if (typeof value !== "number" || !Number.isFinite(value)) { + return DEFAULT_RESET_AT_HOUR; + } + const normalized = Math.floor(value); + if (!Number.isFinite(normalized)) { + return DEFAULT_RESET_AT_HOUR; + } + if (normalized < 0) { + return 0; + } + if (normalized > 23) { + return 23; + } + return normalized; +} diff --git a/src/config/sessions/reset.ts b/src/config/sessions/reset.ts index 6e147f8fda4..31c96664a20 100644 --- a/src/config/sessions/reset.ts +++ b/src/config/sessions/reset.ts @@ -5,25 +5,18 @@ import { } from "../../shared/string-coerce.js"; import { normalizeMessageChannel } from "../../utils/message-channel.js"; import type { SessionConfig, SessionResetConfig } from "../types.base.js"; -import { DEFAULT_IDLE_MINUTES } from "./types.js"; - -export type SessionResetMode = "daily" | "idle"; -export type SessionResetType = "direct" | "group" | "thread"; - -export type SessionResetPolicy = { - mode: SessionResetMode; - atHour: number; - idleMinutes?: number; -}; - -export type SessionFreshness = { - fresh: boolean; - dailyResetAt?: number; - idleExpiresAt?: number; -}; - -export const DEFAULT_RESET_MODE: SessionResetMode = "daily"; -export const DEFAULT_RESET_AT_HOUR = 4; +export { + DEFAULT_RESET_AT_HOUR, + DEFAULT_RESET_MODE, + evaluateSessionFreshness, + resolveDailyResetAtMs, + resolveSessionResetPolicy, + type SessionFreshness, + type SessionResetMode, + type SessionResetPolicy, + type SessionResetType, +} from "./reset-policy.js"; +import type { SessionResetType } from "./reset-policy.js"; const GROUP_SESSION_MARKERS = [":group:", ":channel:"]; @@ -71,54 +64,6 @@ export function resolveThreadFlag(params: { return isThreadSessionKey(params.sessionKey); } -export function resolveDailyResetAtMs(now: number, atHour: number): number { - const normalizedAtHour = normalizeResetAtHour(atHour); - const resetAt = new Date(now); - resetAt.setHours(normalizedAtHour, 0, 0, 0); - if (now < resetAt.getTime()) { - resetAt.setDate(resetAt.getDate() - 1); - } - return resetAt.getTime(); -} - -export function resolveSessionResetPolicy(params: { - sessionCfg?: SessionConfig; - resetType: SessionResetType; - resetOverride?: SessionResetConfig; -}): SessionResetPolicy { - const sessionCfg = params.sessionCfg; - const baseReset = params.resetOverride ?? sessionCfg?.reset; - // Backward compat: accept legacy "dm" key as alias for "direct" - const typeReset = params.resetOverride - ? undefined - : (sessionCfg?.resetByType?.[params.resetType] ?? - (params.resetType === "direct" - ? (sessionCfg?.resetByType as { dm?: SessionResetConfig } | undefined)?.dm - : undefined)); - const hasExplicitReset = Boolean(baseReset || sessionCfg?.resetByType); - const legacyIdleMinutes = params.resetOverride ? undefined : sessionCfg?.idleMinutes; - const mode = - typeReset?.mode ?? - baseReset?.mode ?? - (!hasExplicitReset && legacyIdleMinutes != null ? "idle" : DEFAULT_RESET_MODE); - const atHour = normalizeResetAtHour( - typeReset?.atHour ?? baseReset?.atHour ?? DEFAULT_RESET_AT_HOUR, - ); - const idleMinutesRaw = typeReset?.idleMinutes ?? baseReset?.idleMinutes ?? legacyIdleMinutes; - - let idleMinutes: number | undefined; - if (idleMinutesRaw != null) { - const normalized = Math.floor(idleMinutesRaw); - if (Number.isFinite(normalized)) { - idleMinutes = Math.max(normalized, 0); - } - } else if (mode === "idle") { - idleMinutes = DEFAULT_IDLE_MINUTES; - } - - return { mode, atHour, idleMinutes }; -} - export function resolveChannelResetConfig(params: { sessionCfg?: SessionConfig; channel?: string | null; @@ -135,42 +80,3 @@ export function resolveChannelResetConfig(params: { } return resetByChannel[key]; } - -export function evaluateSessionFreshness(params: { - updatedAt: number; - now: number; - policy: SessionResetPolicy; -}): SessionFreshness { - const dailyResetAt = - params.policy.mode === "daily" - ? resolveDailyResetAtMs(params.now, params.policy.atHour) - : undefined; - const idleExpiresAt = - params.policy.idleMinutes != null && params.policy.idleMinutes > 0 - ? params.updatedAt + params.policy.idleMinutes * 60_000 - : undefined; - const staleDaily = dailyResetAt != null && params.updatedAt < dailyResetAt; - const staleIdle = idleExpiresAt != null && params.now > idleExpiresAt; - return { - fresh: !(staleDaily || staleIdle), - dailyResetAt, - idleExpiresAt, - }; -} - -function normalizeResetAtHour(value: number | undefined): number { - if (typeof value !== "number" || !Number.isFinite(value)) { - return DEFAULT_RESET_AT_HOUR; - } - const normalized = Math.floor(value); - if (!Number.isFinite(normalized)) { - return DEFAULT_RESET_AT_HOUR; - } - if (normalized < 0) { - return 0; - } - if (normalized > 23) { - return 23; - } - return normalized; -} diff --git a/src/cron/isolated-agent/session.test.ts b/src/cron/isolated-agent/session.test.ts index 8b050ce53b4..b7b7ca43aa3 100644 --- a/src/cron/isolated-agent/session.test.ts +++ b/src/cron/isolated-agent/session.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; -vi.mock("../../config/sessions/store.js", () => ({ +vi.mock("../../config/sessions/store-load.js", () => ({ loadSessionStore: vi.fn(), })); @@ -9,7 +9,7 @@ vi.mock("../../config/sessions/paths.js", () => ({ resolveStorePath: vi.fn().mockReturnValue("/tmp/test-store.json"), })); -vi.mock("../../config/sessions/reset.js", () => ({ +vi.mock("../../config/sessions/reset-policy.js", () => ({ evaluateSessionFreshness: vi.fn().mockReturnValue({ fresh: true }), resolveSessionResetPolicy: vi.fn().mockReturnValue({ mode: "idle", idleMinutes: 60 }), })); @@ -24,8 +24,8 @@ vi.mock("../../agents/bootstrap-cache.js", () => ({ })); import { clearBootstrapSnapshot } from "../../agents/bootstrap-cache.js"; -import { evaluateSessionFreshness } from "../../config/sessions/reset.js"; -import { loadSessionStore } from "../../config/sessions/store.js"; +import { evaluateSessionFreshness } from "../../config/sessions/reset-policy.js"; +import { loadSessionStore } from "../../config/sessions/store-load.js"; import { resolveCronSession } from "./session.js"; const NOW_MS = 1_737_600_000_000; diff --git a/src/cron/isolated-agent/session.ts b/src/cron/isolated-agent/session.ts index c936c2941c1..fbfb907c980 100644 --- a/src/cron/isolated-agent/session.ts +++ b/src/cron/isolated-agent/session.ts @@ -4,7 +4,7 @@ import { resolveStorePath } from "../../config/sessions/paths.js"; import { evaluateSessionFreshness, resolveSessionResetPolicy, -} from "../../config/sessions/reset.js"; +} from "../../config/sessions/reset-policy.js"; import { loadSessionStore } from "../../config/sessions/store-load.js"; import type { SessionEntry } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js";