mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:30:47 +00:00
fix: keep session maintenance helpers acyclic
This commit is contained in:
@@ -8,12 +8,18 @@ import {
|
||||
setSerializedSessionStore,
|
||||
writeSessionStoreCache,
|
||||
} from "./store-cache.js";
|
||||
import {
|
||||
capEntryCount,
|
||||
pruneStaleEntries,
|
||||
resolveMaintenanceConfigFromInput,
|
||||
type ResolvedSessionMaintenanceConfig,
|
||||
} from "./store-maintenance.js";
|
||||
import { applySessionStoreMigrations } from "./store-migrations.js";
|
||||
import { capEntryCount, pruneStaleEntries, resolveMaintenanceConfig } from "./store-maintenance.js";
|
||||
import { normalizeSessionRuntimeModelFields, type SessionEntry } from "./types.js";
|
||||
|
||||
export type LoadSessionStoreOptions = {
|
||||
skipCache?: boolean;
|
||||
maintenanceConfig?: ResolvedSessionMaintenanceConfig;
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("sessions/store");
|
||||
@@ -122,7 +128,7 @@ export function loadSessionStore(
|
||||
|
||||
applySessionStoreMigrations(store);
|
||||
normalizeSessionStore(store);
|
||||
const maintenance = resolveMaintenanceConfig();
|
||||
const maintenance = opts.maintenanceConfig ?? resolveMaintenanceConfigFromInput();
|
||||
if (maintenance.mode === "enforce" && Object.keys(store).length > maintenance.maxEntries) {
|
||||
const beforeCount = Object.keys(store).length;
|
||||
const pruned = pruneStaleEntries(store, maintenance.pruneAfterMs, { log: false });
|
||||
|
||||
16
src/config/sessions/store-maintenance-runtime.ts
Normal file
16
src/config/sessions/store-maintenance-runtime.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { loadConfig } from "../config.js";
|
||||
import type { SessionMaintenanceConfig } from "../types.base.js";
|
||||
import {
|
||||
resolveMaintenanceConfigFromInput,
|
||||
type ResolvedSessionMaintenanceConfig,
|
||||
} from "./store-maintenance.js";
|
||||
|
||||
export function resolveMaintenanceConfig(): ResolvedSessionMaintenanceConfig {
|
||||
let maintenance: SessionMaintenanceConfig | undefined;
|
||||
try {
|
||||
maintenance = loadConfig().session?.maintenance;
|
||||
} catch {
|
||||
// Config may not be available in narrow test/runtime helpers.
|
||||
}
|
||||
return resolveMaintenanceConfigFromInput(maintenance);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { parseByteSize } from "../../cli/parse-bytes.js";
|
||||
import { parseDurationMs } from "../../cli/parse-duration.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { normalizeStringifiedOptionalString } from "../../shared/string-coerce.js";
|
||||
import { loadConfig } from "../config.js";
|
||||
import type { SessionMaintenanceConfig, SessionMaintenanceMode } from "../types.base.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
@@ -149,16 +148,6 @@ export function resolveMaintenanceConfigFromInput(
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveMaintenanceConfig(): ResolvedSessionMaintenanceConfig {
|
||||
let maintenance: SessionMaintenanceConfig | undefined;
|
||||
try {
|
||||
maintenance = loadConfig().session?.maintenance;
|
||||
} catch {
|
||||
// Config may not be available (e.g. in tests). Use defaults.
|
||||
}
|
||||
return resolveMaintenanceConfigFromInput(maintenance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entries whose `updatedAt` is older than the configured threshold.
|
||||
* Entries without `updatedAt` are kept (cannot determine staleness).
|
||||
@@ -169,7 +158,7 @@ export function pruneStaleEntries(
|
||||
overrideMaxAgeMs?: number,
|
||||
opts: { log?: boolean; onPruned?: (params: { key: string; entry: SessionEntry }) => void } = {},
|
||||
): number {
|
||||
const maxAgeMs = overrideMaxAgeMs ?? resolveMaintenanceConfig().pruneAfterMs;
|
||||
const maxAgeMs = overrideMaxAgeMs ?? resolveMaintenanceConfigFromInput().pruneAfterMs;
|
||||
const cutoffMs = Date.now() - maxAgeMs;
|
||||
let pruned = 0;
|
||||
for (const [key, entry] of Object.entries(store)) {
|
||||
@@ -278,7 +267,7 @@ export function capEntryCount(
|
||||
onCapped?: (params: { key: string; entry: SessionEntry }) => void;
|
||||
} = {},
|
||||
): number {
|
||||
const maxEntries = overrideMax ?? resolveMaintenanceConfig().maxEntries;
|
||||
const maxEntries = overrideMax ?? resolveMaintenanceConfigFromInput().maxEntries;
|
||||
const keys = Object.keys(store);
|
||||
if (keys.length <= maxEntries) {
|
||||
return 0;
|
||||
@@ -323,7 +312,7 @@ export async function rotateSessionFile(
|
||||
storePath: string,
|
||||
overrideBytes?: number,
|
||||
): Promise<boolean> {
|
||||
const maxBytes = overrideBytes ?? resolveMaintenanceConfig().rotateBytes;
|
||||
const maxBytes = overrideBytes ?? resolveMaintenanceConfigFromInput().rotateBytes;
|
||||
|
||||
// Check current file size (file may not exist yet).
|
||||
const fileSize = await getSessionFileSize(storePath);
|
||||
|
||||
@@ -247,15 +247,6 @@ describe("Integration: saveSessionStore with pruning", () => {
|
||||
});
|
||||
|
||||
it("loadSessionStore prunes stale entries from oversized stores by default", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
session: {
|
||||
maintenance: {
|
||||
maxEntries: 2,
|
||||
pruneAfter: "7d",
|
||||
rotateBytes: 10_485_760,
|
||||
},
|
||||
},
|
||||
});
|
||||
const now = Date.now();
|
||||
const store: Record<string, SessionEntry> = {
|
||||
stale: makeEntry(now - 31 * DAY_MS),
|
||||
@@ -264,7 +255,14 @@ describe("Integration: saveSessionStore with pruning", () => {
|
||||
};
|
||||
await fs.writeFile(storePath, JSON.stringify(store), "utf-8");
|
||||
|
||||
const loaded = loadSessionStore(storePath, { skipCache: true });
|
||||
const loaded = loadSessionStore(storePath, {
|
||||
skipCache: true,
|
||||
maintenanceConfig: {
|
||||
...ENFORCED_MAINTENANCE_OVERRIDE,
|
||||
maxEntries: 2,
|
||||
pruneAfterMs: 7 * DAY_MS,
|
||||
},
|
||||
});
|
||||
|
||||
expect(loaded.stale).toBeUndefined();
|
||||
expect(loaded.recent).toBeDefined();
|
||||
@@ -272,15 +270,6 @@ describe("Integration: saveSessionStore with pruning", () => {
|
||||
});
|
||||
|
||||
it("loadSessionStore caps oversized stores by default", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
session: {
|
||||
maintenance: {
|
||||
maxEntries: 2,
|
||||
pruneAfter: "365d",
|
||||
rotateBytes: 10_485_760,
|
||||
},
|
||||
},
|
||||
});
|
||||
const now = Date.now();
|
||||
const store: Record<string, SessionEntry> = {
|
||||
oldest: makeEntry(now - 3 * DAY_MS),
|
||||
@@ -289,7 +278,14 @@ describe("Integration: saveSessionStore with pruning", () => {
|
||||
};
|
||||
await fs.writeFile(storePath, JSON.stringify(store), "utf-8");
|
||||
|
||||
const loaded = loadSessionStore(storePath, { skipCache: true });
|
||||
const loaded = loadSessionStore(storePath, {
|
||||
skipCache: true,
|
||||
maintenanceConfig: {
|
||||
...ENFORCED_MAINTENANCE_OVERRIDE,
|
||||
maxEntries: 2,
|
||||
pruneAfterMs: 365 * DAY_MS,
|
||||
},
|
||||
});
|
||||
|
||||
expect(Object.keys(loaded)).toHaveLength(2);
|
||||
expect(loaded.oldest).toBeUndefined();
|
||||
|
||||
@@ -34,11 +34,11 @@ import {
|
||||
type SessionStoreLockQueue,
|
||||
type SessionStoreLockTask,
|
||||
} from "./store-lock-state.js";
|
||||
import { resolveMaintenanceConfig } from "./store-maintenance-runtime.js";
|
||||
import {
|
||||
capEntryCount,
|
||||
getActiveSessionMaintenanceWarning,
|
||||
pruneStaleEntries,
|
||||
resolveMaintenanceConfig,
|
||||
rotateSessionFile,
|
||||
type ResolvedSessionMaintenanceConfig,
|
||||
type SessionMaintenanceWarning,
|
||||
|
||||
@@ -52,7 +52,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
sessionKey: params.sessionKey,
|
||||
deps: {
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
JSON.stringify({
|
||||
[alreadySuffixedKey]: {
|
||||
sessionId: "sid",
|
||||
updatedAt: 1,
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastProvider: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
@@ -128,7 +128,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
sessionKey: alreadySuffixedKey,
|
||||
deps: {
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -221,7 +221,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
reason: "interval",
|
||||
deps: {
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -233,7 +233,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
reason: "interval",
|
||||
deps: {
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -267,7 +267,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
JSON.stringify({
|
||||
[alreadyIsolatedKey]: {
|
||||
sessionId: "sid",
|
||||
updatedAt: 1,
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastProvider: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
@@ -284,7 +284,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
sessionKey: alreadyIsolatedKey,
|
||||
deps: {
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
JSON.stringify({
|
||||
[isolatedSessionKey]: {
|
||||
sessionId: "sid",
|
||||
updatedAt: 1,
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastProvider: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
@@ -321,7 +321,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
reason: "hook:wake",
|
||||
deps: {
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -364,7 +364,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
JSON.stringify({
|
||||
[isolatedSessionKey]: {
|
||||
sessionId: "sid",
|
||||
updatedAt: 1,
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastProvider: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
@@ -382,7 +382,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
sessionKey: isolatedSessionKey,
|
||||
deps: {
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -423,7 +423,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
JSON.stringify({
|
||||
[baseSessionKey]: {
|
||||
sessionId: "sid",
|
||||
updatedAt: 1,
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastProvider: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
@@ -472,7 +472,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
JSON.stringify({
|
||||
[legacyIsolatedKey]: {
|
||||
sessionId: "sid",
|
||||
updatedAt: 1,
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastProvider: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
@@ -488,7 +488,7 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => {
|
||||
sessionKey: legacyIsolatedKey,
|
||||
deps: {
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user