openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
567 lines (566 loc) • 25.7 kB
JavaScript
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 };