UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

287 lines (286 loc) 13.6 kB
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js"; import { d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { n as appendRegularFile } from "./fs-safe-B6pvPGnf.js"; import { y as formatMemoryDreamingDay } from "./dreaming-DmJZL5-O.js"; import "./memory-core-host-engine-foundation-BZwrR3xr.js"; import "./string-coerce-runtime-GQa0ehRA.js"; import "./security-runtime-Ckf0kc0h.js"; import { a as sessionPathForFile, r as buildSessionEntry, s as statSessionEntrySync, t as loadMemorySessionMetadata } from "./memory-core-host-engine-sessions-Cn5pEjBS.js"; import "./memory-core-host-status-DAqa4uMk.js"; import { b as writeMemoryCoreWorkspaceEntries, i as DREAMING_SESSION_INGESTION_SEEN_NAMESPACE, r as DREAMING_SESSION_INGESTION_FILES_NAMESPACE, y as readMemoryCoreWorkspaceEntries } from "./dreaming-state-B0qd2W7q.js"; import { i as normalizeSessionIngestionState, t as SESSION_INGESTION_MAX_TRACKED_MESSAGES_PER_SESSION } from "./dreaming-ingestion-state-CsGJDNHD.js"; import { i as listMemorySessionTombstones } from "./memory-entry-origins-DGcbM97X.js"; import path from "node:path"; import fs from "node:fs/promises"; import { createHash } from "node:crypto"; //#region extensions/memory-core/src/session-ingestion.ts const SESSION_CORPUS_RELATIVE_DIR = path.join("memory", ".dreams", "session-corpus"); const SESSION_INGESTION_SCORE = .58; const SESSION_INGESTION_MIN_SNIPPET_CHARS = 12; const SESSION_INGESTION_MAX_SNIPPET_CHARS = 280; function buildSessionScope(agentId, sessionId) { return `${agentId}:${sessionId}`; } function sessionPathFromCorpus(entry) { return entry.transcriptSource === "sqlite" ? path.join("sessions", entry.agentId, entry.sessionId).replace(/\\/g, "/") : sessionPathForFile(entry.sessionFile); } function sessionIngestionSourceFromCorpus(entry) { const sessionPath = sessionPathFromCorpus(entry); if (entry.sessionKind !== "interactive") return null; const scope = entry.transcriptSource === "sqlite" ? `${entry.agentId}:${sessionPath}` : buildSessionScope(entry.agentId, entry.sessionId); return { agentId: entry.agentId, absolutePath: entry.sessionFile, foreign: false, sessionPath, stateKey: `${entry.agentId}:${sessionPath}`, scope, sessionOrigin: { agentId: entry.agentId, sessionId: entry.sessionId, ...entry.sessionKey ? { sessionKey: entry.sessionKey } : {} }, ...entry.transcriptSource === "sqlite" ? { legacyScope: buildSessionScope(entry.agentId, entry.sessionId) } : {}, buildOptions: { sessionKind: "interactive", ...entry.transcriptSource === "sqlite" ? { agentId: entry.agentId, sessionId: entry.sessionId } : {}, ...entry.sessionKey ? { sessionKey: entry.sessionKey } : {}, ...entry.storePath ? { storePath: entry.storePath } : {}, ...entry.updatedAtMs !== void 0 ? { updatedAtMs: entry.updatedAtMs } : {}, ...entry.generatedByDreamingNarrative ? { generatedByDreamingNarrative: true } : {}, ...entry.generatedByCronRun ? { generatedByCronRun: true } : {} } }; } function resolveAdmissionPolicy(pluginConfig) { const exclusions = asNullableRecord(asNullableRecord(pluginConfig?.memoryPolicy)?.excludeSessions); if (!exclusions) return; const values = (key) => Array.isArray(exclusions[key]) ? normalizeStringEntries(exclusions[key].filter((value) => typeof value === "string")) : []; const policy = { hookExternalContentSources: values("hookExternalContentSources"), channels: values("channels"), chatTypes: values("chatTypes") }; return Object.values(policy).some((entries) => entries.length > 0) ? policy : void 0; } function sessionExclusionReason(source, policy, forgottenSessionIds) { if (!source.sessionOrigin) return; const { agentId, sessionId } = source.sessionOrigin; if (forgottenSessionIds ? forgottenSessionIds.has(sessionId) : listMemorySessionTombstones({ agentId, sessionIds: [sessionId] }).length > 0) return "forgotten"; if (!policy) return; const metadata = loadMemorySessionMetadata({ ...source.sessionOrigin, storePath: source.buildOptions.storePath }); if (!metadata) return; if (metadata.hookExternalContentSource && policy.hookExternalContentSources.includes(metadata.hookExternalContentSource)) return `hookExternalContentSource:${metadata.hookExternalContentSource}`; if (metadata.channel && policy.channels.includes(metadata.channel)) return `channel:${metadata.channel}`; return metadata.chatType && policy.chatTypes.includes(metadata.chatType) ? `chatType:${metadata.chatType}` : void 0; } function sessionIngestionStateKeyFromCorpus(entry) { return `${entry.agentId}:${sessionPathFromCorpus(entry)}`; } function foreignSessionIngestionSource(agentId, archiveFile) { const absolutePath = path.resolve(archiveFile); const normalizedPath = absolutePath.replaceAll("\\", "/"); return { agentId, absolutePath, foreign: true, sessionPath: sessionPathForFile(absolutePath), stateKey: `session-backfill:${normalizedPath}`, scope: `archive:${agentId}:${normalizedPath}`, buildOptions: { sessionKind: "interactive" } }; } function normalizeSessionCorpusSnippet(value) { return truncateUtf16Safe(value.replace(/\s+/g, " ").trim(), SESSION_INGESTION_MAX_SNIPPET_CHARS); } function hashSessionMessageId(value) { return createHash("sha1").update(value).digest("hex"); } async function statSessionSource(source) { if (source.buildOptions.agentId && source.buildOptions.storePath) try { const stat = statSessionEntrySync(source.absolutePath, source.buildOptions); return stat ? { mtimeMs: Math.floor(Math.max(0, stat.mtimeMs)), size: Math.floor(stat.size) } : null; } catch { return; } const stat = await fs.stat(source.absolutePath).catch((error) => { if (error.code === "ENOENT") return null; throw error; }); return stat ? { mtimeMs: Math.floor(Math.max(0, stat.mtimeMs)), size: Math.floor(stat.size) } : null; } async function scanSessionIngestionSource(params) { const emptyScan = (status, fileState) => ({ status, candidates: [], ...fileState ? { fileState } : {}, scannedEndIndex: fileState?.lastContentLine ?? 0 }); const fingerprint = await statSessionSource(params.source); if (fingerprint === null) return emptyScan("absent"); if (!params.verifyContent && fingerprint && params.previous?.mtimeMs === fingerprint.mtimeMs && params.previous.size === fingerprint.size && params.previous.contentHash.length > 0 && params.previous.lastContentLine >= params.previous.lineCount) return emptyScan("unchanged", params.previous); const entry = await buildSessionEntry(params.source.absolutePath, params.source.buildOptions); if (!entry) return emptyScan("unavailable", params.previous); const fileFingerprint = { mtimeMs: Math.floor(Math.max(0, entry.mtimeMs)), size: Math.floor(Math.max(0, entry.size)) }; const lines = entry.content ? entry.content.split("\n") : []; const terminalState = { ...fileFingerprint, contentHash: entry.hash.trim(), lineCount: lines.length, lastContentLine: lines.length }; if (entry.generatedByDreamingNarrative || entry.generatedByCronRun) return emptyScan("excluded", terminalState); if (params.previous?.mtimeMs === fileFingerprint.mtimeMs && params.previous.size === fileFingerprint.size && params.previous.contentHash === terminalState.contentHash && params.previous.lineCount === lines.length && params.previous.lastContentLine >= lines.length) return emptyScan("unchanged", params.previous); const startIndex = params.previous?.mtimeMs === fileFingerprint.mtimeMs && params.previous.size === fileFingerprint.size && params.previous.contentHash === terminalState.contentHash && params.previous.lineCount === lines.length ? Math.max(0, Math.min(params.previous?.lastContentLine ?? 0, lines.length)) : 0; const seen = new Set(params.seenMessages[params.source.scope] ?? []); const legacySeen = params.source.legacyScope ? new Set(params.seenMessages[params.source.legacyScope] ?? []) : void 0; const candidates = []; let progressBlockIndex; let scannedEndIndex = startIndex; for (let index = startIndex; index < lines.length; index += 1) { if (params.maxCandidates !== void 0 && candidates.length >= params.maxCandidates) break; scannedEndIndex = index + 1; const snippet = normalizeSessionCorpusSnippet(lines[index] ?? ""); if (snippet.length < SESSION_INGESTION_MIN_SNIPPET_CHARS) continue; const lineNumber = entry.lineMap[index] ?? index + 1; const timestampMs = entry.messageTimestampsMs[index] ?? 0; const parsedProvenance = entry.lineProvenance[index] ?? { originClass: "untrusted", sessionKind: "interactive", observedAt: timestampMs || entry.mtimeMs }; const provenance = params.source.foreign ? { ...parsedProvenance, originClass: "untrusted" } : parsedProvenance; if (params.acceptProvenance && !params.acceptProvenance(provenance)) continue; const day = formatMemoryDreamingDay(timestampMs || entry.mtimeMs, params.timezone); const disposition = params.classifyDay(day); if (disposition !== "include") { if (disposition === "block") progressBlockIndex ??= index; continue; } const basis = timestampMs > 0 ? `ts:${Math.floor(timestampMs)}` : `line:${lineNumber}`; const hash = hashSessionMessageId(`${params.source.scope}\n${basis}\n${snippet}`); const legacyHash = params.source.legacyScope ? hashSessionMessageId(`${params.source.legacyScope}\n${basis}\n${snippet}`) : void 0; if (seen.has(hash) || legacyHash && legacySeen?.has(legacyHash)) continue; candidates.push({ contentIndex: index, day, hash, lineNumber, provenance, rendered: truncateUtf16Safe(`[${params.source.agentId}/${params.source.sessionPath}#L${lineNumber}] ${snippet}`, 344), scope: params.source.scope, stateKey: params.source.stateKey, snippet, ...params.source.sessionOrigin ? { sessionOrigin: params.source.sessionOrigin } : {} }); seen.add(hash); } return { status: "scanned", candidates, fileState: { ...terminalState, lastContentLine: scannedEndIndex }, ...progressBlockIndex !== void 0 ? { progressBlockIndex } : {}, scannedEndIndex }; } function mergeTrackedMessageHashes(existing, additions) { return [.../* @__PURE__ */ new Set([...existing, ...additions])].slice(-SESSION_INGESTION_MAX_TRACKED_MESSAGES_PER_SESSION); } function trimTrackedSessionScopes(seenMessages) { const keep = new Set(Object.keys(seenMessages).toSorted().slice(-2048)); return Object.fromEntries(Object.entries(seenMessages).filter(([scope]) => keep.has(scope))); } async function readSessionIngestionState(workspaceDir) { const [files, seenChunks] = await Promise.all([readMemoryCoreWorkspaceEntries({ namespace: DREAMING_SESSION_INGESTION_FILES_NAMESPACE, workspaceDir }), readMemoryCoreWorkspaceEntries({ namespace: DREAMING_SESSION_INGESTION_SEEN_NAMESPACE, workspaceDir })]); const seenMessages = {}; for (const { value } of seenChunks.toSorted((a, b) => a.value.index - b.value.index)) { if (!value.scope.trim()) continue; seenMessages[value.scope] = [...seenMessages[value.scope] ?? [], ...value.hashes]; } return normalizeSessionIngestionState({ version: 3, files: Object.fromEntries(files.map((entry) => [entry.key, entry.value])), seenMessages }); } async function writeSessionIngestionState(workspaceDir, state) { const seenEntries = Object.entries(state.seenMessages).flatMap(([scope, hashes]) => Array.from({ length: Math.ceil(hashes.length / 512) }, (_, index) => ({ key: `${scope}:${index}`, value: { scope, index, hashes: hashes.slice(index * 512, (index + 1) * 512) } }))); await Promise.all([writeMemoryCoreWorkspaceEntries({ namespace: DREAMING_SESSION_INGESTION_FILES_NAMESPACE, workspaceDir, entries: Object.entries(state.files).map(([key, value]) => ({ key, value })) }), writeMemoryCoreWorkspaceEntries({ namespace: DREAMING_SESSION_INGESTION_SEEN_NAMESPACE, workspaceDir, entries: seenEntries })]); } async function appendSessionCorpusLines(params) { if (params.lines.length === 0) return []; const relativePath = path.posix.join("memory", ".dreams", "session-corpus", `${params.day}.txt`); const absolutePath = path.join(params.workspaceDir, SESSION_CORPUS_RELATIVE_DIR, `${params.day}.txt`); await fs.mkdir(path.dirname(absolutePath), { recursive: true }); const normalized = (await fs.readFile(absolutePath, "utf-8").catch((error) => { if (error.code === "ENOENT") return ""; throw error; })).replace(/\r\n/g, "\n"); const existingLines = normalized ? (normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized).split("\n").length : 0; await appendRegularFile({ filePath: absolutePath, content: `${params.lines.map((entry) => entry.rendered).join("\n")}\n`, rejectSymlinkParents: true }); return params.lines.map((entry, index) => ({ path: relativePath, startLine: existingLines + index + 1, endLine: existingLines + index + 1, score: SESSION_INGESTION_SCORE, snippet: entry.snippet, source: "memory", provenance: entry.provenance, ...entry.sessionOrigin ? { sessionOrigin: entry.sessionOrigin } : {} })); } //#endregion export { mergeTrackedMessageHashes as a, scanSessionIngestionSource as c, sessionIngestionStateKeyFromCorpus as d, trimTrackedSessionScopes as f, foreignSessionIngestionSource as i, sessionExclusionReason as l, SESSION_INGESTION_SCORE as n, readSessionIngestionState as o, writeSessionIngestionState as p, appendSessionCorpusLines as r, resolveAdmissionPolicy as s, SESSION_CORPUS_RELATIVE_DIR as t, sessionIngestionSourceFromCorpus as u };