perf(sessions): isolate reset policy helpers

This commit is contained in:
Vincent Koc
2026-04-13 18:28:53 +01:00
parent b6abd68a29
commit c70be4b4af
4 changed files with 124 additions and 111 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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";