From 1263d4278e86f9c636d1ea69eec5851871087e21 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 09:10:51 +0100 Subject: [PATCH] fix(sessions): preserve active route updates during maintenance --- ...e-context.session-recreate.test-support.ts | 4 ++-- src/config/sessions/store-maintenance.ts | 20 +++++++++++++++---- src/config/sessions/store.ts | 6 +++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/extensions/telegram/src/bot-message-context.session-recreate.test-support.ts b/extensions/telegram/src/bot-message-context.session-recreate.test-support.ts index acd7b453c71..1512259d0a2 100644 --- a/extensions/telegram/src/bot-message-context.session-recreate.test-support.ts +++ b/extensions/telegram/src/bot-message-context.session-recreate.test-support.ts @@ -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) { diff --git a/src/config/sessions/store-maintenance.ts b/src/config/sessions/store-maintenance.ts index 83c793768a9..2d9c316859b 100644 --- a/src/config/sessions/store-maintenance.ts +++ b/src/config/sessions/store-maintenance.ts @@ -156,12 +156,19 @@ export function resolveMaintenanceConfigFromInput( export function pruneStaleEntries( store: Record, overrideMaxAgeMs?: number, - opts: { log?: boolean; onPruned?: (params: { key: string; entry: SessionEntry }) => void } = {}, + opts: { + log?: boolean; + onPruned?: (params: { key: string; entry: SessionEntry }) => void; + preserveKeys?: ReadonlySet; + } = {}, ): 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; } = {}, ): 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) { diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index 00c6158eeb2..db298b3d4f0 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -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(); 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(); 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,