UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

168 lines (167 loc) 9.6 kB
import { r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { H as buildConversationRef } from "./openclaw-agent-db-maintenance-wTIy-jt-.js"; import { g as runOpenClawAgentWriteTransaction } from "./openclaw-agent-db-CWtDoRbC.js"; import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js"; import { c as readExactSessionEntryRow, g as writeSessionEntry } from "./session-accessor.sqlite-entry-store-BxYl0nro.js"; import { i as getSessionKysely, m as toDatabaseOptions, p as runExclusiveSqliteSessionWrite, s as resolveSqliteReadScope } from "./session-accessor.sqlite-scope-2KfMzb44.js"; import { o as resolveAllAgentSessionStoreTargetsSync } from "./targets-Cknmo7YZ.js"; import { t as runDoctorAgentDatabaseOperation } from "./doctor-agent-database-operation-DzkjHFCH.js"; //#region src/commands/doctor-telegram-general-topic-conversations.ts /** Doctor repair for Telegram General-topic conversation identities written before #113063. */ const GENERAL_TOPIC_ID = "1"; const LEGACY_GENERAL_TARGET = /^telegram:(-?\d+):topic:1$/u; function canonicalIdentity(row) { const targetMatch = LEGACY_GENERAL_TARGET.exec(row.delivery_target); if (row.channel !== "telegram" || row.kind !== "group" || row.thread_id !== GENERAL_TOPIC_ID || row.parent_conversation_id !== null || !targetMatch || row.peer_id !== `${targetMatch[1]}:topic:${GENERAL_TOPIC_ID}`) return null; const peerId = targetMatch[1]; return { conversationId: buildConversationRef({ channel: row.channel, accountId: row.account_id, kind: "group", peerId, threadId: GENERAL_TOPIC_ID }), deliveryTarget: `telegram:${peerId}`, peerId }; } function listLegacyRows(database) { const db = getSessionKysely(database); return executeSqliteQuerySync(database, db.selectFrom("conversations").selectAll().where("channel", "=", "telegram").where("kind", "=", "group").where("thread_id", "=", GENERAL_TOPIC_ID).where("parent_conversation_id", "is", null)).rows.filter((row) => canonicalIdentity(row) !== null); } function resolveRepairScopes(cfg, env) { return resolveAllAgentSessionStoreTargetsSync(cfg, { env }).map((target) => { return { scope: resolveSqliteReadScope({ agentId: target.agentId, env, storePath: target.storePath }), storePath: target.storePath }; }); } /** Finds stale General-topic rows without creating or migrating agent databases. */ function detectTelegramGeneralTopicConversationRepairs(params) { const env = params.env ?? process.env; return resolveRepairScopes(params.cfg, env).flatMap(({ scope, storePath }) => { const databaseOptions = toDatabaseOptions(scope); const inspected = runDoctorAgentDatabaseOperation({ agentId: scope.agentId, path: databaseOptions.path ?? storePath, run: () => withOpenClawAgentDatabaseReadOnly((database) => listLegacyRows(database.db).flatMap((row) => { const canonical = canonicalIdentity(row); return canonical && canonical.conversationId !== row.conversation_id ? [{ agentId: scope.agentId, canonicalConversationId: canonical.conversationId, legacyConversationId: row.conversation_id, storePath }] : []; }), databaseOptions) }); return inspected.ok && inspected.value.found ? inspected.value.value : []; }); } const roleRank = { related: 0, participant: 1, primary: 2 }; function canonicalizeLegacySessionEntry(entry, legacyTarget, canonicalTarget) { const delivery = entry.delivery; if (delivery?.kind !== "external" || delivery.context.channel !== "telegram" || String(delivery.context.threadId) !== GENERAL_TOPIC_ID || delivery.context.to !== legacyTarget) return null; return { ...entry, delivery: { ...delivery, context: { ...delivery.context, to: canonicalTarget }, route: { ...delivery.route, target: { ...delivery.route.target, to: canonicalTarget, ...delivery.route.target?.rawTo === legacyTarget ? { rawTo: canonicalTarget } : {} } }, origin: delivery.origin.to === legacyTarget ? { ...delivery.origin, to: canonicalTarget } : delivery.origin } }; } function repairLegacyRow(database, legacyConversationId) { const db = getSessionKysely(database.db); const legacy = executeSqliteQuerySync(database.db, db.selectFrom("conversations").selectAll().where("conversation_id", "=", legacyConversationId)).rows[0]; const canonical = legacy ? canonicalIdentity(legacy) : null; if (!legacy || !canonical || canonical.conversationId === legacy.conversation_id) return false; const existing = executeSqliteQuerySync(database.db, db.selectFrom("conversations").selectAll().where("conversation_id", "=", canonical.conversationId)).rows[0]; if (existing && (existing.channel !== "telegram" || existing.account_id !== legacy.account_id || existing.kind !== "group" || existing.peer_id !== canonical.peerId || existing.thread_id !== GENERAL_TOPIC_ID || existing.parent_conversation_id !== null)) throw new Error(`canonical Telegram conversation id collision: ${canonical.conversationId}`); const merged = existing ?? legacy; executeSqliteQuerySync(database.db, db.insertInto("conversations").values({ ...merged, conversation_id: canonical.conversationId, peer_id: canonical.peerId, delivery_target: canonical.deliveryTarget, created_at: Math.min(existing?.created_at ?? legacy.created_at, legacy.created_at), updated_at: Math.max(existing?.updated_at ?? legacy.updated_at, legacy.updated_at), native_channel_id: existing?.native_channel_id ?? legacy.native_channel_id, native_direct_user_id: existing?.native_direct_user_id ?? legacy.native_direct_user_id, label: existing?.label ?? legacy.label, metadata_json: existing?.metadata_json ?? legacy.metadata_json }).onConflict((conflict) => conflict.column("conversation_id").doUpdateSet({ created_at: Math.min(existing?.created_at ?? legacy.created_at, legacy.created_at), updated_at: Math.max(existing?.updated_at ?? legacy.updated_at, legacy.updated_at), native_channel_id: existing?.native_channel_id ?? legacy.native_channel_id, native_direct_user_id: existing?.native_direct_user_id ?? legacy.native_direct_user_id, label: existing?.label ?? legacy.label, metadata_json: existing?.metadata_json ?? legacy.metadata_json }))); const boundWindows = executeSqliteQuerySync(database.db, db.selectFrom("session_conversations as sc").innerJoin("session_windows as sw", "sw.session_id", "sc.session_id").select(["sw.session_id", "sw.session_key"]).where("sc.conversation_id", "=", legacy.conversation_id)).rows; for (const window of boundWindows) { const current = readExactSessionEntryRow(database, window.session_key); if (!current || current.entry.sessionId !== window.session_id) continue; const normalized = canonicalizeLegacySessionEntry(current.entry, legacy.delivery_target, canonical.deliveryTarget); if (normalized) writeSessionEntry(database, window.session_key, normalized, { previousEntry: current.entry }); } const conversationIds = [legacy.conversation_id, canonical.conversationId]; const bindings = executeSqliteQuerySync(database.db, db.selectFrom("session_conversations").selectAll().where("conversation_id", "in", conversationIds)).rows; const mergedBindings = /* @__PURE__ */ new Map(); for (const binding of bindings) { const current = mergedBindings.get(binding.session_id); const currentRank = roleRank[current?.role] ?? -1; const selected = (roleRank[binding.role] ?? -1) > currentRank ? binding : current ?? binding; mergedBindings.set(binding.session_id, { ...selected, conversation_id: canonical.conversationId, first_seen_at: Math.min(current?.first_seen_at ?? binding.first_seen_at, binding.first_seen_at), last_seen_at: Math.max(current?.last_seen_at ?? binding.last_seen_at, binding.last_seen_at) }); } executeSqliteQuerySync(database.db, db.deleteFrom("session_conversations").where("conversation_id", "in", conversationIds)); if (mergedBindings.size > 0) executeSqliteQuerySync(database.db, db.insertInto("session_conversations").values([...mergedBindings.values()])); executeSqliteQuerySync(database.db, db.updateTable("session_windows").set({ primary_conversation_id: canonical.conversationId }).where("primary_conversation_id", "=", legacy.conversation_id)); executeSqliteQuerySync(database.db, db.updateTable("conversation_deliveries").set({ conversation_id: canonical.conversationId }).where("conversation_id", "=", legacy.conversation_id)); executeSqliteQuerySync(database.db, db.updateTable("conversations").set({ parent_conversation_id: canonical.conversationId }).where("parent_conversation_id", "=", legacy.conversation_id)); executeSqliteQuerySync(database.db, db.deleteFrom("conversations").where("conversation_id", "=", legacy.conversation_id)); return true; } /** Canonicalizes stale rows and merges every durable reference in one transaction per agent DB. */ async function repairTelegramGeneralTopicConversations(params) { const env = params.env ?? process.env; let repaired = 0; for (const { scope } of resolveRepairScopes(params.cfg, env)) await runExclusiveSqliteSessionWrite(scope, async () => { repaired += runOpenClawAgentWriteTransaction((database) => { let databaseRepairs = 0; for (const row of listLegacyRows(database.db)) if (repairLegacyRow(database, row.conversation_id)) databaseRepairs += 1; return databaseRepairs; }, toDatabaseOptions(scope), { operationLabel: "doctor-telegram-general-topic" }); }); return repaired; } //#endregion export { detectTelegramGeneralTopicConversationRepairs, repairTelegramGeneralTopicConversations };