UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

515 lines (514 loc) • 22.6 kB
import { a as asOptionalRecord, i as asOptionalObjectRecord } from "./record-coerce-TIsquoVR.mjs"; import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs"; import { c as resolveTerminalAssistantTranscriptRunId, o as readSessionTranscriptRunId } from "./transcript-events-Ce9wKss8.mjs"; import "./reply-payload-DBtQZ6PK.mjs"; import { t as normalizeMediaReferenceForComparison } from "./media-reference-comparison-D61D3Ct7.mjs"; import { f as withTranscriptWriteLock, u as rewriteTranscriptEventRowsExact } from "./session-accessor.sqlite-transcript-write-DZbFltlG.mjs"; import { s as loadTranscriptEventRowsAfterSeqSync } from "./session-accessor.sqlite-read-CAR7IGtb.mjs"; import { s as readAssistantTextBlocksForPhase, t as extractAssistantPhaseText } from "./chat-message-content-C28ajso0.mjs"; import { t as applyAssistantDeliveryDirectives } from "./transcript-assistant-delivery-BrorVzas.mjs"; import { i as retainAssistantModelContent, n as projectAssistantDisplayContent, r as readAssistantDisplayContent, t as ASSISTANT_DISPLAY_CONTENT_FIELD } from "./assistant-display-content-mzartsGP.mjs"; import { u as patchSessionEntryCore } from "./session-accessor.sqlite-entry-BuCmtpuj.mjs"; import { p as publishTranscriptUpdate } from "./session-history-eviction-LukylhGG.mjs"; import { T as rewriteAssistantTranscriptMessageForRun, a as appendAbortedSessionTranscriptPartial, f as persistSessionTranscriptTurn, l as readSessionTranscriptWatermark } from "./session-accessor-BM3Wa1Bx.mjs"; import { l as makeZeroUsageSnapshot } from "./usage-DDsrNSO2.mjs"; import { t as findTranscriptEvent } from "./session-transcript-match-BsYSZyUw.mjs"; import { t as splitMediaFromOutput } from "./parse-CQsHLk6K.mjs"; import { a as resolveMirroredTranscriptText } from "./transcript-D08D0EMY.mjs"; import { u as sanitizeAssistantDisplayText } from "./chat-assistant-content-yl_wkI21.mjs"; import { n as abortedPartialPersistenceError, t as ABORTED_PARTIAL_PERSISTENCE_WARNING } from "./chat-aborted-partial-D5gNrs8T.mjs"; //#region src/gateway/server-methods/chat-transcript-inject.ts function resolveInjectedAssistantContent(params) { const labelPrefix = params.label ? `[${params.label}]\n\n` : ""; if (params.content && params.content.length > 0) { if (!labelPrefix) return params.content; const first = params.content[0]; if (first?.type === "text" && typeof first.text === "string") return [{ ...first, text: `${labelPrefix}${first.text}` }, ...params.content.slice(1)]; return [{ type: "text", text: labelPrefix.trim() }, ...params.content]; } return [{ type: "text", text: `${labelPrefix}${params.message}` }]; } /** Append a gateway-authored assistant message while preserving transcript parent links. */ async function appendInjectedAssistantMessageToTranscript(params) { const now = params.now ?? Date.now(); const displayMessage = { role: "assistant", content: resolveInjectedAssistantContent(params).map((block) => Object.assign({}, block)) }; const preparedDisplayMessage = applyAssistantDeliveryDirectives(displayMessage); const displayContent = preparedDisplayMessage.content; const canonicalContent = retainAssistantModelContent(displayContent); const rawDeliveryFacts = preparedDisplayMessage.openclawDelivery; const abortRunId = params.abortMeta?.runId; const messageBody = applyAssistantDeliveryDirectives({ role: "assistant", content: canonicalContent, [ASSISTANT_DISPLAY_CONTENT_FIELD]: displayContent, timestamp: now, stopReason: params.stopReason ?? "stop", usage: makeZeroUsageSnapshot(), api: "openai-responses", provider: "openclaw", model: "gateway-injected", ...params.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : {}, ...params.ttsSupplement ? { openclawTtsSupplement: params.ttsSupplement } : {}, ...params.contextFreeCommand === true ? { excludeFromContext: true, __openclaw: { contextFreeCommand: true } } : {}, ...params.abortMeta ? { openclawAbort: { aborted: true, origin: params.abortMeta.origin, runId: params.abortMeta.runId } } : {} }); if (rawDeliveryFacts && messageBody.openclawDelivery === void 0) messageBody.openclawDelivery = rawDeliveryFacts; try { if (!params.transcriptPath && (!params.storePath || !params.sessionId || !params.sessionKey)) return { ok: false, error: "transcript identity not resolved" }; if (params.abortMeta?.producerSettled) { if (!params.storePath || !params.sessionId || !params.sessionKey) return { ok: false, error: "settled producer transcript identity not resolved" }; const scope = { storePath: params.storePath, sessionId: params.sessionId, sessionKey: params.sessionKey, ...params.agentId ? { agentId: params.agentId } : {} }; const result = await appendAbortedSessionTranscriptPartial(scope, { runId: params.abortMeta.runId, message: messageBody, expectedLifecycleRevision: params.expectedLifecycleRevision, now, config: params.config }); if (!result.ok) return { ok: false, error: result.error.code }; if (result.value.skipped) return { ok: true, skipped: true }; const { append } = result.value; return { ok: true, messageId: append.messageId, message: projectAssistantDisplayContent(append.message) }; } let predicateDeclined = false; const turn = await persistSessionTranscriptTurn({ sessionKey: params.sessionKey ?? "", ...params.transcriptPath ? { sessionFile: params.transcriptPath } : {}, ...params.storePath ? { storePath: params.storePath } : {}, ...params.sessionId ? { sessionId: params.sessionId } : {}, ...params.agentId ? { agentId: params.agentId } : {} }, { expectedSessionId: params.expectedSessionId, expectedLifecycleRevision: params.expectedLifecycleRevision, updateMode: "inline", onMessageCommitted: params.onMessageCommitted, ...params.abortMeta ? { runId: params.abortMeta.runId } : {}, touchSessionEntry: Boolean(params.storePath && params.sessionId && params.sessionKey), ...params.config ? { config: params.config } : {}, messages: [{ message: messageBody, idempotencyLookup: "scan-assistant", ...params.abortMeta ? { shouldAppendInTransaction: (readLatestAssistantMessage) => { const latestAssistantMessage = readLatestAssistantMessage(); const committedRunId = resolveTerminalAssistantTranscriptRunId(latestAssistantMessage, readSessionTranscriptRunId(latestAssistantMessage)); const committedText = extractAssistantPhaseText(latestAssistantMessage)?.trim(); predicateDeclined = committedRunId === abortRunId && committedText === params.message.trim(); return !predicateDeclined; } } : {}, now, useRawWhenLinear: true }] }); if (turn.rejectedReason) return { ok: false, error: turn.rejectedReason }; const appended = turn.messages[0]; if (!appended) { if (predicateDeclined) return { ok: true, skipped: true }; return { ok: false, error: "gateway-injected assistant message was not appended" }; } return { ok: true, messageId: appended.messageId, message: projectAssistantDisplayContent(appended.message) }; } catch (err) { return { ok: false, error: formatErrorMessage(err) }; } } //#endregion //#region src/gateway/server-methods/chat-transcript-persistence.ts function mergeAssistantDisplayContent(modelContent, preparedDisplayContent, retainedCommentary) { const remainingDisplayContent = [...preparedDisplayContent]; const content = []; for (const block of modelContent) { if (block.type !== "text" || typeof block.text !== "string" || retainedCommentary.has(block)) { content.push(block); continue; } const matchingTextIndex = remainingDisplayContent.findIndex((candidate) => candidate.type === "text" && candidate.text === block.text); if (matchingTextIndex < 0) { content.push(block); continue; } const nextTextOffset = remainingDisplayContent.slice(matchingTextIndex + 1).findIndex((candidate) => candidate.type === "text"); const segmentEnd = nextTextOffset < 0 ? remainingDisplayContent.length : matchingTextIndex + nextTextOffset + 1; content.push(...remainingDisplayContent.splice(0, segmentEnd)); } content.push(...remainingDisplayContent); return content; } function buildAssistantDisplayRewrite(params) { const previousDisplay = Array.isArray(params.message["openclawDisplayContent"]) ? readAssistantDisplayContent(params.message) : void 0; const previousMedia = asOptionalRecord(params.message.openclawDelivery)?.mediaUrls; const managedMediaUrls = previousDisplay ? [...Array.isArray(previousMedia) ? previousMedia.filter((value) => typeof value === "string") : [], ...params.managedMediaUrls ?? []] : params.managedMediaUrls; const prepared = applyAssistantDeliveryDirectives({ ...params.message, content: params.displayContent.map((block) => Object.assign({}, block)) }, { managedMediaUrls }); const original = previousDisplay ?? (Array.isArray(params.message.content) ? params.message.content : []); const retainedCommentary = new Set(previousDisplay ? readAssistantTextBlocksForPhase({ ...params.message, content: original }, "commentary") : []); let inCommentary = false; const content = []; const seenText = /* @__PURE__ */ new Set(); for (const block of original) { if (block.type === "text") inCommentary = retainedCommentary.has(block); if (inCommentary) { content.push(block); continue; } if (block.type === "thinking" || block.type === "toolCall") { content.push(block); continue; } if (block.type !== "text" || typeof block.text !== "string" || !params.retainOriginalText && !prepared.content.some((candidate) => candidate.type === "text" && candidate.text === block.text)) continue; const splitText = splitMediaFromOutput(block.text).text; if (splitText === block.text && /\bMEDIA:/iu.test(block.text)) continue; const text = sanitizeAssistantDisplayText(splitText, { preserveBoundaries: true }); if (text) { if (text === block.text || previousDisplay) content.push(text === block.text ? block : { ...block, text }); else { const { textSignature: _textSignature, ...rest } = block; content.push({ ...rest, text }); } seenText.add(text); } else if (previousDisplay) content.push({ ...block, text: "" }); } for (const block of prepared.content) if (block.type === "text" && typeof block.text === "string" && !seenText.has(block.text)) content.push(block); return { ...prepared, content: previousDisplay ? params.message.content : content, [ASSISTANT_DISPLAY_CONTENT_FIELD]: mergeAssistantDisplayContent(content, prepared.content, retainedCommentary) }; } function assistantTranscriptScope(params) { const sessionKey = params.sessionKey.trim(); if (!sessionKey || !params.sessionId.trim()) return null; return { sessionKey, sessionId: params.sessionId, ...params.storePath ? { storePath: params.storePath } : {}, ...params.agentId ? { agentId: params.agentId } : {} }; } function transcriptEventId(event) { const id = asOptionalRecord(event)?.id; return typeof id === "string" && id.trim().length > 0 ? id : void 0; } function transcriptEventMessage(event) { return asOptionalRecord(asOptionalRecord(event)?.message); } function transcriptMessageTarget(event) { const message = event ? transcriptEventMessage(event) : void 0; const messageId = event ? transcriptEventId(event) : void 0; return messageId && message ? { messageId, message } : null; } function findAssistantTranscriptMessageByIdempotencyKeyInEvents(events, idempotencyKey) { const trimmedIdempotencyKey = idempotencyKey.trim(); if (!trimmedIdempotencyKey) return null; return transcriptMessageTarget(events.toReversed().find((event) => { const message = transcriptEventMessage(event); return message?.role === "assistant" && message.idempotencyKey === trimmedIdempotencyKey; })); } function findAssistantTranscriptMessageByTurnIndexAndMediaInEvents(events, params) { const expectedMedia = new Set(params.mediaUrls.map((value) => normalizeMediaReferenceForComparison(value)).filter((value) => value.length > 0)); if (expectedMedia.size === 0 || !Number.isSafeInteger(params.assistantMessageIndex) || params.assistantMessageIndex < 1) return null; const target = events.filter((event) => transcriptEventMessage(event)?.role === "assistant")[params.assistantMessageIndex - 1]; const found = transcriptMessageTarget(target); const text = found ? extractAssistantPhaseText(found.message) : void 0; if (!found || !text) return null; const actualMedia = new Set((splitMediaFromOutput(text).mediaUrls ?? []).map((value) => normalizeMediaReferenceForComparison(value)).filter((value) => value.length > 0)); return actualMedia.size === expectedMedia.size && [...expectedMedia].every((value) => actualMedia.has(value)) ? found : null; } function extractAssistantTranscriptText(message) { const content = message.content; if (!Array.isArray(content)) return; return content.map((value) => { const block = asOptionalObjectRecord(value); return block?.type === "text" && typeof block.text === "string" ? block.text.trim() : ""; }).filter(Boolean).join("\n").trim() || void 0; } function findSourceReplyTranscriptMirrorByMetadataInEvents(params) { const byIdempotencyKey = findAssistantTranscriptMessageByIdempotencyKeyInEvents(params.events, params.idempotencyKey); if (byIdempotencyKey?.message.provider === "openclaw" && byIdempotencyKey.message.model === "delivery-mirror") return byIdempotencyKey; const expectedText = resolveMirroredTranscriptText({ text: params.metadata?.text, mediaUrls: params.metadata?.mediaUrls }); if (!expectedText) return null; return transcriptMessageTarget(params.events.toReversed().find((event) => { const message = transcriptEventMessage(event); return typeof transcriptEventId(event) === "string" && message?.role === "assistant" && message.provider === "openclaw" && message.model === "delivery-mirror" && extractAssistantTranscriptText(message) === expectedText; })); } async function transcriptExists(scope) { const sessionId = scope.sessionId; if (!sessionId) return false; return await findTranscriptEvent({ ...scope, sessionId }, { kind: "latest" }).catch(() => void 0) !== void 0; } async function appendAssistantTranscriptMessage(params) { const scope = assistantTranscriptScope(params); if (!scope) return { ok: false, error: "transcript identity not resolved" }; if (!params.createIfMissing && !await transcriptExists(scope)) return { ok: false, error: "transcript not found" }; return appendInjectedAssistantMessageToTranscript({ expectedSessionId: params.expectedSessionId, expectedLifecycleRevision: params.expectedLifecycleRevision, sessionKey: params.sessionKey, sessionId: params.sessionId, storePath: params.storePath, ...params.agentId ? { agentId: params.agentId } : {}, message: params.message, label: params.label, content: params.content, idempotencyKey: params.idempotencyKey, stopReason: params.stopReason, abortMeta: params.abortMeta, ttsSupplement: params.ttsSupplement, ...params.contextFreeCommand === true ? { contextFreeCommand: true } : {}, config: params.cfg, onMessageCommitted: params.onMessageCommitted }); } async function persistAbortedPartials(params) { let warning; for (const snapshot of params.snapshots) { if (snapshot.ok && snapshot.settlement.deferred) continue; try { warning = await persistAbortedPartial({ context: params.context, snapshot }) ?? warning; } catch (error) { throw abortedPartialPersistenceError(error, warning); } } return warning; } async function persistAbortedPartial(params) { const { snapshot } = params; if (!snapshot.ok) throw snapshot.error; const appended = await appendAssistantTranscriptMessage({ ...snapshot.value, abortMeta: { ...snapshot.value.abortMeta, ...params.producerSettled ? { producerSettled: true } : {} } }); if (appended.skipped || appended.ok) return; const error = `chat.abort transcript append failed: ${appended.error ?? "unknown error"}`; params.context.logGateway.warn(error); if (snapshot.abortOrigin === "placement-abandon") throw new Error(error); return ABORTED_PARTIAL_PERSISTENCE_WARNING; } async function touchAssistantTranscriptSessionEntry(scope) { if (!scope.storePath || !scope.sessionKey || !scope.sessionId) return; const transcriptMarkerUpdatedAt = Date.now(); await patchSessionEntryCore({ storePath: scope.storePath, sessionKey: scope.sessionKey, ...scope.agentId ? { agentId: scope.agentId } : {} }, (current) => current.sessionId === scope.sessionId ? { updatedAt: transcriptMarkerUpdatedAt } : null, { skipMaintenance: true }); } async function rewriteSourceReplyTranscriptMirrors(params) { if (params.requests.length === 0 || params.candidates.length === 0) return []; return await withTranscriptWriteLock(params.scope, async (transcript) => { const events = await transcript.readEvents(); const allowedSourceReplyMirrorIds = /* @__PURE__ */ new Set(); for (const candidate of params.candidates) { const target = findSourceReplyTranscriptMirrorByMetadataInEvents({ events, idempotencyKey: candidate.idempotencyKey, metadata: candidate.metadata }); if (target) allowedSourceReplyMirrorIds.add(target.messageId); } const rewriteTargets = []; for (const request of params.requests) { const target = findSourceReplyTranscriptMirrorByMetadataInEvents({ events, idempotencyKey: request.idempotencyKey, metadata: request.metadata }); if (target) rewriteTargets.push({ request, ...target }); } if (rewriteTargets.length === 0) return []; const rewriteTargetIds = new Set(rewriteTargets.map((target) => target.messageId)); const firstRewriteEntryIndex = events.findIndex((event) => { const id = transcriptEventId(event); return id ? rewriteTargetIds.has(id) : false; }); if (!(firstRewriteEntryIndex >= 0 && events.slice(firstRewriteEntryIndex).every((event) => { const id = transcriptEventId(event); return !id || allowedSourceReplyMirrorIds.has(id); }))) return []; const replacementsById = new Map(rewriteTargets.map((target) => [target.messageId, target])); const rewrittenEvents = events.map((event) => { const id = transcriptEventId(event); const replacement = id ? replacementsById.get(id) : void 0; if (!replacement) return event; const message = buildAssistantDisplayRewrite({ message: { ...replacement.message, idempotencyKey: replacement.request.idempotencyKey }, displayContent: replacement.request.state.persistedContent, managedMediaUrls: replacement.request.metadata?.mediaUrls }); return Object.assign({}, event, { message }); }); await transcript.replaceEvents(rewrittenEvents); return rewriteTargets.map((target) => ({ messageId: target.messageId, request: target.request })); }); } async function rewriteAssistantTranscriptMessageByIdempotencyKey(params) { const idempotencyKey = params.idempotencyKey.trim(); if (!idempotencyKey || params.content.length === 0) return null; return await withTranscriptWriteLock(params.scope, async (transcript) => { const events = await transcript.readEvents(); const target = findAssistantTranscriptMessageByIdempotencyKeyInEvents(events, idempotencyKey); if (!target) return null; const rewrittenEvents = events.map((event) => transcriptEventId(event) === target.messageId ? Object.assign({}, event, { message: buildAssistantDisplayRewrite({ message: target.message, displayContent: params.content, managedMediaUrls: params.managedMediaUrls }) }) : event); await transcript.replaceEvents(rewrittenEvents); return { messageId: target.messageId }; }); } async function rewriteAssistantTranscriptMessageByTurnIndexAndMedia(params) { if (params.content.length === 0 || params.mediaUrls.length === 0) return null; const currentWatermark = readSessionTranscriptWatermark(params.scope); const initialGenerationMaterialized = params.expectedGeneration === null && params.afterSeq === 0; if (currentWatermark.generation !== params.expectedGeneration && !initialGenerationMaterialized) return null; const currentTurnRows = loadTranscriptEventRowsAfterSeqSync(params.scope, params.afterSeq); const target = findAssistantTranscriptMessageByTurnIndexAndMediaInEvents(currentTurnRows.map((row) => row.event), params); if (!target) return null; const targetRow = currentTurnRows.find((row) => transcriptEventId(row.event) === target.messageId); if (!targetRow) return null; const rewrittenMessage = buildAssistantDisplayRewrite({ message: target.message, displayContent: params.content, managedMediaUrls: params.mediaUrls, retainOriginalText: true }); const rewrittenEvent = Object.assign({}, targetRow.event, { message: rewrittenMessage }); const rewritten = await rewriteTranscriptEventRowsExact(params.scope, { allowInitialGenerationMaterialization: initialGenerationMaterialized, expectedGeneration: params.expectedGeneration, rows: [{ event: rewrittenEvent, expectedEventJson: JSON.stringify(targetRow.event), seq: targetRow.seq }] }); return rewritten ? { generation: rewritten.generation, messageId: target.messageId } : null; } /** Adds managed display media to the completion reply without rewriting model content. */ async function enrichAssistantTranscriptMediaForRun(params) { return await rewriteAssistantTranscriptMessageForRun({ scope: params.scope, runId: params.runId, expectedLifecycleRevision: params.expectedLifecycleRevision, rewriteMessage: (message) => ({ ...buildAssistantDisplayRewrite({ message, displayContent: params.content, managedMediaUrls: params.mediaUrls, retainOriginalText: true }), content: message.content }) }); } async function publishAssistantTranscriptRewrite(params) { if (params.rewritten.length === 0) return; await touchAssistantTranscriptSessionEntry(params.scope); await publishTranscriptUpdate(params.scope, { messageId: params.rewritten.at(-1)?.messageId }); } //#endregion export { persistAbortedPartials as a, rewriteAssistantTranscriptMessageByTurnIndexAndMedia as c, persistAbortedPartial as i, rewriteSourceReplyTranscriptMirrors as l, assistantTranscriptScope as n, publishAssistantTranscriptRewrite as o, enrichAssistantTranscriptMediaForRun as r, rewriteAssistantTranscriptMessageByIdempotencyKey as s, appendAssistantTranscriptMessage as t };