fix(sessions): preserve active route updates during maintenance

This commit is contained in:
Peter Steinberger
2026-04-23 09:10:51 +01:00
parent 94f703a845
commit 1263d4278e
3 changed files with 23 additions and 7 deletions

View File

@@ -1,5 +1,4 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
clearRuntimeConfigSnapshot,
@@ -10,6 +9,7 @@ import {
loadSessionStore,
updateSessionStore,
} from "openclaw/plugin-sdk/config-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
@@ -20,7 +20,7 @@ function createSuiteTempRootTracker(params: { prefix: string }) {
const children: string[] = [];
return {
async setup() {
root = await fs.mkdtemp(path.join(os.tmpdir(), params.prefix));
root = await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), params.prefix));
},
async make(name: string) {
if (!root) {

View File

@@ -156,12 +156,19 @@ export function resolveMaintenanceConfigFromInput(
export function pruneStaleEntries(
store: Record<string, SessionEntry>,
overrideMaxAgeMs?: number,
opts: { log?: boolean; onPruned?: (params: { key: string; entry: SessionEntry }) => void } = {},
opts: {
log?: boolean;
onPruned?: (params: { key: string; entry: SessionEntry }) => void;
preserveKeys?: ReadonlySet<string>;
} = {},
): number {
const maxAgeMs = overrideMaxAgeMs ?? resolveMaintenanceConfigFromInput().pruneAfterMs;
const cutoffMs = Date.now() - maxAgeMs;
let pruned = 0;
for (const [key, entry] of Object.entries(store)) {
if (opts.preserveKeys?.has(key)) {
continue;
}
if (entry?.updatedAt != null && entry.updatedAt < cutoffMs) {
opts.onPruned?.({ key, entry });
delete store[key];
@@ -265,11 +272,16 @@ export function capEntryCount(
opts: {
log?: boolean;
onCapped?: (params: { key: string; entry: SessionEntry }) => void;
preserveKeys?: ReadonlySet<string>;
} = {},
): number {
const maxEntries = overrideMax ?? resolveMaintenanceConfigFromInput().maxEntries;
const keys = Object.keys(store);
if (keys.length <= maxEntries) {
const preservedCount = opts.preserveKeys
? Object.keys(store).filter((key) => opts.preserveKeys?.has(key)).length
: 0;
const maxRemovableEntries = Math.max(0, maxEntries - preservedCount);
const keys = Object.keys(store).filter((key) => !opts.preserveKeys?.has(key));
if (keys.length <= maxRemovableEntries) {
return 0;
}
@@ -280,7 +292,7 @@ export function capEntryCount(
return bTime - aTime;
});
const toRemove = sorted.slice(maxEntries);
const toRemove = sorted.slice(maxRemovableEntries);
for (const key of toRemove) {
const entry = store[key];
if (entry) {

View File

@@ -281,17 +281,22 @@ async function saveSessionStoreUnlocked(
diskBudget,
});
} else {
const preserveSessionKeys = opts?.activeSessionKey
? new Set([opts.activeSessionKey])
: undefined;
// Prune stale entries and cap total count before serializing.
const removedSessionFiles = new Map<string, string | undefined>();
const pruned = pruneStaleEntries(store, maintenance.pruneAfterMs, {
onPruned: ({ entry }) => {
rememberRemovedSessionFile(removedSessionFiles, entry);
},
preserveKeys: preserveSessionKeys,
});
const capped = capEntryCount(store, maintenance.maxEntries, {
onCapped: ({ entry }) => {
rememberRemovedSessionFile(removedSessionFiles, entry);
},
preserveKeys: preserveSessionKeys,
});
const archivedDirs = new Set<string>();
const referencedSessionIds = new Set(
@@ -726,7 +731,6 @@ export async function updateLastRoute(params: {
const store = loadSessionStore(storePath);
const resolved = resolveSessionStoreEntry({ store, sessionKey });
const existing = resolved.existing;
const now = Date.now();
const explicitContext = normalizeDeliveryContext(params.deliveryContext);
const inlineContext = normalizeDeliveryContext({
channel,