fix: defer plugin runtime globals until use

This commit is contained in:
Ayaan Zaidi 2026-03-21 11:14:40 +05:30
parent 43513cd1df
commit 8a05c05596
No known key found for this signature in database
6 changed files with 131 additions and 88 deletions

View File

@ -51,16 +51,18 @@ type FeishuThreadBindingsState = {
}; };
const FEISHU_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.feishuThreadBindingsState"); const FEISHU_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.feishuThreadBindingsState");
const state = resolveGlobalSingleton<FeishuThreadBindingsState>( let state: FeishuThreadBindingsState | undefined;
FEISHU_THREAD_BINDINGS_STATE_KEY,
() => ({
managersByAccountId: new Map(),
bindingsByAccountConversation: new Map(),
}),
);
const MANAGERS_BY_ACCOUNT_ID = state.managersByAccountId; function getState(): FeishuThreadBindingsState {
const BINDINGS_BY_ACCOUNT_CONVERSATION = state.bindingsByAccountConversation; state ??= resolveGlobalSingleton<FeishuThreadBindingsState>(
FEISHU_THREAD_BINDINGS_STATE_KEY,
() => ({
managersByAccountId: new Map(),
bindingsByAccountConversation: new Map(),
}),
);
return state;
}
function resolveBindingKey(params: { accountId: string; conversationId: string }): string { function resolveBindingKey(params: { accountId: string; conversationId: string }): string {
return `${params.accountId}:${params.conversationId}`; return `${params.accountId}:${params.conversationId}`;
@ -119,7 +121,7 @@ export function createFeishuThreadBindingManager(params: {
cfg: OpenClawConfig; cfg: OpenClawConfig;
}): FeishuThreadBindingManager { }): FeishuThreadBindingManager {
const accountId = normalizeAccountId(params.accountId); const accountId = normalizeAccountId(params.accountId);
const existing = MANAGERS_BY_ACCOUNT_ID.get(accountId); const existing = getState().managersByAccountId.get(accountId);
if (existing) { if (existing) {
return existing; return existing;
} }
@ -138,9 +140,11 @@ export function createFeishuThreadBindingManager(params: {
const manager: FeishuThreadBindingManager = { const manager: FeishuThreadBindingManager = {
accountId, accountId,
getByConversationId: (conversationId) => getByConversationId: (conversationId) =>
BINDINGS_BY_ACCOUNT_CONVERSATION.get(resolveBindingKey({ accountId, conversationId })), getState().bindingsByAccountConversation.get(
resolveBindingKey({ accountId, conversationId }),
),
listBySessionKey: (targetSessionKey) => listBySessionKey: (targetSessionKey) =>
[...BINDINGS_BY_ACCOUNT_CONVERSATION.values()].filter( [...getState().bindingsByAccountConversation.values()].filter(
(record) => record.accountId === accountId && record.targetSessionKey === targetSessionKey, (record) => record.accountId === accountId && record.targetSessionKey === targetSessionKey,
), ),
bindConversation: ({ bindConversation: ({
@ -184,7 +188,7 @@ export function createFeishuThreadBindingManager(params: {
boundAt: now, boundAt: now,
lastActivityAt: now, lastActivityAt: now,
}; };
BINDINGS_BY_ACCOUNT_CONVERSATION.set( getState().bindingsByAccountConversation.set(
resolveBindingKey({ accountId, conversationId: normalizedConversationId }), resolveBindingKey({ accountId, conversationId: normalizedConversationId }),
record, record,
); );
@ -192,30 +196,30 @@ export function createFeishuThreadBindingManager(params: {
}, },
touchConversation: (conversationId, at = Date.now()) => { touchConversation: (conversationId, at = Date.now()) => {
const key = resolveBindingKey({ accountId, conversationId }); const key = resolveBindingKey({ accountId, conversationId });
const existingRecord = BINDINGS_BY_ACCOUNT_CONVERSATION.get(key); const existingRecord = getState().bindingsByAccountConversation.get(key);
if (!existingRecord) { if (!existingRecord) {
return null; return null;
} }
const updated = { ...existingRecord, lastActivityAt: at }; const updated = { ...existingRecord, lastActivityAt: at };
BINDINGS_BY_ACCOUNT_CONVERSATION.set(key, updated); getState().bindingsByAccountConversation.set(key, updated);
return updated; return updated;
}, },
unbindConversation: (conversationId) => { unbindConversation: (conversationId) => {
const key = resolveBindingKey({ accountId, conversationId }); const key = resolveBindingKey({ accountId, conversationId });
const existingRecord = BINDINGS_BY_ACCOUNT_CONVERSATION.get(key); const existingRecord = getState().bindingsByAccountConversation.get(key);
if (!existingRecord) { if (!existingRecord) {
return null; return null;
} }
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(key); getState().bindingsByAccountConversation.delete(key);
return existingRecord; return existingRecord;
}, },
unbindBySessionKey: (targetSessionKey) => { unbindBySessionKey: (targetSessionKey) => {
const removed: FeishuThreadBindingRecord[] = []; const removed: FeishuThreadBindingRecord[] = [];
for (const record of [...BINDINGS_BY_ACCOUNT_CONVERSATION.values()]) { for (const record of [...getState().bindingsByAccountConversation.values()]) {
if (record.accountId !== accountId || record.targetSessionKey !== targetSessionKey) { if (record.accountId !== accountId || record.targetSessionKey !== targetSessionKey) {
continue; continue;
} }
BINDINGS_BY_ACCOUNT_CONVERSATION.delete( getState().bindingsByAccountConversation.delete(
resolveBindingKey({ accountId, conversationId: record.conversationId }), resolveBindingKey({ accountId, conversationId: record.conversationId }),
); );
removed.push(record); removed.push(record);
@ -223,12 +227,12 @@ export function createFeishuThreadBindingManager(params: {
return removed; return removed;
}, },
stop: () => { stop: () => {
for (const key of [...BINDINGS_BY_ACCOUNT_CONVERSATION.keys()]) { for (const key of [...getState().bindingsByAccountConversation.keys()]) {
if (key.startsWith(`${accountId}:`)) { if (key.startsWith(`${accountId}:`)) {
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(key); getState().bindingsByAccountConversation.delete(key);
} }
} }
MANAGERS_BY_ACCOUNT_ID.delete(accountId); getState().managersByAccountId.delete(accountId);
unregisterSessionBindingAdapter({ channel: "feishu", accountId }); unregisterSessionBindingAdapter({ channel: "feishu", accountId });
}, },
}; };
@ -290,22 +294,22 @@ export function createFeishuThreadBindingManager(params: {
}, },
}); });
MANAGERS_BY_ACCOUNT_ID.set(accountId, manager); getState().managersByAccountId.set(accountId, manager);
return manager; return manager;
} }
export function getFeishuThreadBindingManager( export function getFeishuThreadBindingManager(
accountId?: string, accountId?: string,
): FeishuThreadBindingManager | null { ): FeishuThreadBindingManager | null {
return MANAGERS_BY_ACCOUNT_ID.get(normalizeAccountId(accountId)) ?? null; return getState().managersByAccountId.get(normalizeAccountId(accountId)) ?? null;
} }
export const __testing = { export const __testing = {
resetFeishuThreadBindingsForTests() { resetFeishuThreadBindingsForTests() {
for (const manager of MANAGERS_BY_ACCOUNT_ID.values()) { for (const manager of getState().managersByAccountId.values()) {
manager.stop(); manager.stop();
} }
MANAGERS_BY_ACCOUNT_ID.clear(); getState().managersByAccountId.clear();
BINDINGS_BY_ACCOUNT_CONVERSATION.clear(); getState().bindingsByAccountConversation.clear();
}, },
}; };

View File

@ -15,7 +15,12 @@ const MAX_ENTRIES = 5000;
*/ */
const SLACK_THREAD_PARTICIPATION_KEY = Symbol.for("openclaw.slackThreadParticipation"); const SLACK_THREAD_PARTICIPATION_KEY = Symbol.for("openclaw.slackThreadParticipation");
const threadParticipation = resolveGlobalMap<string, number>(SLACK_THREAD_PARTICIPATION_KEY); let threadParticipation: Map<string, number> | undefined;
function getThreadParticipation(): Map<string, number> {
threadParticipation ??= resolveGlobalMap<string, number>(SLACK_THREAD_PARTICIPATION_KEY);
return threadParticipation;
}
function makeKey(accountId: string, channelId: string, threadTs: string): string { function makeKey(accountId: string, channelId: string, threadTs: string): string {
return `${accountId}:${channelId}:${threadTs}`; return `${accountId}:${channelId}:${threadTs}`;
@ -23,17 +28,17 @@ function makeKey(accountId: string, channelId: string, threadTs: string): string
function evictExpired(): void { function evictExpired(): void {
const now = Date.now(); const now = Date.now();
for (const [key, timestamp] of threadParticipation) { for (const [key, timestamp] of getThreadParticipation()) {
if (now - timestamp > TTL_MS) { if (now - timestamp > TTL_MS) {
threadParticipation.delete(key); getThreadParticipation().delete(key);
} }
} }
} }
function evictOldest(): void { function evictOldest(): void {
const oldest = threadParticipation.keys().next().value; const oldest = getThreadParticipation().keys().next().value;
if (oldest) { if (oldest) {
threadParticipation.delete(oldest); getThreadParticipation().delete(oldest);
} }
} }
@ -45,6 +50,7 @@ export function recordSlackThreadParticipation(
if (!accountId || !channelId || !threadTs) { if (!accountId || !channelId || !threadTs) {
return; return;
} }
const threadParticipation = getThreadParticipation();
if (threadParticipation.size >= MAX_ENTRIES) { if (threadParticipation.size >= MAX_ENTRIES) {
evictExpired(); evictExpired();
} }
@ -63,6 +69,7 @@ export function hasSlackThreadParticipation(
return false; return false;
} }
const key = makeKey(accountId, channelId, threadTs); const key = makeKey(accountId, channelId, threadTs);
const threadParticipation = getThreadParticipation();
const timestamp = threadParticipation.get(key); const timestamp = threadParticipation.get(key);
if (timestamp == null) { if (timestamp == null) {
return false; return false;
@ -75,5 +82,5 @@ export function hasSlackThreadParticipation(
} }
export function clearSlackThreadParticipationCache(): void { export function clearSlackThreadParticipationCache(): void {
threadParticipation.clear(); getThreadParticipation().clear();
} }

View File

@ -28,11 +28,17 @@ type TelegramSendMessageDraft = (
*/ */
const TELEGRAM_DRAFT_STREAM_STATE_KEY = Symbol.for("openclaw.telegramDraftStreamState"); const TELEGRAM_DRAFT_STREAM_STATE_KEY = Symbol.for("openclaw.telegramDraftStreamState");
const draftStreamState = resolveGlobalSingleton(TELEGRAM_DRAFT_STREAM_STATE_KEY, () => ({ let draftStreamState: { nextDraftId: number } | undefined;
nextDraftId: 0,
})); function getDraftStreamState(): { nextDraftId: number } {
draftStreamState ??= resolveGlobalSingleton(TELEGRAM_DRAFT_STREAM_STATE_KEY, () => ({
nextDraftId: 0,
}));
return draftStreamState;
}
function allocateTelegramDraftId(): number { function allocateTelegramDraftId(): number {
const draftStreamState = getDraftStreamState();
draftStreamState.nextDraftId = draftStreamState.nextDraftId =
draftStreamState.nextDraftId >= TELEGRAM_DRAFT_ID_MAX ? 1 : draftStreamState.nextDraftId + 1; draftStreamState.nextDraftId >= TELEGRAM_DRAFT_ID_MAX ? 1 : draftStreamState.nextDraftId + 1;
return draftStreamState.nextDraftId; return draftStreamState.nextDraftId;
@ -454,6 +460,6 @@ export function createTelegramDraftStream(params: {
export const __testing = { export const __testing = {
resetTelegramDraftStreamForTests() { resetTelegramDraftStreamForTests() {
draftStreamState.nextDraftId = 0; getDraftStreamState().nextDraftId = 0;
}, },
}; };

View File

@ -103,17 +103,34 @@ function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
} }
const FILE_EXTENSIONS_PATTERN = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi; const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
const FILE_REFERENCE_PATTERN = new RegExp(
`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`,
"gi",
);
const ORPHANED_TLD_PATTERN = new RegExp(
`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=[^a-zA-Z0-9/]|$)`,
"g",
);
const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi; const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
let fileReferencePattern: RegExp | undefined;
let orphanedTldPattern: RegExp | undefined;
function getFileReferencePattern(): RegExp {
if (fileReferencePattern) {
return fileReferencePattern;
}
const fileExtensionsPattern = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
fileReferencePattern = new RegExp(
`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${fileExtensionsPattern}))(?=$|[^a-zA-Z0-9_\\-/])`,
"gi",
);
return fileReferencePattern;
}
function getOrphanedTldPattern(): RegExp {
if (orphanedTldPattern) {
return orphanedTldPattern;
}
const fileExtensionsPattern = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
orphanedTldPattern = new RegExp(
`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${fileExtensionsPattern}))(?=[^a-zA-Z0-9/]|$)`,
"g",
);
return orphanedTldPattern;
}
function wrapStandaloneFileRef(match: string, prefix: string, filename: string): string { function wrapStandaloneFileRef(match: string, prefix: string, filename: string): string {
if (filename.startsWith("//")) { if (filename.startsWith("//")) {
@ -134,8 +151,8 @@ function wrapSegmentFileRefs(
if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) { if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) {
return text; return text;
} }
const wrappedStandalone = text.replace(FILE_REFERENCE_PATTERN, wrapStandaloneFileRef); const wrappedStandalone = text.replace(getFileReferencePattern(), wrapStandaloneFileRef);
return wrappedStandalone.replace(ORPHANED_TLD_PATTERN, (match, prefix: string, tld: string) => return wrappedStandalone.replace(getOrphanedTldPattern(), (match, prefix: string, tld: string) =>
prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`, prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`,
); );
} }

View File

@ -17,7 +17,12 @@ type CacheEntry = {
*/ */
const TELEGRAM_SENT_MESSAGES_KEY = Symbol.for("openclaw.telegramSentMessages"); const TELEGRAM_SENT_MESSAGES_KEY = Symbol.for("openclaw.telegramSentMessages");
const sentMessages = resolveGlobalMap<string, CacheEntry>(TELEGRAM_SENT_MESSAGES_KEY); let sentMessages: Map<string, CacheEntry> | undefined;
function getSentMessages(): Map<string, CacheEntry> {
sentMessages ??= resolveGlobalMap<string, CacheEntry>(TELEGRAM_SENT_MESSAGES_KEY);
return sentMessages;
}
function getChatKey(chatId: number | string): string { function getChatKey(chatId: number | string): string {
return String(chatId); return String(chatId);
@ -37,6 +42,7 @@ function cleanupExpired(entry: CacheEntry): void {
*/ */
export function recordSentMessage(chatId: number | string, messageId: number): void { export function recordSentMessage(chatId: number | string, messageId: number): void {
const key = getChatKey(chatId); const key = getChatKey(chatId);
const sentMessages = getSentMessages();
let entry = sentMessages.get(key); let entry = sentMessages.get(key);
if (!entry) { if (!entry) {
entry = { timestamps: new Map() }; entry = { timestamps: new Map() };
@ -54,7 +60,7 @@ export function recordSentMessage(chatId: number | string, messageId: number): v
*/ */
export function wasSentByBot(chatId: number | string, messageId: number): boolean { export function wasSentByBot(chatId: number | string, messageId: number): boolean {
const key = getChatKey(chatId); const key = getChatKey(chatId);
const entry = sentMessages.get(key); const entry = getSentMessages().get(key);
if (!entry) { if (!entry) {
return false; return false;
} }
@ -67,5 +73,5 @@ export function wasSentByBot(chatId: number | string, messageId: number): boolea
* Clear all cached entries (for testing). * Clear all cached entries (for testing).
*/ */
export function clearSentMessageCache(): void { export function clearSentMessageCache(): void {
sentMessages.clear(); getSentMessages().clear();
} }

View File

@ -77,17 +77,19 @@ type TelegramThreadBindingsState = {
*/ */
const TELEGRAM_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.telegramThreadBindingsState"); const TELEGRAM_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.telegramThreadBindingsState");
const threadBindingsState = resolveGlobalSingleton<TelegramThreadBindingsState>( let threadBindingsState: TelegramThreadBindingsState | undefined;
TELEGRAM_THREAD_BINDINGS_STATE_KEY,
() => ({ function getThreadBindingsState(): TelegramThreadBindingsState {
managersByAccountId: new Map<string, TelegramThreadBindingManager>(), threadBindingsState ??= resolveGlobalSingleton<TelegramThreadBindingsState>(
bindingsByAccountConversation: new Map<string, TelegramThreadBindingRecord>(), TELEGRAM_THREAD_BINDINGS_STATE_KEY,
persistQueueByAccountId: new Map<string, Promise<void>>(), () => ({
}), managersByAccountId: new Map<string, TelegramThreadBindingManager>(),
); bindingsByAccountConversation: new Map<string, TelegramThreadBindingRecord>(),
const MANAGERS_BY_ACCOUNT_ID = threadBindingsState.managersByAccountId; persistQueueByAccountId: new Map<string, Promise<void>>(),
const BINDINGS_BY_ACCOUNT_CONVERSATION = threadBindingsState.bindingsByAccountConversation; }),
const PERSIST_QUEUE_BY_ACCOUNT_ID = threadBindingsState.persistQueueByAccountId; );
return threadBindingsState;
}
function normalizeDurationMs(raw: unknown, fallback: number): number { function normalizeDurationMs(raw: unknown, fallback: number): number {
if (typeof raw !== "number" || !Number.isFinite(raw)) { if (typeof raw !== "number" || !Number.isFinite(raw)) {
@ -168,7 +170,7 @@ function fromSessionBindingInput(params: {
}): TelegramThreadBindingRecord { }): TelegramThreadBindingRecord {
const now = Date.now(); const now = Date.now();
const metadata = params.input.metadata ?? {}; const metadata = params.input.metadata ?? {};
const existing = BINDINGS_BY_ACCOUNT_CONVERSATION.get( const existing = getThreadBindingsState().bindingsByAccountConversation.get(
resolveBindingKey({ resolveBindingKey({
accountId: params.accountId, accountId: params.accountId,
conversationId: params.input.conversationId, conversationId: params.input.conversationId,
@ -310,7 +312,7 @@ async function persistBindingsToDisk(params: {
version: STORE_VERSION, version: STORE_VERSION,
bindings: bindings:
params.bindings ?? params.bindings ??
[...BINDINGS_BY_ACCOUNT_CONVERSATION.values()].filter( [...getThreadBindingsState().bindingsByAccountConversation.values()].filter(
(entry) => entry.accountId === params.accountId, (entry) => entry.accountId === params.accountId,
), ),
}; };
@ -322,7 +324,7 @@ async function persistBindingsToDisk(params: {
} }
function listBindingsForAccount(accountId: string): TelegramThreadBindingRecord[] { function listBindingsForAccount(accountId: string): TelegramThreadBindingRecord[] {
return [...BINDINGS_BY_ACCOUNT_CONVERSATION.values()].filter( return [...getThreadBindingsState().bindingsByAccountConversation.values()].filter(
(entry) => entry.accountId === accountId, (entry) => entry.accountId === accountId,
); );
} }
@ -335,16 +337,17 @@ function enqueuePersistBindings(params: {
if (!params.persist) { if (!params.persist) {
return Promise.resolve(); return Promise.resolve();
} }
const previous = PERSIST_QUEUE_BY_ACCOUNT_ID.get(params.accountId) ?? Promise.resolve(); const previous =
getThreadBindingsState().persistQueueByAccountId.get(params.accountId) ?? Promise.resolve();
const next = previous const next = previous
.catch(() => undefined) .catch(() => undefined)
.then(async () => { .then(async () => {
await persistBindingsToDisk(params); await persistBindingsToDisk(params);
}); });
PERSIST_QUEUE_BY_ACCOUNT_ID.set(params.accountId, next); getThreadBindingsState().persistQueueByAccountId.set(params.accountId, next);
void next.finally(() => { void next.finally(() => {
if (PERSIST_QUEUE_BY_ACCOUNT_ID.get(params.accountId) === next) { if (getThreadBindingsState().persistQueueByAccountId.get(params.accountId) === next) {
PERSIST_QUEUE_BY_ACCOUNT_ID.delete(params.accountId); getThreadBindingsState().persistQueueByAccountId.delete(params.accountId);
} }
}); });
return next; return next;
@ -412,7 +415,7 @@ export function createTelegramThreadBindingManager(
} = {}, } = {},
): TelegramThreadBindingManager { ): TelegramThreadBindingManager {
const accountId = normalizeAccountId(params.accountId); const accountId = normalizeAccountId(params.accountId);
const existing = MANAGERS_BY_ACCOUNT_ID.get(accountId); const existing = getThreadBindingsState().managersByAccountId.get(accountId);
if (existing) { if (existing) {
return existing; return existing;
} }
@ -430,7 +433,7 @@ export function createTelegramThreadBindingManager(
accountId, accountId,
conversationId: entry.conversationId, conversationId: entry.conversationId,
}); });
BINDINGS_BY_ACCOUNT_CONVERSATION.set(key, { getThreadBindingsState().bindingsByAccountConversation.set(key, {
...entry, ...entry,
accountId, accountId,
}); });
@ -448,7 +451,7 @@ export function createTelegramThreadBindingManager(
if (!conversationId) { if (!conversationId) {
return undefined; return undefined;
} }
return BINDINGS_BY_ACCOUNT_CONVERSATION.get( return getThreadBindingsState().bindingsByAccountConversation.get(
resolveBindingKey({ resolveBindingKey({
accountId, accountId,
conversationId, conversationId,
@ -471,7 +474,7 @@ export function createTelegramThreadBindingManager(
return null; return null;
} }
const key = resolveBindingKey({ accountId, conversationId }); const key = resolveBindingKey({ accountId, conversationId });
const existing = BINDINGS_BY_ACCOUNT_CONVERSATION.get(key); const existing = getThreadBindingsState().bindingsByAccountConversation.get(key);
if (!existing) { if (!existing) {
return null; return null;
} }
@ -479,7 +482,7 @@ export function createTelegramThreadBindingManager(
...existing, ...existing,
lastActivityAt: normalizeTimestampMs(at ?? Date.now()), lastActivityAt: normalizeTimestampMs(at ?? Date.now()),
}; };
BINDINGS_BY_ACCOUNT_CONVERSATION.set(key, nextRecord); getThreadBindingsState().bindingsByAccountConversation.set(key, nextRecord);
persistBindingsSafely({ persistBindingsSafely({
accountId, accountId,
persist: manager.shouldPersistMutations(), persist: manager.shouldPersistMutations(),
@ -494,11 +497,11 @@ export function createTelegramThreadBindingManager(
return null; return null;
} }
const key = resolveBindingKey({ accountId, conversationId }); const key = resolveBindingKey({ accountId, conversationId });
const removed = BINDINGS_BY_ACCOUNT_CONVERSATION.get(key) ?? null; const removed = getThreadBindingsState().bindingsByAccountConversation.get(key) ?? null;
if (!removed) { if (!removed) {
return null; return null;
} }
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(key); getThreadBindingsState().bindingsByAccountConversation.delete(key);
persistBindingsSafely({ persistBindingsSafely({
accountId, accountId,
persist: manager.shouldPersistMutations(), persist: manager.shouldPersistMutations(),
@ -521,7 +524,7 @@ export function createTelegramThreadBindingManager(
accountId, accountId,
conversationId: entry.conversationId, conversationId: entry.conversationId,
}); });
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(key); getThreadBindingsState().bindingsByAccountConversation.delete(key);
removed.push(entry); removed.push(entry);
} }
if (removed.length > 0) { if (removed.length > 0) {
@ -540,9 +543,9 @@ export function createTelegramThreadBindingManager(
sweepTimer = null; sweepTimer = null;
} }
unregisterSessionBindingAdapter({ channel: "telegram", accountId }); unregisterSessionBindingAdapter({ channel: "telegram", accountId });
const existingManager = MANAGERS_BY_ACCOUNT_ID.get(accountId); const existingManager = getThreadBindingsState().managersByAccountId.get(accountId);
if (existingManager === manager) { if (existingManager === manager) {
MANAGERS_BY_ACCOUNT_ID.delete(accountId); getThreadBindingsState().managersByAccountId.delete(accountId);
} }
}, },
}; };
@ -574,7 +577,7 @@ export function createTelegramThreadBindingManager(
metadata: input.metadata, metadata: input.metadata,
}, },
}); });
BINDINGS_BY_ACCOUNT_CONVERSATION.set( getThreadBindingsState().bindingsByAccountConversation.set(
resolveBindingKey({ accountId, conversationId }), resolveBindingKey({ accountId, conversationId }),
record, record,
); );
@ -714,14 +717,14 @@ export function createTelegramThreadBindingManager(
sweepTimer.unref?.(); sweepTimer.unref?.();
} }
MANAGERS_BY_ACCOUNT_ID.set(accountId, manager); getThreadBindingsState().managersByAccountId.set(accountId, manager);
return manager; return manager;
} }
export function getTelegramThreadBindingManager( export function getTelegramThreadBindingManager(
accountId?: string, accountId?: string,
): TelegramThreadBindingManager | null { ): TelegramThreadBindingManager | null {
return MANAGERS_BY_ACCOUNT_ID.get(normalizeAccountId(accountId)) ?? null; return getThreadBindingsState().managersByAccountId.get(normalizeAccountId(accountId)) ?? null;
} }
function updateTelegramBindingsBySessionKey(params: { function updateTelegramBindingsBySessionKey(params: {
@ -741,7 +744,7 @@ function updateTelegramBindingsBySessionKey(params: {
conversationId: entry.conversationId, conversationId: entry.conversationId,
}); });
const next = params.update(entry, now); const next = params.update(entry, now);
BINDINGS_BY_ACCOUNT_CONVERSATION.set(key, next); getThreadBindingsState().bindingsByAccountConversation.set(key, next);
updated.push(next); updated.push(next);
} }
if (updated.length > 0) { if (updated.length > 0) {
@ -799,12 +802,12 @@ export function setTelegramThreadBindingMaxAgeBySessionKey(params: {
export const __testing = { export const __testing = {
async resetTelegramThreadBindingsForTests() { async resetTelegramThreadBindingsForTests() {
for (const manager of MANAGERS_BY_ACCOUNT_ID.values()) { for (const manager of getThreadBindingsState().managersByAccountId.values()) {
manager.stop(); manager.stop();
} }
await Promise.allSettled(PERSIST_QUEUE_BY_ACCOUNT_ID.values()); await Promise.allSettled(getThreadBindingsState().persistQueueByAccountId.values());
PERSIST_QUEUE_BY_ACCOUNT_ID.clear(); getThreadBindingsState().persistQueueByAccountId.clear();
MANAGERS_BY_ACCOUNT_ID.clear(); getThreadBindingsState().managersByAccountId.clear();
BINDINGS_BY_ACCOUNT_CONVERSATION.clear(); getThreadBindingsState().bindingsByAccountConversation.clear();
}, },
}; };