UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

567 lines (566 loc) • 25.7 kB
import { m as readNonBlankString } from "./string-coerce-1VuiLaaR.mjs"; import { a as asOptionalRecord, c as isRecord, i as asOptionalObjectRecord } from "./record-coerce-TIsquoVR.mjs"; import { n as filterStringEntries } from "./string-normalization-D6DKCpwE.mjs"; import { i as toDatabaseOptions } from "./session-accessor.sqlite-scope-helpers-CfDTAEsL.mjs"; import { l as releaseSessionSourceAuthorities, s as prepareSessionSourceAuthority, t as acceptSessionSourceValidation } from "./session-source-authority-h1UO3u0I.mjs"; import { r as supportsOpenClawAgentDatabaseExecution } from "./openclaw-agent-execution-C2B2fucX.mjs"; import { d as withOwnedSessionTranscriptWriterFence, r as captureOwnedTranscriptWriteAssertion } from "./transcript-write-context-DwwjCPCE.mjs"; import "./reply-payload-BEgsL2xJ.mjs"; import { t as normalizeMediaReferenceForComparison } from "./media-reference-comparison-D61D3Ct7.mjs"; import { d as resolveSqliteTranscriptScope } from "./session-accessor.sqlite-scope-BkgwxatT.mjs"; import { n as captureIncognitoSessionHistoryBinding, r as captureIncognitoSessionOperation } from "./session-incognito-binding-Dp6RTei2.mjs"; import { h as targetDiscoveryLane } from "./session-transcript-worker-resources-cnBmyJ1M.mjs"; import { t as readSessionTranscriptWatermark } from "./session-accessor.sqlite-transcript-watermark-C7PwEdqC.mjs"; import { s as readAssistantTextBlocksForPhase, t as extractAssistantPhaseText } from "./chat-message-content-C-d0FKNG.mjs"; import { c as loadTranscriptEventRowsAfterSeqSync } from "./session-accessor.sqlite-read-UBWmiUlC.mjs"; import { n as runSessionEntryWorkerOperation } from "./session-entry-patch-fXJDvWHG.mjs"; import { t as withTranscriptLockSettlement } from "./session-transcript-lock-settlement-jhhvQpwf.mjs"; import { t as withSessionTranscriptReadSource } from "./session-transcript-read-source-V0JvL2x_.mjs"; import { d as rewriteTranscriptEventRowsExact, p as withTranscriptWriteSequence } from "./session-accessor.sqlite-transcript-write-ClP59_lJ.mjs"; import { n as publishTranscriptUpdate } from "./session-accessor.sqlite-events-x56_8OQX.mjs"; import { n as applySessionEntryTargetOperation, t as applySessionEntryOperation } from "./session-accessor.sqlite-entry-DLa7_9ui.mjs"; import { r as readAssistantDisplayContent, t as ASSISTANT_DISPLAY_CONTENT_FIELD } from "./assistant-display-content-mzartsGP.mjs"; import { t as applyAssistantDeliveryDirectives } from "./transcript-assistant-delivery-CGqOQ0cS.mjs"; import "./session-accessor-C3ljJ974.mjs"; import { t as executeSessionMessageRewriteOperation } from "./session-message-rewrite-domain-Ll8GY8i_.mjs"; import { a as resolveMirroredTranscriptText } from "./transcript-CpqWjSbD.mjs"; import { n as splitMediaFromOutput } from "./reply-directives-BbFfofv9.mjs"; import { t as rewritePreparedAssistantTranscriptMessageForRun } from "./session-message-rewrite-BcYdAXSy.mjs"; import { u as sanitizeAssistantDisplayText } from "./chat-assistant-content-CZwd1b0s.mjs"; import { n as abortedPartialPersistenceError, t as ABORTED_PARTIAL_PERSISTENCE_WARNING } from "./chat-aborted-partial-C13jjK9r.mjs"; import { t as appendInjectedAssistantMessageToTranscript } from "./chat-transcript-inject-CC_wJAPK.mjs"; import { AsyncLocalStorage } from "node:async_hooks"; import { isMainThread } from "node:worker_threads"; //#region src/config/sessions/session-transcript-correction.ts function selectCorrectionRows(events, replacement, eventJson) { if (replacement.length !== events.length) throw new Error("Transcript correction cannot add or remove events"); return replacement.flatMap((event, index) => { const original = events[index]; if (event === original) return []; if (!isRecord(original) || typeof original.id !== "string") throw new Error("Transcript correction requires an identified event"); return [{ entryId: original.id, expectedEventJson: eventJson[index], event }]; }); } /** Pure display preparation retains its source until exact-row commit and cleanup settle. */ async function withPreparedTranscriptCorrection(requested, run, afterSeq) { const fenced = withOwnedSessionTranscriptWriterFence(requested); const target = resolveSqliteTranscriptScope(fenced); const scope = { ...fenced, sessionId: target.sessionId }; const assertOwned = captureOwnedTranscriptWriteAssertion(scope); const operation = captureIncognitoSessionOperation(scope); const incognito = captureIncognitoSessionHistoryBinding(scope); if (incognito && operation) { const { actor } = incognito; return actor.sessions.withSharedState(async () => { const ownerSource = await prepareSessionSourceAuthority(assertOwned); const failures = []; function refuseOwner(refused) { ownerSource.checks[refused.index]?.refuse(refused.facts); throw new Error("Transcript correction owner refusal omitted its prepared assertion"); } try { if (ownerSource.nativeSource || ownerSource.hasOpaqueCheck || ownerSource.checks.some(({ predicate }) => predicate.source.path !== actor.path || predicate.source.agentId !== actor.agentId || predicate.source.databaseIdentity !== actor.identity.incarnation)) throw new Error("Actor transcript correction requires owner authority prepared for the same actor"); const authority = { assertCurrent: () => { operation.authority.assertCurrent(); (ownerSource.assertPreparedCurrent ?? ownerSource.assertCurrent)(); } }; const input = { ...incognito.target, fence: scope, selectedLifecycleRevision: incognito.target.lifecycleRevision ?? null, ownerSources: ownerSource.checks.map(({ predicate }) => predicate) }; const snapshot = await actor.sessions.transcript(authority, { type: "session.correction.prepare", input: { ...input, afterSeq } }, operation.admissionSignal); if ("refusedOwnerSource" in snapshot) refuseOwner(snapshot.refusedOwnerSource); acceptSessionSourceValidation(ownerSource, snapshot.sourceValidation); authority.assertCurrent(); const eventJson = snapshot.rows.map((row) => row.eventJson); const events = eventJson.map((json) => JSON.parse(json)); const context = { generation: snapshot.version.generation, readEvents: async () => events, replaceEvents: async (replacement) => { authority.assertCurrent(); const rows = selectCorrectionRows(events, replacement, eventJson); const committed = await actor.sessions.transcript(authority, { type: "session.correction.commit", input: { ...input, version: snapshot.version, rows, allowLaterAppends: afterSeq !== void 0 } }, void 0, void 0, void 0, void 0, (_refused, validation) => { acceptSessionSourceValidation(ownerSource, validation); authority.assertCurrent(); }); if ("refusedOwnerSource" in committed) refuseOwner(committed.refusedOwnerSource); context.generation = committed.generation; } }; const result = await withTranscriptLockSettlement((enqueue) => { const queue = AsyncLocalStorage.bind(enqueue); return run({ get generation() { return context.generation; }, readEvents: () => queue(async () => { operation.admissionSignal?.throwIfAborted(); authority.assertCurrent(); if (ownerSource.checks.length) { const validated = await actor.sessions.transcript(authority, { type: "session.lock.facts", input: { ...input, idempotencyKeys: [] } }, operation.admissionSignal); if ("refusedOwnerSource" in validated) refuseOwner(validated.refusedOwnerSource); acceptSessionSourceValidation(ownerSource, validated.sourceValidation); authority.assertCurrent(); } return context.readEvents(); }), replaceEvents: async (replacement) => { operation.admissionSignal?.throwIfAborted(); return queue(() => context.replaceEvents(replacement)); } }); }); authority.assertCurrent(); return result; } catch (error) { failures.push(error); throw error; } finally { await releaseSessionSourceAuthorities([ownerSource], failures); } }); } const runNative = async (native) => { if (afterSeq !== void 0) { const rows = loadTranscriptEventRowsAfterSeqSync(native, afterSeq); const context = { generation: readSessionTranscriptWatermark(native).generation, readEvents: async () => rows.map((row) => row.event), replaceEvents: async (events) => { const rewritten = await rewriteTranscriptEventRowsExact(native, { expectedGeneration: context.generation, rows: events.flatMap((event, index) => event === rows[index]?.event ? [] : [{ event, expectedEventJson: JSON.stringify(rows[index].event), seq: rows[index].seq }]) }); context.generation = rewritten?.generation ?? null; } }; return run(context); } return withTranscriptWriteSequence({ ...scope, ...native }, (locked) => run({ ...locked, generation: readSessionTranscriptWatermark(native).generation })); }; if (!isMainThread || !supportsOpenClawAgentDatabaseExecution(toDatabaseOptions(target))) return runNative(scope); return withSessionTranscriptReadSource(scope, (native) => runNative({ ...scope, ...native }), async (source) => { const assertCurrent = () => { source.assertCurrent(); assertOwned(); }; const database = { ...toDatabaseOptions(source.resolved), path: source.scope.storePath }; const resolved = { ...source.resolved, sessionKey: source.resolved.sessionKey ?? target.sessionKey }; if (!source.expectedIdentity) return run({ generation: null, readEvents: async () => [], replaceEvents: async () => { throw new Error("Cannot correct a missing transcript"); } }); const result = await runSessionEntryWorkerOperation({ database, agentId: resolved.agentId, assertCurrent, candidateKind: "session-transcript-correction", prepareWorker: () => ({ async prepare() { const { restoreSessionColdTranscript } = await import("./session-cold-storage-L7zFwmx1.mjs"); await restoreSessionColdTranscript(source.scope, assertCurrent); }, beforeWrite: assertCurrent, async release() {} }), async run(worker, commit) { const hydration = await source.owner.readTranscript({ target: source.scope, resolvedScope: resolved, expectedIdentity: source.expectedIdentity, afterSeq, includeEventJson: true }); assertCurrent(); if (hydration.kind !== "full" || !hydration.snapshot.eventJson) throw new Error("Transcript correction requires its complete source bytes"); const { events, eventJson, version } = hydration.snapshot; const context = { generation: version.generation, readEvents: async () => events, replaceEvents: async (replacement) => { assertCurrent(); const rows = selectCorrectionRows(events, replacement, eventJson); const committed = await commit(() => executeSessionMessageRewriteOperation(worker, database.agentId, { type: "session.transcript.correct", input: { scope: resolved, fence: scope, version, rows, allowLaterAppends: afterSeq !== void 0 } })); if (!("generation" in committed)) throw new Error("Transcript correction omitted its committed generation"); context.generation = committed.generation; } }; const value = await run(context); assertCurrent(); return { value }; }, onCommitted: ({ generation }) => ({ generation }) }); if (!("value" in result)) throw new Error("Transcript correction omitted its selected result"); return result.value; }, void 0, targetDiscoveryLane); } //#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 ? [...filterStringEntries(previousMedia), ...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 || 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) { return readNonBlankString(asOptionalRecord(event)?.id); } 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.findLast((event) => { const message = transcriptEventMessage(event); return message?.role === "assistant" && message.idempotencyKey === trimmedIdempotencyKey; })); } function mediaReferenceSet(mediaUrls) { return new Set(mediaUrls.map(normalizeMediaReferenceForComparison).filter((value) => value.length > 0)); } function findAssistantTranscriptMessageByTurnIndexAndMediaInEvents(events, params) { const expectedMedia = mediaReferenceSet(params.mediaUrls); if (expectedMedia.size === 0 && params.rejectedMediaCount === 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 parsed = splitMediaFromOutput(text); const actualMedia = mediaReferenceSet(parsed.mediaUrls ?? []); return actualMedia.size === expectedMedia.size && [...expectedMedia].every((value) => actualMedia.has(value)) && (parsed.rejectedMediaCount ?? 0) === params.rejectedMediaCount ? 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.findLast((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 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 appendInjectedAssistantMessageToTranscript({ ...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, readSource) { if (!scope.storePath || !scope.sessionKey || !scope.sessionId) return; const transcriptMarkerUpdatedAt = Date.now(); const target = { storePath: scope.storePath, sessionKey: scope.sessionKey, ...scope.agentId ? { agentId: scope.agentId } : {} }; const operation = { kind: "fields", expected: { sessionId: scope.sessionId }, patch: { updatedAt: transcriptMarkerUpdatedAt } }; if (readSource) await applySessionEntryTargetOperation({ ...target, readSource, target: { canonicalKey: scope.sessionKey, storeKeys: [scope.sessionKey] } }, operation, { skipMaintenance: true }); else await applySessionEntryOperation(target, operation, { skipMaintenance: true }); } async function rewriteSourceReplyTranscriptMirrors(params) { if (params.requests.length === 0 || params.candidates.length === 0) return []; return await withPreparedTranscriptCorrection(params.scope, async (transcript) => { const events = await transcript.readEvents(); const findMirror = (mirror) => findSourceReplyTranscriptMirrorByMetadataInEvents({ ...mirror, events }); const allowedSourceReplyMirrorIds = /* @__PURE__ */ new Set(); for (const candidate of params.candidates) { const target = findMirror(candidate); if (target) allowedSourceReplyMirrorIds.add(target.messageId); } const rewriteTargets = params.requests.flatMap((request) => { const target = findMirror(request); return target ? [{ 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 withPreparedTranscriptCorrection(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 && params.rejectedMediaCount === 0) return null; return withPreparedTranscriptCorrection(params.scope, async (transcript) => { const initialGenerationMaterialized = params.expectedGeneration === null && params.afterSeq === 0; if (transcript.generation !== params.expectedGeneration && !initialGenerationMaterialized) return null; const events = await transcript.readEvents(); const target = findAssistantTranscriptMessageByTurnIndexAndMediaInEvents(events, params); if (!target) return null; const rewrittenMessage = buildAssistantDisplayRewrite({ message: target.message, displayContent: params.content, managedMediaUrls: params.mediaUrls, retainOriginalText: true }); await transcript.replaceEvents(events.map((event) => transcriptEventId(event) === target.messageId ? Object.assign({}, event, { message: rewrittenMessage }) : event)); return transcript.generation ? { generation: transcript.generation, messageId: target.messageId } : null; }, params.afterSeq); } /** Adds managed display media to the completion reply without rewriting model content. */ async function enrichAssistantTranscriptMediaForRun(params) { return await rewritePreparedAssistantTranscriptMessageForRun({ scope: params.scope, readSource: params.readSource, 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, params.readSource); await publishTranscriptUpdate(params.scope, { messageId: params.rewritten.at(-1)?.messageId }, params.readSource); } //#endregion export { publishAssistantTranscriptRewrite as a, rewriteSourceReplyTranscriptMirrors as c, persistAbortedPartials as i, enrichAssistantTranscriptMediaForRun as n, rewriteAssistantTranscriptMessageByIdempotencyKey as o, persistAbortedPartial as r, rewriteAssistantTranscriptMessageByTurnIndexAndMedia as s, assistantTranscriptScope as t };