mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:50:42 +00:00
perf(sessions): isolate reset policy helpers
This commit is contained in:
107
src/config/sessions/reset-policy.ts
Normal file
107
src/config/sessions/reset-policy.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user