* Tests: add fresh module import helper * Process: share command queue runtime state * Agents: share embedded run runtime state * Reply: share followup queue runtime state * Reply: share followup drain callback state * Reply: share queued message dedupe state * Reply: share inbound dedupe state * Tests: cover shared command queue runtime state * Tests: cover shared embedded run runtime state * Tests: cover shared followup queue runtime state * Tests: cover shared inbound dedupe state * Tests: cover shared Slack thread participation state * Slack: share sent thread participation state * Tests: document fresh import helper * Telegram: share draft stream runtime state * Tests: cover shared Telegram draft stream state * Telegram: share sent message cache state * Tests: cover shared Telegram sent message cache * Telegram: share thread binding runtime state * Tests: cover shared Telegram thread binding state * Tests: avoid duplicate shared queue reset * refactor(runtime): centralize global singleton access * refactor(runtime): preserve undefined global singleton values * test(runtime): cover undefined global singleton values --------- Co-authored-by: Nimrod Gutman <nimrod.gutman@gmail.com>
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import { logVerbose, shouldLogVerbose } from "../../globals.js";
|
|
import { createDedupeCache, type DedupeCache } from "../../infra/dedupe.js";
|
|
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
|
|
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
|
|
import type { MsgContext } from "../templating.js";
|
|
|
|
const DEFAULT_INBOUND_DEDUPE_TTL_MS = 20 * 60_000;
|
|
const DEFAULT_INBOUND_DEDUPE_MAX = 5000;
|
|
|
|
/**
|
|
* Keep inbound dedupe shared across bundled chunks so the same provider
|
|
* message cannot bypass dedupe by entering through a different chunk copy.
|
|
*/
|
|
const INBOUND_DEDUPE_CACHE_KEY = Symbol.for("openclaw.inboundDedupeCache");
|
|
|
|
const inboundDedupeCache = resolveGlobalSingleton<DedupeCache>(INBOUND_DEDUPE_CACHE_KEY, () =>
|
|
createDedupeCache({
|
|
ttlMs: DEFAULT_INBOUND_DEDUPE_TTL_MS,
|
|
maxSize: DEFAULT_INBOUND_DEDUPE_MAX,
|
|
}),
|
|
);
|
|
|
|
const normalizeProvider = (value?: string | null) => value?.trim().toLowerCase() || "";
|
|
|
|
const resolveInboundPeerId = (ctx: MsgContext) =>
|
|
ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? ctx.SessionKey;
|
|
|
|
function resolveInboundDedupeSessionScope(ctx: MsgContext): string {
|
|
const sessionKey =
|
|
(ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey : undefined)?.trim() ||
|
|
ctx.SessionKey?.trim() ||
|
|
"";
|
|
if (!sessionKey) {
|
|
return "";
|
|
}
|
|
const parsed = parseAgentSessionKey(sessionKey);
|
|
if (!parsed) {
|
|
return sessionKey;
|
|
}
|
|
// The same physical inbound message should never run twice for the same
|
|
// agent, even if a routing bug presents it under both main and direct keys.
|
|
return `agent:${parsed.agentId}`;
|
|
}
|
|
|
|
export function buildInboundDedupeKey(ctx: MsgContext): string | null {
|
|
const provider = normalizeProvider(ctx.OriginatingChannel ?? ctx.Provider ?? ctx.Surface);
|
|
const messageId = ctx.MessageSid?.trim();
|
|
if (!provider || !messageId) {
|
|
return null;
|
|
}
|
|
const peerId = resolveInboundPeerId(ctx);
|
|
if (!peerId) {
|
|
return null;
|
|
}
|
|
const sessionScope = resolveInboundDedupeSessionScope(ctx);
|
|
const accountId = ctx.AccountId?.trim() ?? "";
|
|
const threadId =
|
|
ctx.MessageThreadId !== undefined && ctx.MessageThreadId !== null
|
|
? String(ctx.MessageThreadId)
|
|
: "";
|
|
return [provider, accountId, sessionScope, peerId, threadId, messageId].filter(Boolean).join("|");
|
|
}
|
|
|
|
export function shouldSkipDuplicateInbound(
|
|
ctx: MsgContext,
|
|
opts?: { cache?: DedupeCache; now?: number },
|
|
): boolean {
|
|
const key = buildInboundDedupeKey(ctx);
|
|
if (!key) {
|
|
return false;
|
|
}
|
|
const cache = opts?.cache ?? inboundDedupeCache;
|
|
const skipped = cache.check(key, opts?.now);
|
|
if (skipped && shouldLogVerbose()) {
|
|
logVerbose(`inbound dedupe: skipped ${key}`);
|
|
}
|
|
return skipped;
|
|
}
|
|
|
|
export function resetInboundDedupe(): void {
|
|
inboundDedupeCache.clear();
|
|
}
|