Compare commits
3 Commits
packager-r
...
fix/discor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf690507bf | ||
|
|
4e6330e2ab | ||
|
|
f6be7038b5 |
@@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents: prevent file descriptor leaks in child process cleanup. (#13565) Thanks @KyleChen26.
|
||||
- Agents: prevent double compaction caused by cache TTL bypassing guard. (#13514) Thanks @taw0002.
|
||||
- Agents: use last API call's cache tokens for context display instead of accumulated sum. (#13805) Thanks @akari-musubi.
|
||||
- Discord: add TTL/LRU eviction to the thread starter cache to prevent unbounded growth. (#5274) Thanks @webvijayi.
|
||||
- Discord tests: use a partial @buape/carbon mock in slash command coverage. (#13262) Thanks @arosstale.
|
||||
- Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik.
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Client } from "@buape/carbon";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ChannelType, type Client } from "@buape/carbon";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildAgentSessionKey } from "../../routing/resolve-route.js";
|
||||
import {
|
||||
__resetDiscordThreadStarterCacheForTest,
|
||||
resolveDiscordAutoThreadContext,
|
||||
resolveDiscordAutoThreadReplyPlan,
|
||||
resolveDiscordReplyDeliveryPlan,
|
||||
resolveDiscordThreadStarter,
|
||||
} from "./threading.js";
|
||||
|
||||
describe("resolveDiscordAutoThreadContext", () => {
|
||||
@@ -142,3 +144,45 @@ describe("resolveDiscordAutoThreadReplyPlan", () => {
|
||||
expect(plan.autoThreadContext).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDiscordThreadStarter cache", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
__resetDiscordThreadStarterCacheForTest();
|
||||
});
|
||||
|
||||
it("expires cached entries after TTL", async () => {
|
||||
vi.useFakeTimers();
|
||||
const baseTime = new Date("2026-02-12T00:00:00Z").getTime();
|
||||
vi.setSystemTime(baseTime);
|
||||
|
||||
const restGet = vi.fn(async () => ({
|
||||
content: "starter",
|
||||
author: { username: "starter", id: "user-1" },
|
||||
timestamp: "2026-02-12T00:00:00Z",
|
||||
}));
|
||||
const client = { rest: { get: restGet } } as unknown as Client;
|
||||
|
||||
const params = {
|
||||
channel: { id: "thread-1" },
|
||||
client,
|
||||
parentId: "parent-1",
|
||||
parentType: ChannelType.GuildText,
|
||||
resolveTimestampMs: () => baseTime,
|
||||
};
|
||||
|
||||
const first = await resolveDiscordThreadStarter(params);
|
||||
expect(first?.text).toBe("starter");
|
||||
expect(restGet).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.setSystemTime(baseTime + 60_000);
|
||||
const second = await resolveDiscordThreadStarter(params);
|
||||
expect(second).toEqual(first);
|
||||
expect(restGet).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.setSystemTime(baseTime + 60_000 + 5 * 60_000 + 1);
|
||||
const third = await resolveDiscordThreadStarter(params);
|
||||
expect(third).toEqual(first);
|
||||
expect(restGet).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,12 +29,54 @@ type DiscordThreadParentInfo = {
|
||||
type?: ChannelType;
|
||||
};
|
||||
|
||||
const DISCORD_THREAD_STARTER_CACHE = new Map<string, DiscordThreadStarter>();
|
||||
// Cache entry with timestamp for TTL-based eviction
|
||||
type DiscordThreadStarterCacheEntry = {
|
||||
value: DiscordThreadStarter;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
// Cache configuration: 5 minute TTL (thread starters rarely change), max 500 entries
|
||||
const DISCORD_THREAD_STARTER_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const DISCORD_THREAD_STARTER_CACHE_MAX = 500;
|
||||
|
||||
const DISCORD_THREAD_STARTER_CACHE = new Map<string, DiscordThreadStarterCacheEntry>();
|
||||
|
||||
export function __resetDiscordThreadStarterCacheForTest() {
|
||||
DISCORD_THREAD_STARTER_CACHE.clear();
|
||||
}
|
||||
|
||||
// Get cached entry with TTL check, refresh LRU position on hit
|
||||
function getCachedThreadStarter(key: string, now: number): DiscordThreadStarter | undefined {
|
||||
const entry = DISCORD_THREAD_STARTER_CACHE.get(key);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
// Check TTL expiry
|
||||
if (now - entry.updatedAt > DISCORD_THREAD_STARTER_CACHE_TTL_MS) {
|
||||
DISCORD_THREAD_STARTER_CACHE.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
// Refresh LRU position by re-inserting (Map maintains insertion order)
|
||||
DISCORD_THREAD_STARTER_CACHE.delete(key);
|
||||
DISCORD_THREAD_STARTER_CACHE.set(key, { ...entry, updatedAt: now });
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
// Set cached entry with LRU eviction when max size exceeded
|
||||
function setCachedThreadStarter(key: string, value: DiscordThreadStarter, now: number): void {
|
||||
// Remove existing entry first (to update LRU position)
|
||||
DISCORD_THREAD_STARTER_CACHE.delete(key);
|
||||
DISCORD_THREAD_STARTER_CACHE.set(key, { value, updatedAt: now });
|
||||
// Evict oldest entries (first in Map) when over max size
|
||||
while (DISCORD_THREAD_STARTER_CACHE.size > DISCORD_THREAD_STARTER_CACHE_MAX) {
|
||||
const iter = DISCORD_THREAD_STARTER_CACHE.keys().next();
|
||||
if (iter.done) {
|
||||
break;
|
||||
}
|
||||
DISCORD_THREAD_STARTER_CACHE.delete(iter.value);
|
||||
}
|
||||
}
|
||||
|
||||
function isDiscordThreadType(type: ChannelType | undefined): boolean {
|
||||
return (
|
||||
type === ChannelType.PublicThread ||
|
||||
@@ -100,7 +142,8 @@ export async function resolveDiscordThreadStarter(params: {
|
||||
resolveTimestampMs: (value?: string | null) => number | undefined;
|
||||
}): Promise<DiscordThreadStarter | null> {
|
||||
const cacheKey = params.channel.id;
|
||||
const cached = DISCORD_THREAD_STARTER_CACHE.get(cacheKey);
|
||||
const now = Date.now();
|
||||
const cached = getCachedThreadStarter(cacheKey, now);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
@@ -146,7 +189,7 @@ export async function resolveDiscordThreadStarter(params: {
|
||||
author,
|
||||
timestamp: timestamp ?? undefined,
|
||||
};
|
||||
DISCORD_THREAD_STARTER_CACHE.set(cacheKey, payload);
|
||||
setCachedThreadStarter(cacheKey, payload, Date.now());
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user