Files
openclaw/extensions/slack/src/sent-thread-cache.ts
Peter Steinberger eafe2a8d0b refactor: consolidate duplicated plugin state and doctor migration plumbing onto SDK seams (#99850)
* refactor(plugin-sdk): add createPersistentDedupeCache and migrate channel presence caches

* refactor(matrix): adopt SDK approval reaction target store

* refactor(plugin-sdk): share doctor legacy-state migration fs helpers

* refactor(memory-core): dedupe qmd cache entry envelope validation

* chore(plugin-sdk): pin surface budgets for shared dedupe and doctor helpers

* test(matrix): use future approval expiry fixtures for reaction targets

* test(matrix): use future approval expiry fixtures for reaction targets
2026-07-04 01:51:03 -07:00

92 lines
2.9 KiB
TypeScript

// Slack plugin module implements sent thread cache behavior.
import { createPersistentDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
import { getOptionalSlackRuntime } from "./runtime.js";
/**
* Cache of Slack threads the bot has participated in.
* Used to auto-respond in threads without requiring @mention after the first reply.
*/
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
const MAX_ENTRIES = 5000;
const PERSISTENT_MAX_ENTRIES = 1000;
const PERSISTENT_NAMESPACE = "slack.thread-participation";
type SlackThreadParticipationRecord = {
agentId?: string;
repliedAt: number;
};
/**
* Keep Slack thread participation shared across bundled chunks so thread
* auto-reply gating does not diverge between prepare/dispatch call paths.
*/
const SLACK_THREAD_PARTICIPATION_KEY = Symbol.for("openclaw.slackThreadParticipation");
const threadParticipation = createPersistentDedupeCache<SlackThreadParticipationRecord>({
globalKey: SLACK_THREAD_PARTICIPATION_KEY,
ttlMs: TTL_MS,
maxSize: MAX_ENTRIES,
persistent: {
namespace: PERSISTENT_NAMESPACE,
maxEntries: PERSISTENT_MAX_ENTRIES,
openStore: (options) => getOptionalSlackRuntime()?.state.openKeyedStore(options),
logError: (error) => {
try {
getOptionalSlackRuntime()
?.logging.getChildLogger({ plugin: "slack", feature: "thread-participation-state" })
.warn("Slack persistent thread participation state failed", { error: String(error) });
} catch {
// Best effort only: persistent state must never break Slack message handling.
}
},
},
});
function makeKey(accountId: string, channelId: string, threadTs: string): string {
return `${accountId}:${channelId}:${threadTs}`;
}
export function recordSlackThreadParticipation(
accountId: string,
channelId: string,
threadTs: string,
opts?: { agentId?: string },
): void {
if (!accountId || !channelId || !threadTs) {
return;
}
void threadParticipation.register(makeKey(accountId, channelId, threadTs), {
// Stored for future per-agent thread routing; current reads only need presence.
...(opts?.agentId ? { agentId: opts.agentId } : {}),
repliedAt: Date.now(),
});
}
export function hasSlackThreadParticipation(
accountId: string,
channelId: string,
threadTs: string,
): boolean {
if (!accountId || !channelId || !threadTs) {
return false;
}
return threadParticipation.peek(makeKey(accountId, channelId, threadTs));
}
export async function hasSlackThreadParticipationWithPersistence(params: {
accountId: string;
channelId: string;
threadTs: string;
}): Promise<boolean> {
if (!params.accountId || !params.channelId || !params.threadTs) {
return false;
}
return await threadParticipation.lookup(
makeKey(params.accountId, params.channelId, params.threadTs),
);
}
export function clearSlackThreadParticipationCache(): void {
threadParticipation.clearForTest();
}