From f4f20c6762c1d92ace2e892255a79425c58d7fb8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 05:28:52 +0000 Subject: [PATCH] refactor: normalize session route fields Co-authored-by: adam91holt --- src/config/sessions.test.ts | 21 +++++++++++++++++++++ src/config/sessions/store.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/config/sessions.test.ts b/src/config/sessions.test.ts index bc94dce3e..8c5dff606 100644 --- a/src/config/sessions.test.ts +++ b/src/config/sessions.test.ts @@ -157,6 +157,27 @@ describe("sessions", () => { expect(store["agent:main:two"]?.sessionId).toBe("sess-2"); }); + it("normalizes last route fields on write", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-")); + const storePath = path.join(dir, "sessions.json"); + await fs.writeFile(storePath, "{}", "utf-8"); + + await updateSessionStore(storePath, (store) => { + store["agent:main:main"] = { + sessionId: "sess-normalized", + updatedAt: 1, + lastChannel: " WhatsApp ", + lastTo: " +1555 ", + lastAccountId: " acct-1 ", + }; + }); + + const store = loadSessionStore(storePath); + expect(store["agent:main:main"]?.lastChannel).toBe("whatsapp"); + expect(store["agent:main:main"]?.lastTo).toBe("+1555"); + expect(store["agent:main:main"]?.lastAccountId).toBe("acct-1"); + }); + it("updateSessionStore keeps deletions when concurrent writes happen", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-")); const storePath = path.join(dir, "sessions.json"); diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index d5de85b47..8378df605 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -4,6 +4,8 @@ import path from "node:path"; import JSON5 from "json5"; import { getFileMtimeMs, isCacheEnabled, resolveCacheTtlMs } from "../cache-utils.js"; +import { normalizeAccountId } from "../../utils/account-id.js"; +import { normalizeMessageChannel } from "../../utils/message-channel.js"; import { mergeSessionEntry, type SessionEntry } from "./types.js"; // ============================================================================ @@ -41,6 +43,35 @@ function invalidateSessionStoreCache(storePath: string): void { SESSION_STORE_CACHE.delete(storePath); } +function normalizeSessionEntryDelivery(entry: SessionEntry): SessionEntry { + const normalizedLastChannel = normalizeMessageChannel(entry.lastChannel) ?? undefined; + const normalizedLastTo = typeof entry.lastTo === "string" ? entry.lastTo.trim() : undefined; + const normalizedLastAccountId = normalizeAccountId(entry.lastAccountId); + if ( + normalizedLastChannel === entry.lastChannel && + normalizedLastTo === entry.lastTo && + normalizedLastAccountId === entry.lastAccountId + ) { + return entry; + } + return { + ...entry, + lastChannel: normalizedLastChannel, + lastTo: normalizedLastTo || undefined, + lastAccountId: normalizedLastAccountId, + }; +} + +function normalizeSessionStore(store: Record): void { + for (const [key, entry] of Object.entries(store)) { + if (!entry) continue; + const normalized = normalizeSessionEntryDelivery(entry); + if (normalized !== entry) { + store[key] = normalized; + } + } +} + export function clearSessionStoreCacheForTest(): void { SESSION_STORE_CACHE.clear(); } @@ -114,6 +145,8 @@ async function saveSessionStoreUnlocked( // Invalidate cache on write to ensure consistency invalidateSessionStoreCache(storePath); + normalizeSessionStore(store); + await fs.promises.mkdir(path.dirname(storePath), { recursive: true }); const json = JSON.stringify(store, null, 2);