openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
236 lines (235 loc) • 11.2 kB
JavaScript
import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs";
import { a as createSqliteLifecycleAggregateError } from "./sqlite-lifecycle-errors-DuOygjcn.mjs";
import { t as SessionTranscriptWriterClaimReboundError } from "./session-transcript-writer-claim-error-eP-_sDN9.mjs";
import { l as hasSqliteWorkerOutcomeUnknown } from "./sqlite-worker-contract-C51cJHzg.mjs";
import { i as onInternalSessionTranscriptUpdate, s as readSessionTranscriptRunId } from "./transcript-events-DSEdnn1V.mjs";
import { l as runWithOwnedSessionTranscriptWrite, r as captureOwnedTranscriptWriteAssertion } from "./transcript-write-context-DwwjCPCE.mjs";
import { c as getReplyPayloadMetadata } from "./reply-payload-BEgsL2xJ.mjs";
import { s as readAssistantTextBlocksForPhase } from "./chat-message-content-C-d0FKNG.mjs";
import { t as readActiveTranscriptEntryAnchorAsync } from "./session-transcript-anchor-read-D6gfv_oG.mjs";
import { t as ASSISTANT_DISPLAY_CONTENT_FIELD } from "./assistant-display-content-mzartsGP.mjs";
import { n as recordAssistantManagedMediaUrls } from "./transcript-assistant-delivery-CGqOQ0cS.mjs";
import { i as withChannelReadAuthority } from "./channel-read-authority-DJtFPb9x.mjs";
import { n as splitMediaFromOutput } from "./reply-directives-BbFfofv9.mjs";
import { n as rewritePreparedTranscriptMessageAtAnchor } from "./session-message-rewrite-BcYdAXSy.mjs";
import { r as loadGatewaySessionEntry } from "./session-utils-store-qn-zT-Fk.mjs";
import "./session-utils-BLqrCe75.mjs";
import { t as formatForLog } from "./ws-log-DmZrdpqI.mjs";
import { a as createManagedOutgoingMediaBlocks, c as prepareOutgoingMediaFromReplyPayload, n as attachManagedOutgoingMediaToMessage, r as buildManagedMediaFailureBlock, u as removeManagedOutgoingMediaBlocks } from "./managed-image-attachments-CNmXrf8z.mjs";
import { n as getWebchatReplyMediaLocalRoots, r as normalizeWebchatReplyMediaPathsForDisplay, t as captureWebchatReplyMediaScope } from "./chat-reply-media-Bu6kF3Cz.mjs";
import { a as publishAssistantTranscriptRewrite } from "./chat-transcript-persistence-JKSwSL50.mjs";
import { isDeepStrictEqual } from "node:util";
//#region src/gateway/server-methods/chat-send-commentary-media.ts
/** Own authored progress attachments while the caller retains its admitted run. */
function createAssistantCommentaryMediaCustody(params) {
let preparingTranscript = false;
let lastRewrite;
const prepareAssistantTranscriptMessage = (message, sourceText) => {
if (!preparingTranscript || !params.isCurrent() || params.abortSignal?.aborted || !sourceText) return message;
const prepared = recordAssistantManagedMediaUrls(message, splitMediaFromOutput(sourceText).mediaUrls);
return params.prepareAssistantTranscriptMessage?.(prepared, sourceText) ?? prepared;
};
return {
prepareAssistantTranscriptMessage,
get lastRewrite() {
return lastRewrite;
},
async run(operation) {
lastRewrite = void 0;
preparingTranscript = true;
const observer = observeChatSendCommentaryMedia(params);
try {
return await operation();
} finally {
preparingTranscript = false;
lastRewrite = await observer.close();
}
}
};
}
/** Materialize committed progress attachments within the same admitted Gateway run. */
function observeChatSendCommentaryMedia(params) {
const { session } = params;
const seen = /* @__PURE__ */ new Set();
let pending = Promise.resolve();
let uncertainRewrite = false;
let lastRewrite;
const reportPreparationFailure = (error) => {
if (!(error instanceof SessionTranscriptWriterClaimReboundError)) params.logGateway.warn(`webchat commentary media preparation failed: ${formatForLog(error)}`);
};
const unsubscribe = onInternalSessionTranscriptUpdate((update) => {
const message = asOptionalRecord(update.message);
const target = update.target;
const messageId = update.messageId;
if (!target || target.sessionKey !== session.sessionKey || target.agentId !== session.agentId || !messageId || message?.role !== "assistant" || readSessionTranscriptRunId(message) !== params.getRunId() || !params.isCurrent() || uncertainRewrite || !Array.isArray(message.content) || Array.isArray(message["openclawDisplayContent"])) return;
const delivery = asOptionalRecord(message.openclawDelivery);
const authoredMedia = new Set(Array.isArray(delivery?.mediaUrls) ? delivery.mediaUrls.filter((url) => typeof url === "string") : []);
const commentaryBlocks = new Set(readAssistantTextBlocksForPhase(message, "commentary"));
const commentaryIndexes = /* @__PURE__ */ new Set();
const mediaUrls = Array.from(new Set(message.content.flatMap((value, index) => {
const block = asOptionalRecord(value);
if (commentaryBlocks.has(value)) commentaryIndexes.add(index);
return block && commentaryIndexes.has(index) && typeof block.text === "string" ? (splitMediaFromOutput(block.text).mediaUrls ?? []).filter((url) => authoredMedia.has(url)) : [];
})));
const key = `${target.sessionId}:${messageId}`;
if (mediaUrls.length === 0 || seen.has(key)) return;
seen.add(key);
const runId = params.getRunId();
const current = loadGatewaySessionEntry(session.sessionKey, session.sessionLoadOptions);
if (current.entry?.sessionId !== target.sessionId) return;
const lifecycleRevision = current.entry.lifecycleRevision;
const scope = {
...target,
storePath: current.storePath
};
const assertOwned = captureOwnedTranscriptWriteAssertion(scope);
const assertLive = () => {
assertOwned();
if (uncertainRewrite || !params.isCurrent() || params.abortSignal?.aborted || params.getRunId() !== runId) throw new SessionTranscriptWriterClaimReboundError();
};
const assertCurrent = () => {
assertLive();
const latest = loadGatewaySessionEntry(session.sessionKey, session.sessionLoadOptions);
if (latest.entry?.sessionId !== scope.sessionId || latest.entry.lifecycleRevision !== lifecycleRevision) throw new SessionTranscriptWriterClaimReboundError();
};
const originalContent = structuredClone(message.content);
const previous = pending;
pending = runWithOwnedSessionTranscriptWrite({ sessionTarget: scope }, () => previous.then(async () => {
assertCurrent();
let anchor = await readActiveTranscriptEntryAnchorAsync({
...scope,
entryId: messageId
});
assertCurrent();
if (!anchor) {
const { waitForSessionTranscriptProjection } = await import("./config/sessions/session-transcript-reconcile.js");
await waitForSessionTranscriptProjection(scope, params.abortSignal);
assertCurrent();
anchor = await readActiveTranscriptEntryAnchorAsync({
...scope,
entryId: messageId
});
assertCurrent();
}
if (!anchor) return;
const managedMedia = /* @__PURE__ */ new Map();
let committed = false;
try {
const mediaScope = captureWebchatReplyMediaScope({
requesterContext: params.requesterContext,
cfg: session.cfg,
sessionKey: scope.sessionKey,
agentId: scope.agentId,
sessionLoadOptions: session.sessionLoadOptions,
accountId: params.accountId,
assertCurrent
});
await withChannelReadAuthority(mediaScope.assertCurrent, async () => {
const payloads = await normalizeWebchatReplyMediaPathsForDisplay({
...mediaScope,
payloads: mediaUrls.map((url) => ({ mediaUrls: [url] }))
});
const localRoots = getWebchatReplyMediaLocalRoots(mediaScope);
assertCurrent();
for (const [index, payload] of payloads.entries()) {
const blocks = await createManagedOutgoingMediaBlocks({
sessionKey: scope.sessionKey,
agentId: scope.agentId,
items: prepareOutgoingMediaFromReplyPayload(payload),
localRoots,
continueOnPrepareError: true,
assertCurrent: mediaScope.assertCurrent,
abortSignal: params.abortSignal
});
blocks.push(...(getReplyPayloadMetadata(payload)?.assistantMediaFailures ?? []).map(buildManagedMediaFailureBlock));
managedMedia.set(mediaUrls[index], blocks);
assertCurrent();
}
});
const { waitForSessionTranscriptProjection } = await import("./config/sessions/session-transcript-reconcile.js");
await waitForSessionTranscriptProjection(scope, params.abortSignal);
assertCurrent();
const rewritten = await rewritePreparedTranscriptMessageAtAnchor(anchor, (value) => {
assertLive();
const currentMessage = asOptionalRecord(value);
if (!currentMessage || readSessionTranscriptRunId(currentMessage) !== runId || !isDeepStrictEqual(currentMessage.content, originalContent)) return;
const displayContent = [];
for (const [index, raw] of originalContent.entries()) {
const block = asOptionalRecord(raw);
if (!block || !commentaryIndexes.has(index) || typeof block.text !== "string") {
displayContent.push(raw);
continue;
}
const segments = splitMediaFromOutput(block.text).segments ?? [{
type: "text",
text: block.text
}];
displayContent.push({
...block,
text: ""
});
for (const segment of segments) if (segment.type === "text") displayContent.push({
...block,
text: segment.text
});
else displayContent.push(...managedMedia.get(segment.url) ?? [{
...block,
text: `MEDIA:${segment.url}`
}]);
}
return {
...currentMessage,
[ASSISTANT_DISPLAY_CONTENT_FIELD]: displayContent
};
}, {
active: "sequence",
expectedEntry: { lifecycleRevision: lifecycleRevision ?? null },
assertCurrent: assertLive,
assertNativeCurrent: assertCurrent
});
if (rewritten) {
committed = true;
lastRewrite = {
sessionId: scope.sessionId,
generation: rewritten.generation
};
const mediaBlocks = [...managedMedia.values()].flat().filter((block) => block.type === "image" || block.type === "audio" || block.type === "video" || block.type === "attachment");
if (mediaBlocks.length > 0 && !await attachManagedOutgoingMediaToMessage({
messageId,
blocks: mediaBlocks
})) throw new Error("Webchat commentary media ownership could not be persisted");
await publishAssistantTranscriptRewrite({
scope,
rewritten: [{ messageId }]
});
}
} catch (error) {
if (hasSqliteWorkerOutcomeUnknown(error)) {
uncertainRewrite = true;
committed = true;
try {
if (!await attachManagedOutgoingMediaToMessage({
messageId,
blocks: [...managedMedia.values()].flat()
})) throw new Error("Uncertain commentary media ownership could not be retained", { cause: error });
} catch (retentionError) {
throw createSqliteLifecycleAggregateError([error, retentionError], "Transcript rewrite and media retention failed", error);
}
}
throw error;
} finally {
if (!committed) await removeManagedOutgoingMediaBlocks({
blocks: [...managedMedia.values()].flat(),
messageId: null
});
}
}).catch(reportPreparationFailure)).catch(reportPreparationFailure);
});
return { async close() {
unsubscribe();
await pending;
return lastRewrite;
} };
}
//#endregion
export { createAssistantCommentaryMediaCustody as t };