diff --git a/extensions/slack/src/monitor/monitor.media.test.ts b/extensions/slack/src/monitor/monitor.media.test.ts index feb9f5871c3..86b6669d3af 100644 --- a/extensions/slack/src/monitor/monitor.media.test.ts +++ b/extensions/slack/src/monitor/monitor.media.test.ts @@ -61,6 +61,45 @@ describe("resolveSlackThreadStarter cache", () => { expect(replies).toHaveBeenCalledTimes(2); }); + it("drops cached thread starters when the current clock is not a valid date timestamp", async () => { + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000); + const { replies, client } = createThreadStarterRepliesClient(); + + const first = await resolveSlackThreadStarter({ + channelId: "C1", + threadTs: "1000.1", + client, + }); + nowSpy.mockReturnValue(Number.NaN); + const second = await resolveSlackThreadStarter({ + channelId: "C1", + threadTs: "1000.1", + client, + }); + + expect(first).toEqual(second); + expect(replies).toHaveBeenCalledTimes(2); + }); + + it("does not cache thread starters when the expiry timestamp would exceed the valid date range", async () => { + vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_000); + const { replies, client } = createThreadStarterRepliesClient(); + + const first = await resolveSlackThreadStarter({ + channelId: "C1", + threadTs: "1000.1", + client, + }); + const second = await resolveSlackThreadStarter({ + channelId: "C1", + threadTs: "1000.1", + client, + }); + + expect(first).toEqual(second); + expect(replies).toHaveBeenCalledTimes(2); + }); + it("does not cache empty starter text", async () => { const { replies, client } = createThreadStarterRepliesClient({ messages: [{ text: " ", user: "U1", ts: "1000.1" }], diff --git a/extensions/slack/src/monitor/thread.ts b/extensions/slack/src/monitor/thread.ts index 9425078cb35..794d41cd3d6 100644 --- a/extensions/slack/src/monitor/thread.ts +++ b/extensions/slack/src/monitor/thread.ts @@ -1,6 +1,10 @@ import type { WebClient as SlackWebClient } from "@slack/web-api"; import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { + asDateTimestampMs, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; import { formatSlackFileReferenceList } from "../file-reference.js"; import type { SlackFile } from "../types.js"; import { logVerbose } from "./thread.runtime.js"; @@ -15,7 +19,7 @@ export type SlackThreadStarter = { type SlackThreadStarterCacheEntry = { value: SlackThreadStarter; - cachedAt: number; + expiresAt: number; }; const THREAD_STARTER_CACHE = new Map(); @@ -23,9 +27,13 @@ const THREAD_STARTER_CACHE_TTL_MS = 6 * 60 * 60_000; const THREAD_STARTER_CACHE_MAX = 2000; function evictThreadStarterCache(): void { - const now = Date.now(); + const now = asDateTimestampMs(Date.now()); + if (now === undefined) { + THREAD_STARTER_CACHE.clear(); + return; + } for (const [cacheKey, entry] of THREAD_STARTER_CACHE.entries()) { - if (now - entry.cachedAt > THREAD_STARTER_CACHE_TTL_MS) { + if (asDateTimestampMs(entry.expiresAt) === undefined || entry.expiresAt <= now) { THREAD_STARTER_CACHE.delete(cacheKey); } } @@ -44,10 +52,11 @@ export async function resolveSlackThreadStarter(params: { evictThreadStarterCache(); const cacheKey = `${params.channelId}:${params.threadTs}`; const cached = THREAD_STARTER_CACHE.get(cacheKey); - if (cached && Date.now() - cached.cachedAt <= THREAD_STARTER_CACHE_TTL_MS) { - return cached.value; - } if (cached) { + const now = asDateTimestampMs(Date.now()); + if (now !== undefined && cached.expiresAt > now) { + return cached.value; + } THREAD_STARTER_CACHE.delete(cacheKey); } try { @@ -78,14 +87,17 @@ export async function resolveSlackThreadStarter(params: { ts: message.ts, files, }; - if (THREAD_STARTER_CACHE.has(cacheKey)) { - THREAD_STARTER_CACHE.delete(cacheKey); + const expiresAt = resolveExpiresAtMsFromDurationMs(THREAD_STARTER_CACHE_TTL_MS); + if (expiresAt !== undefined) { + if (THREAD_STARTER_CACHE.has(cacheKey)) { + THREAD_STARTER_CACHE.delete(cacheKey); + } + THREAD_STARTER_CACHE.set(cacheKey, { + value: starter, + expiresAt, + }); + evictThreadStarterCache(); } - THREAD_STARTER_CACHE.set(cacheKey, { - value: starter, - cachedAt: Date.now(), - }); - evictThreadStarterCache(); return starter; } catch (err) { logVerbose(