openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
243 lines (242 loc) • 17 kB
JavaScript
import { y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { g as runOpenClawAgentWriteTransaction, p as openOpenClawAgentDatabase } from "./openclaw-agent-db-CWtDoRbC.js";
import { C as copySessionNodeArtifactsForRepair, S as collectSessionStateIdsForEntry, T as deleteSessionMembersForRepair, b as bindSessionWindowEntryProjection } from "./session-accessor.sqlite-entry-store-BxYl0nro.js";
import { a as normalizeStoreSessionKey } from "./store-entry-CzRELcpv.js";
import { i as getSessionKysely, l as resolveSqliteStoreScope, m as toDatabaseOptions, p as runExclusiveSqliteSessionWrite } from "./session-accessor.sqlite-scope-2KfMzb44.js";
import { S as parseSessionEntryJson, m as canonicalSessionKeyMigrationRequiredError, r as ensureTranscriptGenerationInTransaction, v as publishSessionEntryCacheInvalidation } from "./session-accessor.sqlite-transcript-state-DwF2owZS.js";
import { f as readSessionGenerationIdsForKeys } from "./session-accessor.sqlite-lifecycle-state-BTh4yZ7R.js";
import { a as deleteSessionTranscriptIndexInTransaction, c as reconcileSessionTranscriptIndexInTransaction } from "./session-transcript-index-DmCW_GtW.js";
//#region src/config/sessions/session-accessor.sqlite-canonical-repair.ts
function resolveSqliteCanonicalRepairLookupKeys(canonicalKey, storedKeys) {
return uniqueStrings([
canonicalKey,
...storedKeys,
...storedKeys.flatMap((key) => {
const trimmedKey = key.trim();
return [trimmedKey, normalizeStoreSessionKey(trimmedKey)];
})
]);
}
/** Doctor probes only the exact staged target and may replace a malformed partial row. */
function readExactSessionEntryRowForCanonicalRepair(database, sessionKey, options = {}) {
const db = getSessionKysely(database.db);
const row = executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("session_nodes").selectAll().where("session_key", "=", sessionKey));
if (!row) return;
if (row.entry_json === "{}") {
if (executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("session_windows").select("session_id").where("session_id", "=", row.current_session_id).where("session_key", "=", row.session_key))) return;
}
const parsedEntry = parseSessionEntryJson(row);
if (!parsedEntry && !options.allowMalformedRowRepair) throw canonicalSessionKeyMigrationRequiredError(`invalid persisted session row requires repair for ${sessionKey}`);
return {
entry: parsedEntry ?? {
sessionId: row.current_session_id,
updatedAt: row.updated_at
},
row
};
}
/** Doctor-only cross-store copy; the source node remains until lifecycle archival succeeds. */
function copySqliteSessionOwnedStateForCanonicalRepair(params) {
const source = resolveSqliteStoreScope(params.source.storePath, { agentId: params.source.agentId });
const sourceDatabase = openOpenClawAgentDatabase(toDatabaseOptions(source));
copySqliteSessionOwnedStateForRepair({
canonicalKey: params.canonicalKey,
destination: params.destinationDatabase,
...params.preferredEntry ? { preferredEntry: params.preferredEntry } : {},
...params.preferredSessionKey ? { preferredSessionKey: params.preferredSessionKey } : {},
source: sourceDatabase,
sourceEntries: params.sourceEntries,
sourceKeys: params.sourceKeys
});
}
/** Doctor-only inventory of every generation copied for one canonical-key group. */
function listSqliteSessionGenerationIdsForCanonicalRepair(params) {
const source = resolveSqliteStoreScope(params.storePath, { agentId: params.agentId });
const database = openOpenClawAgentDatabase(toDatabaseOptions(source));
return readSessionGenerationIdsForKeys(database, uniqueStrings(params.sourceKeys), { exactStoredKeys: true });
}
/** Doctor-only normalization of imported transcript rows before copy or archival. */
async function ensureSqliteTranscriptGenerationsForCanonicalRepair(sources) {
const byDatabase = /* @__PURE__ */ new Map();
for (const source of sources) {
const resolved = resolveSqliteStoreScope(source.storePath, { agentId: source.agentId });
const key = `${resolved.path ?? source.storePath}\0${resolved.databaseAgentId ?? resolved.agentId}`;
const grouped = byDatabase.get(key) ?? {
resolved,
sources: []
};
byDatabase.set(key, {
...grouped,
sources: [...grouped.sources, source]
});
}
for (const group of byDatabase.values()) await runExclusiveSqliteSessionWrite(group.resolved, async () => {
runOpenClawAgentWriteTransaction((database) => {
const sessionIds = uniqueStrings([...group.sources.flatMap((source) => [...collectSessionStateIdsForEntry(source.entry)]), ...readSessionGenerationIdsForKeys(database, group.sources.map((source) => source.sessionKey), { exactStoredKeys: true })]);
const db = getSessionKysely(database.db);
const eventSessionIds = executeSqliteQuerySync(database.db, db.selectFrom("transcript_events").select("session_id").where("session_id", "in", sessionIds).groupBy("session_id")).rows;
for (const row of eventSessionIds) ensureTranscriptGenerationInTransaction(database, row.session_id);
}, toDatabaseOptions(group.resolved));
});
}
/** Doctor-only same-store rewrite for delivery attribution owned by removed aliases. */
function rehomeSqliteSessionDeliveryReferencesForCanonicalRepair(database, canonicalKey, previousKeys) {
rehomeSqliteSessionDeliveryReferencesForCanonicalRepairBatch(database, [{
canonicalKey,
previousKeys
}]);
}
/** Doctor-only batched delivery rewrite with one session identity inventory per database. */
function rehomeSqliteSessionDeliveryReferencesForCanonicalRepairBatch(database, repairs) {
if (repairs.length === 0) return;
const db = getSessionKysely(database.db);
const storedSessionKeys = executeSqliteQuerySync(database.db, db.selectFrom("session_nodes").select("session_key")).rows.map((row) => row.session_key);
const storedSessionKeySet = new Set(storedSessionKeys);
const identityCounts = /* @__PURE__ */ new Map();
for (const sessionKey of storedSessionKeys) {
const identity = normalizeStoreSessionKey(sessionKey.trim());
identityCounts.set(identity, (identityCounts.get(identity) ?? 0) + 1);
}
for (const repair of repairs) {
const ownedKeys = /* @__PURE__ */ new Set([repair.canonicalKey, ...repair.previousKeys]);
const ownedIdentityCounts = /* @__PURE__ */ new Map();
for (const sessionKey of ownedKeys) {
if (!storedSessionKeySet.has(sessionKey)) continue;
const identity = normalizeStoreSessionKey(sessionKey.trim());
ownedIdentityCounts.set(identity, (ownedIdentityCounts.get(identity) ?? 0) + 1);
}
const aliases = resolveSqliteCanonicalRepairLookupKeys(repair.canonicalKey, repair.previousKeys).filter((key) => {
if (key === repair.canonicalKey) return false;
if (ownedKeys.has(key)) return true;
const identity = normalizeStoreSessionKey(key.trim());
return (identityCounts.get(identity) ?? 0) <= (ownedIdentityCounts.get(identity) ?? 0);
});
if (aliases.length === 0) continue;
executeSqliteQuerySync(database.db, db.updateTable("conversation_deliveries").set({ source_session_key: repair.canonicalKey }).where("source_session_key", "in", aliases));
}
}
function copySqliteSessionOwnedStateForRepair(params) {
const storedSourceKeys = uniqueStrings(params.sourceKeys.filter((key) => key.length > 0));
if (storedSourceKeys.length === 0) return;
const sourceKeys = storedSourceKeys;
const sourceDb = getSessionKysely(params.source.db);
const destinationDb = getSessionKysely(params.destination.db);
const entrySessionIds = uniqueStrings(params.sourceEntries.flatMap((entry) => [...collectSessionStateIdsForEntry(entry)]));
const windows = executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("session_windows").selectAll().where((eb) => entrySessionIds.length === 0 ? eb("session_key", "in", sourceKeys) : eb.or([eb("session_key", "in", sourceKeys), eb("session_id", "in", entrySessionIds)]))).rows;
const sessionIds = uniqueStrings([...windows.map((row) => row.session_id), ...entrySessionIds]);
const sessionLinks = sessionIds.length === 0 ? [] : executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("session_conversations").selectAll().where("session_id", "in", sessionIds)).rows;
const linkedConversationIds = uniqueStrings([...windows.flatMap((row) => row.primary_conversation_id ? [row.primary_conversation_id] : []), ...sessionLinks.map((row) => row.conversation_id)]);
const sourceKeyReferences = new Set(sourceKeys);
const sourceLineageIdentities = new Set(sourceKeys.map((key) => normalizeStoreSessionKey(key.trim())));
const deliveryLookupKeys = resolveSqliteCanonicalRepairLookupKeys(params.canonicalKey, sourceKeys);
const competingDeliveryIdentities = new Set(executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("session_nodes").select("session_key")).rows.flatMap((row) => sourceKeyReferences.has(row.session_key) ? [] : [normalizeStoreSessionKey(row.session_key.trim())]));
const deliverySourceKeys = deliveryLookupKeys.filter((key) => sourceKeyReferences.has(key) || !competingDeliveryIdentities.has(normalizeStoreSessionKey(key.trim())));
const deliverySourceKeyReferences = new Set(deliverySourceKeys);
const deliveries = executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("conversation_deliveries").selectAll().where("source_session_key", "in", deliverySourceKeys)).rows;
const conversationIds = uniqueStrings([...linkedConversationIds, ...deliveries.map((delivery) => delivery.conversation_id)]);
if (conversationIds.length > 0) {
const conversations = executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("conversations").selectAll().where("conversation_id", "in", conversationIds)).rows;
for (const conversation of conversations) {
const { conversation_id: _conversationId, ...replacement } = conversation;
executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("conversations").values(conversation).onConflict((conflict) => conflict.column("conversation_id").doUpdateSet(replacement)));
}
for (const delivery of deliveries) {
const canonicalDelivery = {
...delivery,
source_session_key: delivery.source_session_key !== null && deliverySourceKeyReferences.has(delivery.source_session_key) ? params.canonicalKey : delivery.source_session_key
};
const { operation_id: _operationId, ...replacement } = canonicalDelivery;
executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("conversation_deliveries").values(canonicalDelivery).onConflict((conflict) => conflict.column("operation_id").doUpdateSet(replacement)));
}
}
const preferredWindowProjection = params.preferredEntry ? bindSessionWindowEntryProjection({
entry: params.preferredEntry,
sessionKey: params.canonicalKey
}) : void 0;
const preferredWindowProvenance = params.preferredEntry ? executeSqliteQueryTakeFirstSync(params.destination.db, destinationDb.selectFrom("session_windows").select([
"session_entry_provenance",
"acp_owned",
"plugin_owner_id",
"hook_external_content_source"
]).where("session_id", "=", params.preferredEntry.sessionId)) : void 0;
for (const window of windows) {
const canonicalWindow = {
...window,
session_key: params.canonicalKey,
parent_session_key: window.parent_session_key && sourceLineageIdentities.has(normalizeStoreSessionKey(window.parent_session_key.trim())) ? params.canonicalKey : window.parent_session_key,
spawned_by: window.spawned_by && sourceLineageIdentities.has(normalizeStoreSessionKey(window.spawned_by.trim())) ? params.canonicalKey : window.spawned_by,
...preferredWindowProjection && window.session_id === params.preferredEntry?.sessionId ? {
...preferredWindowProjection,
...preferredWindowProvenance
} : {}
};
const { session_id: _sessionId, ...replacement } = { ...canonicalWindow };
executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("session_windows").values(canonicalWindow).onConflict((conflict) => conflict.column("session_id").doUpdateSet(replacement)));
}
const copiedWindowIds = new Set(windows.map((row) => row.session_id));
for (const sessionId of entrySessionIds) {
if (copiedWindowIds.has(sessionId)) continue;
const entry = (params.preferredEntry?.sessionId === sessionId ? params.preferredEntry : void 0) ?? params.sourceEntries.find((candidate) => candidate.sessionId === sessionId) ?? params.sourceEntries.find((candidate) => new Set(collectSessionStateIdsForEntry(candidate)).has(sessionId));
const updatedAt = entry?.updatedAt ?? Date.now();
const recoveryWindow = {
session_key: params.canonicalKey,
previous_session_id: entry?.sessionId === sessionId ? entry.previousSessionId ?? null : null,
reason: "recovery",
session_scope: "conversation",
created_at: updatedAt,
updated_at: updatedAt
};
executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("session_windows").values({
session_id: sessionId,
...recoveryWindow
}).onConflict((conflict) => conflict.column("session_id").doUpdateSet({ session_key: params.canonicalKey })));
}
for (const link of sessionLinks) executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("session_conversations").values(link).onConflict((conflict) => conflict.columns([
"session_id",
"conversation_id",
"role"
]).doUpdateSet({
first_seen_at: link.first_seen_at,
last_seen_at: link.last_seen_at
})));
for (const sessionId of sessionIds) {
if (!copySqliteSessionGenerationRows({
destination: params.destination,
sessionId,
source: params.source,
sourceWindowPresent: copiedWindowIds.has(sessionId)
})) continue;
deleteSessionTranscriptIndexInTransaction(params.destination.db, sessionId);
reconcileSessionTranscriptIndexInTransaction(params.destination.db, sessionId);
publishSessionEntryCacheInvalidation(params.destination);
}
deleteSessionMembersForRepair(params.destination, params.canonicalKey);
copySessionNodeArtifactsForRepair(params.source, params.destination, sourceKeys, params.canonicalKey, { includeMembers: false });
copySessionNodeArtifactsForRepair(params.source, params.destination, params.preferredSessionKey ? [params.preferredSessionKey] : sourceKeys, params.canonicalKey, { includeParticipants: false });
}
function copySqliteSessionGenerationRows(params) {
const sourceDb = getSessionKysely(params.source.db);
const destinationDb = getSessionKysely(params.destination.db);
const transcriptEvents = executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("transcript_events").selectAll().where("session_id", "=", params.sessionId)).rows;
const transcriptIdentities = executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("transcript_event_identities").selectAll().where("session_id", "=", params.sessionId)).rows;
const rewriteWatermarks = executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("transcript_rewrite_watermarks").selectAll().where("session_id", "=", params.sessionId)).rows;
const trajectoryEvents = executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("trajectory_runtime_events").selectAll().where("session_id", "=", params.sessionId)).rows;
const parentStreamEvents = executeSqliteQuerySync(params.source.db, sourceDb.selectFrom("acp_parent_stream_events").selectAll().where("session_id", "=", params.sessionId)).rows;
if (!params.sourceWindowPresent && transcriptEvents.length === 0 && transcriptIdentities.length === 0 && rewriteWatermarks.length === 0 && trajectoryEvents.length === 0 && parentStreamEvents.length === 0) return false;
for (const table of [
"transcript_event_identities",
"transcript_events",
"transcript_rewrite_watermarks",
"trajectory_runtime_events",
"acp_parent_stream_events"
]) executeSqliteQuerySync(params.destination.db, destinationDb.deleteFrom(table).where("session_id", "=", params.sessionId));
for (const row of transcriptEvents) executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("transcript_events").values(row));
for (const row of transcriptIdentities) executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("transcript_event_identities").values(row));
for (const row of rewriteWatermarks) executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("transcript_rewrite_watermarks").values(row));
for (const row of trajectoryEvents) executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("trajectory_runtime_events").values(row));
for (const row of parentStreamEvents) executeSqliteQuerySync(params.destination.db, destinationDb.insertInto("acp_parent_stream_events").values(row));
return true;
}
//#endregion
export { rehomeSqliteSessionDeliveryReferencesForCanonicalRepair as a, readExactSessionEntryRowForCanonicalRepair as i, ensureSqliteTranscriptGenerationsForCanonicalRepair as n, rehomeSqliteSessionDeliveryReferencesForCanonicalRepairBatch as o, listSqliteSessionGenerationIdsForCanonicalRepair as r, copySqliteSessionOwnedStateForCanonicalRepair as t };