From 522c09656fd59e469948b2316aa54c525804cbd0 Mon Sep 17 00:00:00 2001 From: Bryan Tegomoh Date: Wed, 18 Mar 2026 01:38:10 -0500 Subject: [PATCH] fix(sessions): preserve updatedAt in updateLastRoute to allow idle/daily resets --- src/config/sessions/store.ts | 2 +- .../sessions/store.update-last-route.test.ts | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/config/sessions/store.update-last-route.test.ts diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index a70285c4c62..885464334c4 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -869,7 +869,7 @@ export async function updateLastRoute(params: { lastAccountId: normalized.lastAccountId, lastThreadId: normalized.lastThreadId, }; - const next = mergeSessionEntry( + const next = mergeSessionEntryPreserveActivity( existing, metaPatch ? { ...basePatch, ...metaPatch } : basePatch, ); diff --git a/src/config/sessions/store.update-last-route.test.ts b/src/config/sessions/store.update-last-route.test.ts new file mode 100644 index 00000000000..8348c723ad7 --- /dev/null +++ b/src/config/sessions/store.update-last-route.test.ts @@ -0,0 +1,75 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { clearSessionStoreCacheForTest, loadSessionStore, saveSessionStore, updateLastRoute } from "./store.js"; +import type { SessionEntry } from "./types.js"; + +// Prevent reads of a real openclaw.json during tests. +vi.mock("../config.js", () => ({ + loadConfig: vi.fn().mockReturnValue({}), +})); + +let testDir = ""; + +beforeEach(async () => { + testDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ulr-test-")); +}); + +afterEach(async () => { + clearSessionStoreCacheForTest(); + await fs.rm(testDir, { recursive: true, force: true }); +}); + +function makeEntry(updatedAt: number): SessionEntry { + return { sessionId: crypto.randomUUID(), updatedAt }; +} + +describe("updateLastRoute", () => { + it("does not bump updatedAt for an existing session", async () => { + // Verifies fix for issue #49515: updateLastRoute was calling mergeSessionEntry + // which reset updatedAt to Date.now() on every inbound message, preventing + // idle/daily session resets from firing. + const storePath = path.join(testDir, "sessions.json"); + const sessionKey = "agent:main:telegram:dm:user-1"; + const originalUpdatedAt = Date.now() - 60_000; // 1 minute ago + + const initial: Record = { + [sessionKey]: makeEntry(originalUpdatedAt), + }; + await saveSessionStore(storePath, initial); + + await updateLastRoute({ + storePath, + sessionKey, + deliveryContext: { channel: "telegram", to: "telegram:user-1" }, + }); + + const store = loadSessionStore(storePath); + const entry = store[sessionKey]; + expect(entry).toBeDefined(); + // updatedAt must not be bumped to wall-clock time — it should stay at originalUpdatedAt + // so that evaluateSessionFreshness can correctly detect idle sessions. + expect(entry!.updatedAt).toBe(originalUpdatedAt); + }); + + it("sets updatedAt to now for a new (non-existing) session", async () => { + const storePath = path.join(testDir, "sessions.json"); + await saveSessionStore(storePath, {}); + + const before = Date.now(); + await updateLastRoute({ + storePath, + sessionKey: "agent:main:telegram:dm:user-2", + deliveryContext: { channel: "telegram", to: "telegram:user-2" }, + }); + const after = Date.now(); + + const store = loadSessionStore(storePath); + const entry = store["agent:main:telegram:dm:user-2"]; + expect(entry).toBeDefined(); + expect(entry!.updatedAt).toBeGreaterThanOrEqual(before); + expect(entry!.updatedAt).toBeLessThanOrEqual(after); + }); +});