UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

190 lines (189 loc) 9.08 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { x as formatHookErrorForLog } from "./hook-runner-global-0kfmMG4T.js"; import { o as hasInterSessionUserProvenance } from "./input-provenance-DiG-Cwnd.js"; import { Nt as selectVisibleTranscriptEvents, d as readSessionTranscriptBoundedMessageTailPage } from "./session-accessor-YsytfDtG.js"; import { S as loadTranscriptEventsSync } from "./session-accessor.sqlite-transcript-store-B0zw3fAU.js"; import { a as isOpenClawDeliveryMirrorAssistantMessage } from "./transcript-only-openclaw-assistant-CVgy4bjA.js"; import { r as sanitizeModelSpecialTokens } from "./external-content-CpqslxXH.js"; import { i as resolveHookConfig } from "./config-DWWH6LIu.js"; import { t as classifySessionMessageOrigin } from "./session-provenance-HdsJkZBJ.js"; //#region src/hooks/bundled/session-memory/transcript.ts const SESSION_MEMORY_TOOL_DIRECTIVE_PREFIX = String.raw`(?:(?:\|DSML\|)|(?:\uFF5CDSML\uFF5C))?`; const SESSION_MEMORY_TOOL_DIRECTIVE_KIND = String.raw`(?:tool_calls?|function_calls?|tool_use_error)`; const SESSION_MEMORY_DROP_BLOCK_RE = new RegExp(String.raw`<${SESSION_MEMORY_TOOL_DIRECTIVE_PREFIX}${SESSION_MEMORY_TOOL_DIRECTIVE_KIND}\b[^>]*>` + String.raw`[\s\S]*?(?:<\/${SESSION_MEMORY_TOOL_DIRECTIVE_PREFIX}${SESSION_MEMORY_TOOL_DIRECTIVE_KIND}>|$)`, "gi"); const SESSION_MEMORY_ROLE_DIRECTIVE_BLOCK_RE = /<(system|assistant|user)\b[^>]*>[\s\S]*?<\/\1>/gi; const SESSION_MEMORY_ROLE_DIRECTIVE_TAG_RE = /<\/?(?:system|assistant|user)\b[^>]*>/gi; const SESSION_MEMORY_TRAILING_NO_REPLY_RE = /(?:^|\n)\s*NO_REPLY\s*$/i; const SESSION_MEMORY_JSON_LINE_SEPARATOR_RE = /[\u0085\u2028\u2029]/gu; function quoteSessionMemoryText(text) { return JSON.stringify(text).replace(SESSION_MEMORY_JSON_LINE_SEPARATOR_RE, (separator) => `\\u${separator.charCodeAt(0).toString(16).padStart(4, "0")}`); } function isNoReplyMarker(text) { const trimmed = text.trim(); return /^NO_REPLY$/i.test(trimmed) || /^\{\s*"action"\s*:\s*"NO_REPLY"\s*\}$/i.test(trimmed); } function sanitizeSessionMemoryTranscriptText(text) { if (isNoReplyMarker(text)) return null; return sanitizeModelSpecialTokens(text).replace(SESSION_MEMORY_DROP_BLOCK_RE, "").replace(SESSION_MEMORY_ROLE_DIRECTIVE_BLOCK_RE, "").replace(SESSION_MEMORY_ROLE_DIRECTIVE_TAG_RE, "").replace(SESSION_MEMORY_TRAILING_NO_REPLY_RE, "").trim() || null; } function extractTextMessageContent(content) { if (typeof content === "string") return content; if (!Array.isArray(content)) return; for (const block of content) { if (!block || typeof block !== "object") continue; const candidate = block; if (candidate.type === "text" && typeof candidate.text === "string") return candidate.text; } } function renderSessionMemoryMessage(entry, turnOrigin) { if (!entry || typeof entry !== "object") return { turnOrigin }; const record = entry; if (record.type !== "message" || !record.message) return { turnOrigin }; const role = record.message.role; if (role !== "user" && role !== "assistant" || !("content" in record.message)) return { turnOrigin }; const nextTurnOrigin = role === "user" ? classifySessionMessageOrigin(record.message, turnOrigin) : turnOrigin; const originClass = classifySessionMessageOrigin(record.message, nextTurnOrigin); if (role === "user" && hasInterSessionUserProvenance(record.message)) return { turnOrigin: nextTurnOrigin }; const text = extractTextMessageContent(record.message.content); const sanitized = text ? sanitizeSessionMemoryTranscriptText(text) : null; if (!sanitized) return { turnOrigin: nextTurnOrigin }; if (sanitized.startsWith("/")) return { turnOrigin: nextTurnOrigin, ...role === "user" ? { message: { isDeliveryMirror: false, originClass, role } } : {} }; return { turnOrigin: nextTurnOrigin, message: { isDeliveryMirror: isOpenClawDeliveryMirrorAssistantMessage(record.message), originClass, role, text: sanitized } }; } function renderSessionMemoryRecords(events) { const allMessages = []; let lastAssistantText; let turnOrigin = "untrusted"; for (const event of events) { const result = renderSessionMemoryMessage(event, turnOrigin); turnOrigin = result.turnOrigin; const rendered = result.message; if (!rendered) continue; if (rendered.role === "user") lastAssistantText = void 0; if (!rendered.text) continue; if (rendered.isDeliveryMirror && rendered.text === lastAssistantText) continue; allMessages.push({ line: `${rendered.role}: ${quoteSessionMemoryText(rendered.text)}`, originClass: rendered.originClass }); if (rendered.role === "assistant") lastAssistantText = rendered.text; } return allMessages; } /** Counts transcript events that remain after session-memory filtering and deduplication. */ function countSessionMemoryMessages(events) { return renderSessionMemoryRecords(events).length; } function getRecentSessionProjectionFromEvents(events, messageCount = 15) { const limit = Number.isFinite(messageCount) ? Math.max(0, Math.floor(messageCount)) : 0; if (limit === 0) return null; const records = renderSessionMemoryRecords(events).slice(-limit); if (records.length === 0) return null; return { content: records.map((record) => record.line).join("\n"), originClass: records.some((record) => record.originClass === "untrusted" || record.originClass === "system") ? "untrusted" : "agent" }; } //#endregion //#region src/hooks/bundled/session-memory/capture.ts const SESSION_MEMORY_CAPTURE_MAX_BYTES = 8388608; const SESSION_MEMORY_CAPTURE_PAGE_MESSAGES = 256; const SESSION_MEMORY_CAPTURE_MAX_SCANNED_MESSAGES = 4096; function relinkCapturedActiveMessageEvents(events) { let parentId = null; return events.map((event, index) => { if (!isRecord(event) || event.type !== "message") return event; const id = typeof event.id === "string" ? event.id : `session-memory-${index + 1}`; const linked = { ...event, id, parentId }; parentId = id; return linked; }); } function captureRecentSessionMemoryEvents(scope, messageCount) { const captured = []; let capturedBytes = 0; let offset = 0; let totalMessages = Number.POSITIVE_INFINITY; while (offset < totalMessages && offset < SESSION_MEMORY_CAPTURE_MAX_SCANNED_MESSAGES && capturedBytes < SESSION_MEMORY_CAPTURE_MAX_BYTES && countSessionMemoryMessages(selectVisibleTranscriptEvents(relinkCapturedActiveMessageEvents(captured))) < messageCount) { const page = readSessionTranscriptBoundedMessageTailPage(scope, { maxBytes: SESSION_MEMORY_CAPTURE_MAX_BYTES - capturedBytes, maxMessages: Math.min(SESSION_MEMORY_CAPTURE_PAGE_MESSAGES, SESSION_MEMORY_CAPTURE_MAX_SCANNED_MESSAGES - offset), offset }); totalMessages = page.totalMessages; if (page.scannedMessages === 0) break; captured.unshift(...page.events.map(({ event }) => event)); capturedBytes += page.serializedBytes; offset += page.scannedMessages; } return relinkCapturedActiveMessageEvents(captured); } function selectCurrentMemoryWindow(events) { const active = selectVisibleTranscriptEvents(events); const boundaryIndex = active.findLastIndex((event) => isRecord(event) && event.type === "reset"); const boundary = active[boundaryIndex]; if (!isRecord(boundary)) return active; const firstKeptIndex = typeof boundary.firstKeptEntryId === "string" ? active.findIndex((event) => isRecord(event) && event.id === boundary.firstKeptEntryId) : -1; return [...(firstKeptIndex >= 0 && firstKeptIndex < boundaryIndex ? active.slice(firstKeptIndex, boundaryIndex) : []).filter((event) => isRecord(event) && event.type === "message" && isRecord(event.message) && (event.message.role === "user" || event.message.role === "assistant")), ...active.slice(boundaryIndex + 1)]; } function captureAuthoritativeMemoryEvents(scope) { const messages = selectCurrentMemoryWindow(loadTranscriptEventsSync(scope)).filter((event) => isRecord(event) && event.type === "message").slice(-4096); const captured = []; let bytes = 0; for (const event of messages.toReversed()) { const size = Buffer.byteLength(JSON.stringify(event)) + 1; if (bytes + size <= SESSION_MEMORY_CAPTURE_MAX_BYTES) { captured.unshift(event); bytes += size; } } return relinkCapturedActiveMessageEvents(captured); } /** Capture while the caller still owns the departing session's active window. */ function captureSessionMemoryTranscript(scope, cfg) { const hookConfig = resolveHookConfig(cfg, "session-memory"); const messageCount = typeof hookConfig?.messages === "number" && hookConfig.messages > 0 ? hookConfig.messages : 15; try { let events; try { events = captureRecentSessionMemoryEvents(scope, messageCount); } catch { events = captureAuthoritativeMemoryEvents(scope); } const projection = getRecentSessionProjectionFromEvents(events, messageCount); return projection ? { status: "available", ...projection } : { status: "available", content: null, originClass: "agent" }; } catch (error) { return { status: "unavailable", reason: formatHookErrorForLog(error) }; } } //#endregion export { captureSessionMemoryTranscript as t };