diff --git a/src/config/sessions/store.session-key-normalization.test.ts b/src/config/sessions/store.session-key-normalization.test.ts index 76fdf4d723b..8f95f885f9f 100644 --- a/src/config/sessions/store.session-key-normalization.test.ts +++ b/src/config/sessions/store.session-key-normalization.test.ts @@ -108,4 +108,41 @@ describe("session store key normalization", () => { expect(store[CANONICAL_KEY]?.sessionId).toBe("legacy-session"); expect(store[MIXED_CASE_KEY]).toBeUndefined(); }); + + it("preserves updatedAt when recording inbound metadata for an existing session", async () => { + await fs.writeFile( + storePath, + JSON.stringify( + { + [CANONICAL_KEY]: { + sessionId: "existing-session", + updatedAt: 1111, + chatType: "direct", + channel: "webchat", + origin: { + provider: "webchat", + chatType: "direct", + from: "WebChat:User-1", + to: "webchat:user-1", + }, + }, + }, + null, + 2, + ), + "utf-8", + ); + clearSessionStoreCacheForTest(); + + await recordSessionMetaFromInbound({ + storePath, + sessionKey: CANONICAL_KEY, + ctx: createInboundContext(), + }); + + const store = loadSessionStore(storePath, { skipCache: true }); + expect(store[CANONICAL_KEY]?.sessionId).toBe("existing-session"); + expect(store[CANONICAL_KEY]?.updatedAt).toBe(1111); + expect(store[CANONICAL_KEY]?.origin?.provider).toBe("webchat"); + }); }); diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index 501a2063f9b..0e240309057 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -1,3 +1,4 @@ +import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { acquireSessionWriteLock } from "../../agents/session-write-lock.js"; @@ -736,7 +737,16 @@ export async function recordSessionMetaFromInbound(params: { if (!existing && !createIfMissing) { return null; } - const next = mergeSessionEntry(existing, patch); + const next = existing + ? normalizeSessionRuntimeModelFields({ + ...existing, + ...patch, + // Inbound metadata updates must not refresh activity timestamps; + // idle reset evaluation relies on updatedAt from actual session turns. + sessionId: existing.sessionId ?? crypto.randomUUID(), + updatedAt: existing.updatedAt ?? Date.now(), + }) + : mergeSessionEntry(existing, patch); store[resolved.normalizedKey] = next; for (const legacyKey of resolved.legacyKeys) { delete store[legacyKey];