openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
3,294 lines • 137 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js";
import { i as asOptionalRecord } from "./record-coerce-DHZ4bFlT.js";
import { a as formatUncaughtError, i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js";
import { S as safeFileURLToPath, v as assertNoWindowsNetworkPath } from "./fs-safe-aqmM_n6V.js";
import { r as openLocalFileSafely } from "./secure-temp-dir-XAWcZnE2.js";
import { v as resolveSessionAgentId } from "./agent-scope-MrLta7Pq.js";
import { c as parseAgentSessionKey } from "./session-key-utils-Bx3apsJ3.js";
import { g as scopeLegacySessionKeyToAgent, u as normalizeAgentId } from "./session-key-B_NoIfpX.js";
import { c as resolveDefaultAgentId, n as listAgentIds, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js";
import { l as measureDiagnosticsTimelineSpan, s as emitDiagnosticsTimelineEvent, u as measureDiagnosticsTimelineSpanSync } from "./plugin-metadata-snapshot-Ctk9Oarq.js";
import { r as resolveProviderIdForAuth } from "./provider-auth-aliases-BNZrcvHv.js";
import { a as hasGatewayClientCap, i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES, t as GATEWAY_CLIENT_CAPS } from "./client-info-CcqJJIan.js";
import { t as ADMIN_SCOPE } from "./operator-scopes-CS3xdS-V.js";
import "./method-scopes-BtdN3y7s.js";
import { a as getReplyPayloadTtsSupplement, c as isReplyPayloadTtsSupplement, i as getReplyPayloadMetadata, n as buildTtsSupplementMediaPayload, s as isReplyPayloadStatusNotice } from "./reply-payload-CZJ2cgZc.js";
import { o as isAudioFileName } from "./mime-C8mVE2Bw.js";
import { t as isPassThroughRemoteMediaSource } from "./media-source-url-BL9SUd7E.js";
import "./local-file-access-CBe_wA_B.js";
import { i as resolveSessionStoreKey } from "./session-store-key-BsydjA1h.js";
import { n as deliveryContextFromSession } from "./delivery-context.shared-CpAdEETO.js";
import { t as INTERNAL_MESSAGE_CHANNEL } from "./message-channel-constants-CgI32lqD.js";
import { i as resolveSessionFilePath } from "./paths-NEwU8m3X.js";
import { f as updateSessionStoreEntry } from "./store-Qsgtu-0y.js";
import { i as normalizeMessageChannel } from "./message-channel-normalize-BLLf0ubu.js";
import { a as isOperatorUiClient, n as isGatewayCliClient, o as isWebchatClient } from "./message-channel-BiOeMu0l.js";
import "./sessions-D6zZvoxX.js";
import { n as estimateBase64DecodedBytes } from "./base64-l6yrCyHc.js";
import { o as resolveMirroredTranscriptText, s as appendSessionTranscriptMessage } from "./transcript-Cw-EfeYD.js";
import { t as emitSessionTranscriptUpdate } from "./transcript-events-DTn-thXR.js";
import "./transcript-jsonl-CD0KPCG0.js";
import { i as deleteMediaBuffer, t as MEDIA_MAX_BYTES, u as saveMediaBuffer } from "./store-C9M_0A1p.js";
import { t as resolveAgentTimeoutMs } from "./timeout-Drw0_zOv.js";
import { _ as stripEnvelopeFromMessage, c as readSessionMessageByIdAsync, d as readSessionMessagesAsync, g as readSessionTranscriptIndex, i as readRecentSessionMessagesAsync, n as capArrayByJsonBytes } from "./session-utils.fs-D_8-X4jl.js";
import { r as jsonUtf8Bytes } from "./json-utf8-bytes-C14lActR.js";
import { s as normalizeInputProvenance } from "./input-provenance-CYQvfQZP.js";
import { i as stripInlineDirectiveTagsForDisplay, n as sanitizeReplyDirectiveId } from "./directive-tags-Dg-nkHHo.js";
import { a as getSessionDefaults, b as resolveSessionModelRef, h as resolveGatewayModelSupportsImages, l as loadGatewaySessionRow, o as listAgentsForGateway, p as resolveDeletedAgentIdFromSessionKey, t as buildGatewaySessionInfo, u as loadSessionEntry } from "./session-utils-CHNCuY8a.js";
import { n as resolveSendPolicy } from "./send-policy-CfC5ATxp.js";
import { n as getSessionBindingService } from "./session-binding-service-cfUL2BWM.js";
import { m as resolveSendableOutboundReplyParts } from "./reply-payload-D-VMfpYI.js";
import { t as dispatchInboundMessage } from "./dispatch-DO0Fpkbp.js";
import { r as createUserTurnTranscriptRecorder } from "./user-turn-transcript-BKLlhoCh.js";
import { r as getAgentScopedMediaLocalRoots, t as appendLocalMediaParentRoots } from "./local-roots-BqdtViGS.js";
import { n as assertLocalMediaAllowed, t as LocalMediaAccessError } from "./local-media-access-Bw3v5URj.js";
import { i as normalizeReplyPayloadsForDelivery } from "./payloads-B4DTFTLz.js";
import { in as errorShape, rn as ErrorCodes } from "./schema-BwaBORnA.js";
import { C as validateChatMessageGetParams, E as validateChatSendParams, S as validateChatInjectParams, T as validateChatMetadataParams, t as formatValidationErrors, x as validateChatHistoryParams, y as validateChatAbortParams } from "./src-oj0IwW6K.js";
import { a as ensureSandboxWorkspaceForSession } from "./sandbox-BQ4p-0vf.js";
import { t as rewriteTranscriptEntriesInSessionFile } from "./transcript-rewrite-CFSWWhu_.js";
import { u as isPluginOwnedSessionBindingRecord } from "./conversation-binding-F1u3otwq.js";
import { s as augmentChatHistoryWithCliSessionImports } from "./attempt-execution.helpers-CPoC_qYr.js";
import { n as createReplyMediaPathNormalizer } from "./reply-media-paths.runtime-CgZsH182.js";
import { n as createReplyDispatcher } from "./reply-dispatcher.types-CKL81tAi.js";
import { n as isSuppressedControlReplyText } from "./control-reply-text-F2bAzr1v.js";
import { a as isChatStopCommandText, c as resolveInFlightRunSnapshot, i as boundInFlightRunSnapshotForChatHistory, l as updateChatRunProvider, o as registerChatAbortController, t as abortChatRunById } from "./chat-abort-Cu_ehtms.js";
import { t as createChannelReplyPipeline } from "./reply-pipeline-eJPstCAz.js";
import { n as runAgentHarnessBeforeMessageWriteHook } from "./hook-helpers-B1pwlijz.js";
import "./channel-outbound-B3_Zy-kG.js";
import { t as logLargePayload } from "./diagnostic-payload-DELJ7QXt.js";
import { a as MAX_PAYLOAD_BYTES, c as getMaxChatHistoryMessagesBytes } from "./server-constants-BGwLM6XN.js";
import { t as formatForLog } from "./ws-log-DlzuStZb.js";
import { n as modelCatalogBrowseRequiresFullDiscovery } from "./model-catalog-browse-CKW2B8Bh.js";
import { i as projectChatDisplayMessage, n as augmentChatHistoryWithCanvasBlocks, o as projectRecentChatDisplayMessages, r as dropPreSessionStartAnnouncePairs, s as resolveEffectiveChatHistoryMaxChars } from "./chat-display-projection-Bv2HD4Rr.js";
import { t as stageSandboxMedia } from "./stage-sandbox-media-B8GL16pJ.js";
import { a as resolveChatAttachmentMaxBytes, i as parseMessageWithAttachments, n as MediaOffloadError, r as UnsupportedAttachmentError, t as normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize-CATn3nI8.js";
import { i as createManagedOutgoingImageBlocks, n as attachManagedOutgoingImagesToMessage, r as cleanupManagedOutgoingImageRecords } from "./managed-image-attachments-DW7RaUWq.js";
import { r as resolveSessionHistoryTailReadOptions } from "./session-history-state-B5plVodV.js";
import { n as setGatewayDedupeEntry } from "./agent-wait-dedupe-Cb0P9yCb.js";
import { n as startOptionalServerMethodModelCatalogLoad, t as loadOptionalServerMethodModelCatalog } from "./optional-model-catalog-lfLlMqV0.js";
import fs from "node:fs";
import path from "node:path";
import { createHash } from "node:crypto";
import { performance } from "node:perf_hooks";
//#region src/gateway/chat-input-sanitize.ts
/** Drop disallowed control characters while preserving tab and line breaks. */
function stripDisallowedChatControlChars(message) {
let output = "";
for (const char of message) {
const code = char.charCodeAt(0);
if (code === 9 || code === 10 || code === 13 || code >= 32 && code !== 127) output += char;
}
return output;
}
/** Normalize chat text and reject null bytes before routing to channels. */
function sanitizeChatSendMessageInput(message) {
const normalized = message.normalize("NFC");
if (normalized.includes("\0")) return {
ok: false,
error: "message must not contain null bytes"
};
return {
ok: true,
message: stripDisallowedChatControlChars(normalized)
};
}
//#endregion
//#region src/gateway/server-methods/chat-reply-media.ts
function isDataUrlMedia(mediaUrl) {
return mediaUrl.trim().toLowerCase().startsWith("data:");
}
function shouldPreserveDisplayMediaUrl(payload, mediaUrl) {
if (isDataUrlMedia(mediaUrl)) return true;
if (!isAudioFileName(mediaUrl)) return false;
if (isPassThroughRemoteMediaSource(mediaUrl)) return true;
return payload.trustedLocalMedia === true;
}
/** Normalize reply media paths for webchat display without leaking sensitive media. */
async function normalizeWebchatReplyMediaPathsForDisplay(params) {
if (params.payloads.length === 0) return params.payloads;
const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, params.agentId);
if (!workspaceDir) return params.payloads;
const normalizeMediaPaths = createReplyMediaPathNormalizer({
cfg: params.cfg,
sessionKey: params.sessionKey,
agentId: params.agentId,
workspaceDir,
accountId: params.accountId
});
const normalized = [];
for (const payload of params.payloads) {
if (payload.sensitiveMedia === true) {
normalized.push(payload);
continue;
}
const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
if (!mediaUrls.some((mediaUrl) => shouldPreserveDisplayMediaUrl(payload, mediaUrl))) {
normalized.push(await normalizeMediaPaths(payload));
continue;
}
if (!mediaUrls.some((mediaUrl) => !shouldPreserveDisplayMediaUrl(payload, mediaUrl))) {
normalized.push(payload);
continue;
}
const mergedMediaUrls = [];
const text = payload.text;
for (const mediaUrl of mediaUrls) {
if (shouldPreserveDisplayMediaUrl(payload, mediaUrl)) {
mergedMediaUrls.push(mediaUrl);
continue;
}
const normalizedMediaUrls = resolveSendableOutboundReplyParts(await normalizeMediaPaths({
...payload,
mediaUrl,
mediaUrls: [mediaUrl]
})).mediaUrls;
if (normalizedMediaUrls.length === 0) continue;
mergedMediaUrls.push(...normalizedMediaUrls);
}
normalized.push({
...payload,
text,
mediaUrl: mergedMediaUrls[0],
mediaUrls: mergedMediaUrls
});
}
return normalized;
}
//#endregion
//#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 && typeof first === "object" && 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 messageBody = {
role: "assistant",
content: resolveInjectedAssistantContent({
message: params.message,
label: params.label,
content: params.content
}),
timestamp: now,
stopReason: "stop",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0
}
},
api: "openai-responses",
provider: "openclaw",
model: "gateway-injected",
...params.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : {},
...params.ttsSupplement ? { openclawTtsSupplement: params.ttsSupplement } : {},
...params.abortMeta ? { openclawAbort: {
aborted: true,
origin: params.abortMeta.origin,
runId: params.abortMeta.runId
} } : {}
};
try {
const { messageId, message: appendedMessage } = await appendSessionTranscriptMessage({
transcriptPath: params.transcriptPath,
message: messageBody,
now,
useRawWhenLinear: true,
config: params.config
});
emitSessionTranscriptUpdate({
sessionFile: params.transcriptPath,
...params.sessionKey ? { sessionKey: params.sessionKey } : {},
...params.agentId ? { agentId: params.agentId } : {},
message: appendedMessage,
messageId
});
return {
ok: true,
messageId,
message: appendedMessage
};
} catch (err) {
return {
ok: false,
error: formatErrorMessage(err)
};
}
}
//#endregion
//#region src/gateway/server-methods/chat-webchat-media.ts
/** Cap local audio files exposed through assistant media. */
const MAX_WEBCHAT_AUDIO_BYTES = 15 * 1024 * 1024;
const MAX_WEBCHAT_IMAGE_DATA_URL_CHARS = 2e6;
const MAX_WEBCHAT_IMAGE_DATA_BYTES = 15e5;
const ALLOWED_WEBCHAT_DATA_IMAGE_MEDIA_TYPES = new Set([
"image/apng",
"image/avif",
"image/bmp",
"image/gif",
"image/jpeg",
"image/png",
"image/webp"
]);
const MIME_BY_EXT = {
".aac": "audio/aac",
".m4a": "audio/mp4",
".mp3": "audio/mpeg",
".oga": "audio/ogg",
".ogg": "audio/ogg",
".opus": "audio/opus",
".wav": "audio/wav",
".webm": "audio/webm"
};
/** Map `mediaUrl` strings to an absolute filesystem path for local embedding (plain paths or `file:` URLs). */
function resolveLocalMediaPathForEmbedding(raw) {
const trimmed = raw.trim();
if (!trimmed) return null;
if (/^data:/i.test(trimmed)) return null;
if (/^https?:/i.test(trimmed)) return null;
if (trimmed.startsWith("file:")) try {
const p = safeFileURLToPath(trimmed);
if (!path.isAbsolute(p)) return null;
return p;
} catch {
return null;
}
if (!path.isAbsolute(trimmed)) return null;
try {
assertNoWindowsNetworkPath(trimmed, "Local media path");
} catch {
return null;
}
return trimmed;
}
async function readLocalAudioContentBlockForEmbedding(payload, raw, options) {
if (payload.trustedLocalMedia !== true) return null;
const resolved = resolveLocalMediaPathForEmbedding(raw);
if (!resolved) return null;
if (!isAudioFileName(resolved)) return null;
let opened;
try {
await assertLocalMediaAllowed(resolved, options?.localRoots);
opened = await openLocalFileSafely({ filePath: resolved });
await assertLocalMediaAllowed(opened.realPath, options?.localRoots);
if (opened.stat.size > MAX_WEBCHAT_AUDIO_BYTES) return null;
return {
path: opened.realPath,
block: {
type: "attachment",
attachment: {
url: opened.realPath,
kind: "audio",
label: path.basename(opened.realPath),
mimeType: mimeTypeForPath(opened.realPath),
...payload.audioAsVoice === true ? { isVoiceNote: true } : {}
}
}
};
} catch (err) {
if (err instanceof LocalMediaAccessError) options?.onLocalAudioAccessDenied?.(err);
return null;
} finally {
await opened?.handle.close().catch(() => {});
}
}
async function resolveReplyMediaAudioEmbedding(payload, raw, seenAudio, options) {
const url = raw.trim();
if (!url) return null;
const audio = await readLocalAudioContentBlockForEmbedding(payload, url, options);
if (!audio || seenAudio.has(audio.path)) return { url };
seenAudio.add(audio.path);
return {
url,
audioBlock: audio.block
};
}
function mimeTypeForPath(filePath) {
return MIME_BY_EXT[normalizeLowercaseStringOrEmpty(path.extname(filePath))] ?? "audio/mpeg";
}
function isBase64DataPayload(value) {
if (value.length === 0) return false;
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47 || code === 61) && !(code === 9 || code === 10 || code === 11 || code === 12 || code === 13 || code === 32)) return false;
}
return true;
}
function resolveEmbeddableImageUrl(url) {
const trimmed = url.trim();
if (!trimmed) return null;
if (trimmed.length > MAX_WEBCHAT_IMAGE_DATA_URL_CHARS) return null;
const commaIndex = trimmed.indexOf(",");
if (commaIndex < 0) return null;
const metadata = trimmed.slice(0, commaIndex);
const match = /^data:(image\/[a-z0-9.+-]+);base64$/i.exec(metadata);
const base64Data = trimmed.slice(commaIndex + 1);
if (!match || !isBase64DataPayload(base64Data)) return null;
const mediaType = normalizeLowercaseStringOrEmpty(match[1]);
if (!ALLOWED_WEBCHAT_DATA_IMAGE_MEDIA_TYPES.has(mediaType)) return null;
if (estimateBase64DecodedBytes(base64Data) > MAX_WEBCHAT_IMAGE_DATA_BYTES) return null;
return trimmed;
}
function resolveReplyDirectivePrefix(payload) {
const replyToId = sanitizeReplyDirectiveId(payload.replyToId);
if (replyToId) return `[[reply_to:${replyToId}]]`;
if (payload.replyToCurrent) return "[[reply_to_current]]";
return "";
}
/**
* Build Control UI / transcript `content` blocks for local TTS (or other) audio files
* referenced by slash-command / agent replies when the webchat path only had text aggregation.
*/
async function buildWebchatAudioContentBlocksFromReplyPayloads(payloads, options) {
const seen = /* @__PURE__ */ new Set();
const blocks = [];
for (const payload of payloads) {
if (payload.isReasoning === true) continue;
const parts = resolveSendableOutboundReplyParts(payload);
for (const raw of parts.mediaUrls) {
const media = await resolveReplyMediaAudioEmbedding(payload, raw, seen, options);
if (!media?.audioBlock) continue;
blocks.push(media.audioBlock);
}
}
return blocks;
}
async function buildWebchatAssistantMessageFromReplyPayloads(payloads, options) {
const content = [];
const transcriptTextParts = [];
const seenAudio = /* @__PURE__ */ new Set();
const seenImages = /* @__PURE__ */ new Set();
let hasAudio = false;
let hasImage = false;
for (const payload of payloads) {
if (payload.isReasoning === true) continue;
const visibleText = payload.text?.trim();
const text = visibleText && !isSuppressedControlReplyText(visibleText) ? visibleText : void 0;
const replyDirectivePrefix = resolveReplyDirectivePrefix(payload);
let payloadHasAudio = false;
let payloadHasImage = false;
const payloadMediaBlocks = [];
const parts = resolveSendableOutboundReplyParts(payload);
for (const raw of parts.mediaUrls) {
const media = await resolveReplyMediaAudioEmbedding(payload, raw, seenAudio, options);
if (!media) continue;
if (media.audioBlock) {
payloadMediaBlocks.push(media.audioBlock);
hasAudio = true;
payloadHasAudio = true;
continue;
}
const imageUrl = resolveEmbeddableImageUrl(media.url);
if (!imageUrl || seenImages.has(imageUrl)) continue;
seenImages.add(imageUrl);
payloadMediaBlocks.push({
type: "input_image",
image_url: imageUrl
});
hasImage = true;
payloadHasImage = true;
}
const syntheticText = payloadMediaBlocks.length > 0 && (!text || replyDirectivePrefix) && transcriptTextParts.length === 0 ? payloadHasAudio && payloadHasImage ? "Media reply" : payloadHasAudio ? "Audio reply" : "Image reply" : void 0;
const blockText = text ?? syntheticText;
if (blockText) {
const fullText = replyDirectivePrefix ? `${replyDirectivePrefix}${blockText}` : blockText;
transcriptTextParts.push(fullText);
content.push({
type: "text",
text: fullText
});
} else if (replyDirectivePrefix) {
transcriptTextParts.push(replyDirectivePrefix);
content.push({
type: "text",
text: replyDirectivePrefix
});
}
content.push(...payloadMediaBlocks);
}
if (!hasAudio && !hasImage) return null;
const transcriptText = transcriptTextParts.join("\n\n").trim() || (hasAudio && hasImage ? "Media reply" : hasAudio ? "Audio reply" : "Image reply");
if (transcriptTextParts.length === 0) content.unshift({
type: "text",
text: transcriptText
});
return {
content,
transcriptText
};
}
//#endregion
//#region src/gateway/server-methods/session-active-runs.ts
function collectTrackedActiveSessionRuns(context) {
const runs = [];
if (!(context.chatAbortControllers instanceof Map)) return runs;
for (const active of context.chatAbortControllers.values()) if (active.projectSessionActive !== false && active.controlUiVisible !== false && typeof active.sessionKey === "string" && active.sessionKey.trim()) runs.push({
sessionKey: active.sessionKey,
agentId: typeof active.agentId === "string" ? normalizeAgentId(active.agentId) : void 0
});
return runs;
}
function isTrackedActiveSessionRunForKey(active, key, agentId, defaultAgentId) {
if (active.sessionKey !== key) return false;
if (key !== "global") return true;
const requestedAgentId = agentId ?? defaultAgentId;
if (!requestedAgentId) return true;
const activeAgentId = active.agentId ?? defaultAgentId;
return activeAgentId ? normalizeAgentId(activeAgentId) === normalizeAgentId(requestedAgentId) : false;
}
/** Returns true when either requested or canonical session key has a visible active run. */
function hasTrackedActiveSessionRun(params) {
return collectTrackedActiveSessionRuns(params.context).some((active) => isTrackedActiveSessionRunForKey(active, params.canonicalKey, params.agentId, params.defaultAgentId) || isTrackedActiveSessionRunForKey(active, params.requestedKey, params.agentId, params.defaultAgentId));
}
//#endregion
//#region src/gateway/server-methods/session-change-event.ts
function emitSessionsChanged(context, payload) {
const connIds = context.getSessionEventSubscriberConnIds();
if (connIds.size === 0) return;
const sessionRow = payload.sessionKey ? loadGatewaySessionRow(payload.sessionKey, payload.sessionKey === "global" && payload.agentId ? { agentId: payload.agentId } : void 0) : null;
const omitUnscopedGlobalGoal = payload.sessionKey === "global" && !payload.agentId;
const defaultAgentId = resolveDefaultAgentId(context.getRuntimeConfig());
context.broadcastToConnIds("sessions.changed", {
...payload,
ts: Date.now(),
...sessionRow ? {
updatedAt: sessionRow.updatedAt ?? void 0,
sessionId: sessionRow.sessionId,
kind: sessionRow.kind,
channel: sessionRow.channel,
subject: sessionRow.subject,
groupChannel: sessionRow.groupChannel,
space: sessionRow.space,
chatType: sessionRow.chatType,
origin: sessionRow.origin,
spawnedBy: sessionRow.spawnedBy,
spawnedWorkspaceDir: sessionRow.spawnedWorkspaceDir,
spawnedCwd: sessionRow.spawnedCwd,
forkedFromParent: sessionRow.forkedFromParent,
spawnDepth: sessionRow.spawnDepth,
subagentRole: sessionRow.subagentRole,
subagentControlScope: sessionRow.subagentControlScope,
label: sessionRow.label,
displayName: sessionRow.displayName,
deliveryContext: sessionRow.deliveryContext,
parentSessionKey: sessionRow.parentSessionKey,
childSessions: sessionRow.childSessions,
thinkingLevel: sessionRow.thinkingLevel,
fastMode: sessionRow.fastMode,
verboseLevel: sessionRow.verboseLevel,
traceLevel: sessionRow.traceLevel,
reasoningLevel: sessionRow.reasoningLevel,
elevatedLevel: sessionRow.elevatedLevel,
sendPolicy: sessionRow.sendPolicy,
systemSent: sessionRow.systemSent,
abortedLastRun: sessionRow.abortedLastRun,
inputTokens: sessionRow.inputTokens,
outputTokens: sessionRow.outputTokens,
lastChannel: sessionRow.lastChannel,
lastTo: sessionRow.lastTo,
lastAccountId: sessionRow.lastAccountId,
lastThreadId: sessionRow.lastThreadId,
totalTokens: sessionRow.totalTokens,
totalTokensFresh: sessionRow.totalTokensFresh,
...omitUnscopedGlobalGoal ? {} : { goal: sessionRow.goal ?? null },
contextTokens: sessionRow.contextTokens,
estimatedCostUsd: sessionRow.estimatedCostUsd,
responseUsage: sessionRow.responseUsage,
modelProvider: sessionRow.modelProvider,
model: sessionRow.model,
status: sessionRow.status,
hasActiveRun: hasTrackedActiveSessionRun({
context,
requestedKey: payload.sessionKey ?? sessionRow.key,
canonicalKey: sessionRow.key,
agentId: sessionRow.key === "global" ? payload.agentId : void 0,
defaultAgentId
}),
startedAt: sessionRow.startedAt,
endedAt: sessionRow.endedAt,
runtimeMs: sessionRow.runtimeMs,
compactionCheckpointCount: sessionRow.compactionCheckpointCount,
latestCompactionCheckpoint: sessionRow.latestCompactionCheckpoint,
pluginExtensions: sessionRow.pluginExtensions
} : {}
}, connIds, { dropIfSlow: true });
}
//#endregion
//#region src/gateway/server-methods/chat.ts
function roundedChatSendTimingMs(value) {
return Math.max(0, Math.round(value * 1e3) / 1e3);
}
function chatSendAckServerTimingAttributes(timing) {
if (!timing) return {};
return {
serverReceivedToAckMs: timing.receivedToAckMs,
serverLoadSessionMs: timing.loadSessionMs,
...timing.prepareAttachmentsMs !== void 0 ? { serverPrepareAttachmentsMs: timing.prepareAttachmentsMs } : {}
};
}
function shouldIncludeChatSendAckServerTiming(client) {
return isOperatorUiClient(client);
}
function emitOperatorChatSendServerTiming(params) {
const connId = typeof params.client?.connId === "string" && params.client.connId.trim() ? params.client.connId.trim() : void 0;
if (!connId || !isOperatorUiClient(params.client?.connect?.client)) return;
const nowMs = performance.now();
params.context.broadcastToConnIds("chat.send_timing", {
phase: params.phase,
runId: params.runId,
sessionKey: params.sessionKey,
...params.agentId ? { agentId: params.agentId } : {},
ackToPhaseMs: roundedChatSendTimingMs(nowMs - params.ackedAtMs),
receivedToPhaseMs: roundedChatSendTimingMs(nowMs - params.receivedAtMs),
...params.dispatchStartedAtMs !== void 0 ? { dispatchStartedToPhaseMs: roundedChatSendTimingMs(nowMs - params.dispatchStartedAtMs) } : {},
...params.extra
}, new Set([connId]), { dropIfSlow: true });
}
async function handleChatMetadataRequest({ params, respond, context }) {
if (!validateChatMetadataParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid chat.metadata params: ${formatValidationErrors(validateChatMetadataParams.errors)}`));
return;
}
const metadataParams = params;
const cfg = context.getRuntimeConfig();
const requestedAgentId = typeof metadataParams.agentId === "string" && metadataParams.agentId.trim() ? normalizeAgentId(metadataParams.agentId) : resolveDefaultAgentId(cfg);
if (!listAgentIds(cfg).includes(requestedAgentId)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Unknown agent id "${metadataParams.agentId}"`));
return;
}
try {
respond(true, await buildChatMetadataResult({
cfg,
context,
agentId: requestedAgentId
}));
} catch (err) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, String(err)));
}
}
async function buildChatMetadataResult(params) {
const [{ buildModelsListResult }, { buildCommandsListResult }] = await Promise.all([import("./models-list-result-rQnrqKJL.js"), import("./commands-list-result-Bhv70M6Z.js")]);
const [models, commands] = await Promise.all([buildModelsListResult({
context: params.context,
agentId: params.agentId,
params: { view: "configured" },
preloadedCatalog: params.preloadedModelCatalog
}), Promise.resolve(buildCommandsListResult({
cfg: params.cfg,
agentId: params.agentId,
includeArgs: true,
scope: "text"
}))]);
return {
...models,
...commands
};
}
async function buildChatStartupMetadataResult(params) {
if (!params.modelCatalog) return;
if (modelCatalogBrowseRequiresFullDiscovery({
cfg: params.cfg,
view: "configured"
})) return;
try {
const { buildModelsListResult } = await import("./models-list-result-rQnrqKJL.js");
return await buildModelsListResult({
context: params.context,
agentId: params.agentId,
params: { view: "configured" },
preloadedCatalog: params.modelCatalog
});
} catch (err) {
params.context.logGateway.debug(`chat.startup continuing without metadata: ${formatErrorMessage(err)}`);
return;
}
}
function normalizeUnknownText(value) {
return typeof value === "string" ? normalizeOptionalText(value) : void 0;
}
/** True when a reply payload carries at least one media reference (mediaUrl or mediaUrls). */
function isMediaBearingPayload(payload) {
if (payload.isReasoning === true) return false;
if (payload.mediaUrl?.trim()) return true;
if (payload.mediaUrls?.some((url) => url.trim())) return true;
return false;
}
function stripVisibleTextFromTtsSupplement(payload) {
return isReplyPayloadTtsSupplement(payload) ? buildTtsSupplementMediaPayload(payload) : payload;
}
function resolveTtsSupplementMarkerText(text) {
const trimmed = text.trim();
const projected = projectChatDisplayMessage({
role: "assistant",
content: [{
type: "text",
text: trimmed
}]
}, { maxChars: Number.MAX_SAFE_INTEGER });
return extractAssistantDisplayTextFromContent(Array.isArray(projected?.content) ? projected.content : void 0) ?? (typeof projected?.text === "string" ? projected.text.trim() : void 0) ?? trimmed;
}
function buildTtsSupplementTranscriptMarker(payload) {
const supplement = getReplyPayloadTtsSupplement(payload);
if (!supplement) return;
const visibleText = resolveTtsSupplementMarkerText(payload.text?.trim() || supplement.spokenText.trim());
return { textSha256: createHash("sha256").update(visibleText).digest("hex") };
}
function buildMediaOnlyTtsSupplementTranscriptMarker(payload) {
if (payload.text?.trim()) return;
return buildTtsSupplementTranscriptMarker(payload);
}
function resolveWebchatPromptCacheKey(params) {
return `openclaw-webchat-${createHash("sha256").update([
"v1",
params.provider.trim().toLowerCase(),
params.model.trim(),
normalizeAgentId(params.agentId),
params.sessionKey
].join("\0"), "utf8").digest("hex").slice(0, 32)}`;
}
async function buildWebchatAssistantMediaMessage(payloads, options) {
return buildWebchatAssistantMessageFromReplyPayloads(payloads, {
localRoots: options?.localRoots,
onLocalAudioAccessDenied: (err) => {
options?.onLocalAudioAccessDenied?.(formatForLog(err));
}
});
}
const CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES = 128 * 1024;
const CHAT_HISTORY_OVERSIZED_PLACEHOLDER = "[chat.history omitted: message too large]";
const CHAT_STARTUP_OPTIONAL_MODEL_CATALOG_TIMEOUT_MS = 25;
const MANAGED_OUTGOING_IMAGE_PATH_PREFIX = "/api/chat/media/outgoing/";
let chatHistoryPlaceholderEmitCount = 0;
const chatHistoryManagedImageCleanupState = /* @__PURE__ */ new Map();
const CHANNEL_AGNOSTIC_SESSION_SCOPES = new Set([
"main",
"direct",
"dm",
"group",
"channel",
"cron",
"run",
"subagent",
"acp",
"thread",
"topic"
]);
const CHANNEL_SCOPED_SESSION_SHAPES = new Set([
"direct",
"dm",
"group",
"channel"
]);
const ACTIVE_CHAT_SEND_DEDUPE_PREFIX = "chat:active-send";
function resolveActiveChatSendRunId(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const runId = value.runId;
return typeof runId === "string" && runId.trim() ? runId : null;
}
function clearActiveChatSendDedupeRun(dedupe, key, runId) {
if (!key || resolveActiveChatSendRunId(dedupe.get(key)?.payload) !== runId) return;
dedupe.delete(key);
}
function buildAbortedChatSendPayload(params) {
return {
runId: params.runId,
status: "timeout",
summary: "aborted",
...params.stopReason ? { stopReason: params.stopReason } : {},
endedAt: params.endedAt
};
}
function buildActiveChatSendDedupeKey(params) {
const message = params.message.trim();
if (!message || message.startsWith("/") || params.attachmentCount > 0 || params.explicitDeliverRoute || normalizeMessageChannel(params.originatingChannel) !== "webchat") return null;
const dedupeParts = params.systemScope?.trim() ? [
params.sessionKey,
message,
params.systemScope.trim()
] : [params.sessionKey, message];
return `${ACTIVE_CHAT_SEND_DEDUPE_PREFIX}:${createHash("sha256").update(JSON.stringify(dedupeParts)).digest("hex").slice(0, 32)}`;
}
function validateChatSelectedAgent(params) {
const agentId = params.agentId ? normalizeAgentId(params.agentId) : void 0;
if (!agentId) return { ok: true };
if (!listAgentIds(params.cfg).includes(agentId)) return {
ok: false,
error: `Unknown agent id "${params.agentId}"`
};
const requestedSessionKey = params.requestedSessionKey.trim();
const parsed = parseAgentSessionKey(requestedSessionKey);
if (parsed && normalizeAgentId(parsed.agentId) !== agentId) return {
ok: false,
error: `agentId "${params.agentId}" does not match session key "${params.requestedSessionKey}"`
};
if (requestedSessionKey.toLowerCase() === "global") return {
ok: true,
agentId
};
if (resolveSessionStoreKey({
cfg: params.cfg,
sessionKey: requestedSessionKey
}) === "global") return {
ok: true,
agentId
};
if (!parsed || normalizeAgentId(parsed.agentId) !== agentId) return {
ok: false,
error: `agentId "${params.agentId}" does not match session key "${params.requestedSessionKey}"`
};
return {
ok: true,
agentId
};
}
function resolveRequestedChatAgentId(params) {
const explicitAgentId = normalizeOptionalText(params.agentId);
if (explicitAgentId) return normalizeAgentId(explicitAgentId);
if (!params.cfg) return;
const parsed = parseAgentSessionKey(params.requestedSessionKey.trim());
if (!parsed?.agentId || resolveSessionStoreKey({
cfg: params.cfg,
sessionKey: params.requestedSessionKey
}) !== "global") return;
return normalizeAgentId(parsed.agentId);
}
function resolveChatSendActiveScopeKey(params) {
if (params.sessionKey !== "global" || !params.agentId) return params.sessionKey;
return scopeLegacySessionKeyToAgent({
agentId: params.agentId,
sessionKey: params.sessionKey,
mainKey: params.mainKey
}) ?? params.sessionKey;
}
function formatAttachmentFailureForLog(err) {
const primary = formatUncaughtError(err);
const cause = err instanceof Error ? err.cause : void 0;
if (cause === void 0) return primary;
const causeText = formatUncaughtError(cause);
if (!causeText || causeText === primary) return primary;
return `${primary}\nCaused by: ${causeText}`;
}
function logAttachmentFailure(logGateway, label, err) {
logGateway.error(label, {
error: formatAttachmentFailureForLog(err),
consoleMessage: `${label}: ${formatForLog(err)}`
});
}
function buildTranscriptReplyText(payloads) {
return payloads.map((payload) => {
if (payload.isReasoning === true) return "";
const parts = resolveSendableOutboundReplyParts(payload);
const lines = [];
const replyToId = sanitizeReplyDirectiveId(payload.replyToId);
if (replyToId) lines.push(`[[reply_to:${replyToId}]]`);
else if (payload.replyToCurrent) lines.push("[[reply_to_current]]");
const text = payload.text?.trim();
if (text && !isSuppressedControlReplyText(text)) lines.push(text);
for (const mediaUrl of parts.mediaUrls) {
if (payload.sensitiveMedia === true) continue;
const trimmed = mediaUrl.trim();
if (trimmed) lines.push(`Attachment: ${trimmed}`);
}
if (payload.audioAsVoice && parts.mediaUrls.some((mediaUrl) => isAudioFileName(mediaUrl))) lines.push("[[audio_as_voice]]");
return lines.join("\n").trim();
}).filter(Boolean).join("\n\n").trim();
}
function hasSensitiveMediaPayload(payloads) {
return payloads.some((payload) => payload.sensitiveMedia === true && isMediaBearingPayload(payload));
}
function sanitizeAssistantDisplayText(value) {
if (!value) return;
const withoutEnvelope = stripEnvelopeFromMessage(value);
return stripInlineDirectiveTagsForDisplay(typeof withoutEnvelope === "string" ? withoutEnvelope : value).text.trim() || void 0;
}
function extractAssistantDisplayTextFromContent(content) {
if (!Array.isArray(content) || content.length === 0) return;
const parts = content.map((block) => {
if (block?.type !== "text" || typeof block.text !== "string") return "";
return block.text.trim();
}).filter(Boolean);
return parts.length > 0 ? parts.join("\n\n") : void 0;
}
async function buildAssistantDisplayContentFromReplyPayloads(params) {
const rawTextPayloadCount = params.payloads.filter((payload) => payload.isReasoning !== true && typeof payload.text === "string" && payload.text.trim().length > 0).length;
const normalized = normalizeReplyPayloadsForDelivery(params.payloads);
if (normalized.length === 0) return rawTextPayloadCount > 0 ? [{
type: "text",
text: ""
}] : void 0;
const content = [];
let strippedTextPayloadCount = 0;
for (const payload of normalized) {
const text = sanitizeAssistantDisplayText(payload.text);
if (text) content.push({
type: "text",
text
});
else if (typeof payload.text === "string" && payload.text.trim().length > 0) strippedTextPayloadCount += 1;
if (params.includeSensitiveMedia === false && payload.sensitiveMedia === true) continue;
const audioBlocks = await buildWebchatAudioContentBlocksFromReplyPayloads([payload], {
localRoots: Array.isArray(params.managedImageLocalRoots) ? params.managedImageLocalRoots : void 0,
onLocalAudioAccessDenied: (err) => {
params.onLocalAudioAccessDenied?.(formatForLog(err));
}
});
content.push(...audioBlocks);
const mediaUrls = Array.from(new Set([...Array.isArray(payload.mediaUrls) ? payload.mediaUrls : [], ...typeof payload.mediaUrl === "string" ? [payload.mediaUrl] : []]));
const imageBlocks = await createManagedOutgoingImageBlocks({
sessionKey: params.sessionKey,
...params.sessionKey === "global" && params.agentId ? { agentId: params.agentId } : {},
mediaUrls,
localRoots: params.managedImageLocalRoots,
continueOnPrepareError: true,
onPrepareError: (error) => {
params.onManagedImagePrepareError?.(error.message);
}
});
if (imageBlocks.length > 0) content.push(...imageBlocks);
}
if (content.length > 0) return content;
return strippedTextPayloadCount > 0 ? [{
type: "text",
text: ""
}] : void 0;
}
function replaceAssistantContentTextBlocks(content, transcriptMediaMessage) {
const transcriptTextBlocks = (transcriptMediaMessage?.content ?? []).filter((block) => Boolean(block) && typeof block === "object" && block.type === "text" && typeof block.text === "string");
if (transcriptTextBlocks.length === 0) return content ? [...content] : void 0;
if (!content || content.length === 0) return [...transcriptTextBlocks];
const merged = [];
let transcriptTextIndex = 0;
for (const block of content) {
if (block?.type === "text" && typeof block.text === "string" && transcriptTextIndex < transcriptTextBlocks.length) {
merged.push(transcriptTextBlocks[transcriptTextIndex++]);
continue;
}
merged.push(block);
}
if (transcriptTextIndex < transcriptTextBlocks.length) merged.unshift(...transcriptTextBlocks.slice(transcriptTextIndex));
return merged;
}
function isManagedOutgoingImageUrl(value) {
if (typeof value !== "string" || !value.trim()) return false;
try {
return new URL(value, "http://localhost").pathname.startsWith(MANAGED_OUTGOING_IMAGE_PATH_PREFIX);
} catch {
return false;
}
}
function stripManagedOutgoingAssistantContentBlocks(content) {
if (!content || content.length === 0) return;
const filtered = content.filter((block) => {
if (block?.type !== "image") return true;
return !(isManagedOutgoingImageUrl(block.url) || isManagedOutgoingImageUrl(block.openUrl));
});
return filtered.length > 0 ? filtered : void 0;
}
function extractAssistantDisplayText(content) {
if (!content || content.length === 0) return;
return content.map((block) => block?.type === "text" && typeof block.text === "string" ? block.text : "").filter(Boolean).join("\n\n").trim() || void 0;
}
function hasAssistantDisplayMediaContent(content) {
return Boolean(content?.some((block) => block?.type !== "text"));
}
function hasVisibleAssistantFinalMessage(message) {
if (!message) return false;
if (typeof message.text === "string" && message.text.trim()) return true;
return (Array.isArray(message.content) ? message.content : []).some((block) => {
if (!block || typeof block !== "object") return false;
const record = block;
if (record.type === "text") return typeof record.text === "string" && record.text.trim().length > 0;
return true;
});
}
function hasManagedOutgoingAssistantContent(content) {
return Boolean(content?.some((block) => block?.type === "image" && (isManagedOutgoingImageUrl(block.url) || isManagedOutgoingImageUrl(block.openUrl))));
}
function scheduleChatHistoryManagedImageCleanup(params) {
const cleanupKey = params.sessionKey === "global" && params.agentId ? `agent:${params.agentId}:global` : params.sessionKey;
if (chatHistoryManagedImageCleanupState.has(cleanupKey)) return;
const pending = cleanupManagedOutgoingImageRecords({
sessionKey: params.sessionKey,
...params.sessionKey === "global" && params.agentId ? { agentId: params.agentId } : {}
}).then(() => void 0).catch((error) => {
params.context.logGateway.debug(`chat.history managed image cleanup skipped sessionKey=${JSON.stringify(params.sessionKey)} error=${formatForLog(error)}`);
}).finally(() => {
if (chatHistoryManagedImageCleanupState.get(cleanupKey) === pending) chatHistoryManagedImageCleanupState.delete(cleanupKey);
});
chatHistoryManagedImageCleanupState.set(cleanupKey, pending);
}
function resolveChatSendOriginatingRoute(params) {
if (params.explicitOrigin?.originatingChannel && params.explicitOrigin.originatingTo) return {
originatingChannel: params.explicitOrigin.originatingChannel,
originatingTo: params.explicitOrigin.originatingTo,
...params.explicitOrigin.accountId ? { accountId: params.explicitOrigin.accountId } : {},
...params.explicitOrigin.messageThreadId ? { messageThreadId: params.explicitOrigin.messageThreadId } : {},
explicitDeliverRoute: params.deliver === true
};
if (!(params.deliver === true)) return {
originatingChannel: INTERNAL_MESSAGE_CHANNEL,
explicitDeliverRoute: false
};
const sessionDeliveryContext = deliveryContextFromSession(params.entry);
const routeChannelCandidate = normalizeMessageChannel(sessionDeliveryContext?.channel ?? params.entry?.lastChannel ?? params.entry?.origin?.provider);
const routeToCandidate = sessionDeliveryContext?.to ?? params.entry?.lastTo;
const routeAccountIdCandidate = sessionDeliveryContext?.accountId ?? params.entry?.lastAccountId ?? params.entry?.origin?.accountId ?? void 0;
const routeThreadIdCandidate = sessionDeliveryContext?.threadId ?? params.entry?.lastThreadId ?? params.entry?.origin?.threadId;
if (params.sessionKey.length > 512) return {
originatingChannel: INTERNAL_MESSAGE_CHANNEL,
explicitDeliverRoute: false
};
const sessionScopeParts = (parseAgentSessionKey(params.sessionKey)?.rest ?? params.sessionKey).split(":", 3).filter(Boolean);
const sessionScopeHead = sessionScopeParts[0];
const sessionChannelHint = normalizeMessageChannel(sessionScopeHead);
const normalizedSessionScopeHead = (sessionScopeHead ?? "").trim().toLowerCase();
const sessionPeerShapeCandidates = [sessionScopeParts[1], sessionScopeParts[2]].map((part) => (part ?? "").trim().toLowerCase()).filter(Boolean);
const isChannelAgnosticSessionScope = CHANNEL_AGNOSTIC_SESSION_SCOPES.has(normalizedSessionScopeHead);
const isChannelScopedSession = sessionPeerShapeCandidates.some((part) => CHANNEL_SCOPED_SESSION_SHAPES.has(part));
const hasLegacyChannelPeerShape = !isChannelScopedSession && typeof sessionScopeParts[1] === "string" && sessionChannelHint === routeChannelCandidate;
const isFromWebchatClient = isWebchatClient(params.client);
const isFromGatewayCliClient = isGatewayCliClient(params.client);
const hasClientMetadata = typeof params.client?.mode === "string" && params.client.mode.trim().length > 0 || typeof params.client?.id === "string" && params.client.id.trim().length > 0;
const configuredMainKey = (params.mainKey ?? "main").trim().toLowerCase();
const canInheritConfiguredMainRoute = normalizedSessionScopeHead.length > 0 && normalizedSessionScopeHead === configuredMainKey && params.hasConnectedClient && (isFromGatewayCliClient || !hasClientMetadata);
if (!(Boolean(!isFromWebchatClient && sessionChannelHint && sessionChannelHint !== "webchat" && (!isChannelAgnosticSessionScope && (isChannelScopedSession || hasLegacyChannelPeerShape) || canInheritConfiguredMainRoute)) && routeChannelCandidate && routeChannelCandidate !== "webchat" && typeof routeToCandidate === "string" && routeToCandidate.trim().length > 0)) return {
originatingChannel: INTERNAL_MESSAGE_CHANNEL,
explicitDeliverRoute: false
};
return {
originatingChannel: routeChannelCandidate,
originatingTo: routeToCandidate,
accountId: routeAccountIdCandidate,
messageThreadId: routeThreadIdCandidate,
explicitDeliverRoute: true
};
}
function isAcpSessionKey(sessionKey) {
return Boolean(sessionKey?.split(":").includes("acp"));
}
function explicitOriginTargetsAcpSession(origin) {
if (!origin?.originatingChannel || !origin.originatingTo || !origin.accountId) return false;
const channel = normalizeMessageChannel(origin.originatingChannel);
if (!channel || channel === "webchat") return false;
return isAcpSessionKey(getSessionBindingService().resolveByConversation({
channel,
accountId: origin.accountId,
conversationId: origin.originatingTo
})?.targetSessionKey);
}
function explicitOriginTargetsPluginBinding(origin) {
if (!origin?.originatingChannel || !origin.originatingTo || !origin.accountId) return false;
const channel = normalizeMessageChannel(origin.originatingChannel);
if (!channel || channel === "webchat") return false;
return isPluginOwnedSessionBindingRecord(getSessionBindingService().resolveByConversation({
channel,
accountId: origin.accountId,
conversationId: origin.originatingTo
}));
}
function normalizeOptionalChatSystemReceipt(value) {
if (value == null) return { ok: true };
if (typeof value !== "string") return {
ok: false,
error: "systemProvenanceReceipt must be a string"
};
const sanitized = sanitizeChatSendMessageInput(value);
if (!sanitized.ok) return sanitized;
return {
ok: true,
receipt: sanitized.message.trim() || void 0
};
}
function isAcpBridgeClient(client) {
const info = client?.connect?.client;
return info?.id === GATEWAY_CLIENT_NAMES.CLI && info?.mode === GATEWAY_CLIENT_MODES.CLI && info?.displayName === "ACP" && info?.version === "acp";
}
function canInjectSystemProvenance(client) {
return (Array.isArray(client?.connect?.scopes) ? client.connect.scopes : []).includes(ADMIN_SCOPE);
}
async function persistChatSendImages(params) {
if (params.images.length === 0 && params.offloadedRefs.length === 0 || isAcpBridgeClient(params.client)) return [];
const inlineSaved = [];
for (const img of params.images) try {
inlineSaved.push(await saveMediaBuffer(Buffer.from(img.data, "base64"), img.mimeType, "inbound"));
} catch (err) {
params.logGateway.warn(`chat.send: failed to persist inbound image (${img.mimeType}): ${formatForLog(err)}`);
}
const imageOffloadedSaved = [];
const nonImageOffloadedSaved = [];
for (const ref of params.offloadedRefs) {
const entry = {
id: ref.id,
path: ref.path,
size: 0,
contentType: ref.mimeType
};
if (ref.mimeType.startsWith("image/")) imageOffloadedSaved.push(entry);
else nonImageOffloadedSaved.push(entry);
}
if (params.imageOrder.length === 0) return [
...inlineSaved,
...imageOffloadedSaved,
...nonImageOffloadedSaved
];
const saved = [];
let inlineIndex = 0;
let offloadedIndex = 0;
for (const entry of params.imageOrder) {
if (entry === "inline") {
const inline = inlineSaved[inlineIndex++];
if (inline) saved.push(inline);
continue;
}
const offloaded = imageOffloadedSaved[offloadedIndex++];
if (offloaded) saved.push(offloaded);
}
for (; inlineIndex < inlineSaved.length; inlineIndex++) {
const inline = inlineSaved[inlineIndex];
if (inline) saved.push(inline);
}
for (; offloadedIndex < imageOffloadedSaved.length; offloadedIndex++) {
const offloaded = imageOffloadedSaved[offloadedIndex];
if (offloaded) saved.push(offloaded);
}
for (const offloaded of nonImageOffloadedSaved) saved.push(offloaded);
return saved;
}
function stripTrailingOffloadedMediaMarkers(message, refs) {
if (refs.length === 0) return message;
const removableRefs = new Set(refs.map((ref) => ref.mediaRef));
const lines = message.split(/\r?\n/);
while (lines.length > 0) {
const last = lines[lines.length - 1]?.trim() ?? "";
const match = /^\[media attached:\s*(media:\/\/inbound\/[^\]\s]+)\]$/.exec(last);
if (!match?.[1] || !removableRefs.delete(match[1])) break;
lines.pop();
}
return lines.join("\n").trimEnd();
}
async function prestageMediaPathOffloads(params) {
const mediaPathRefs = params.offloadedRefs.filter((ref) => params.includeImageRefs || !ref.mimeType.startsWith("image/"));
if (mediaPathRefs.length === 0) return {
paths: [],
types: []
};
try {
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
const sandbox = await ensureSandboxWorkspaceForSession({
config: params.cfg,
sessionKey: params.sessionKey,
workspaceDir
});
if (!sandbox) return {
paths: mediaPathRefs.map((ref) => ref.path),
types: mediaPathRefs.map((ref) => ref.mimeType)
};
const oversizedForSandbox = mediaPathRefs.filter((ref) => ref.sizeBytes > MEDIA_MAX_BYTES);
if (oversizedForSandbox.length > 0) throw new UnsupportedAttachmentError("non-image-too-large-for-sandbox", `attachments exceed sandbox staging limit (${MEDIA_MAX_BYTES} bytes): ${oversizedForSandbox.map((ref) => `${ref.label} (${ref.sizeBytes} bytes)`).join(", ")}`);
const stagingCtx = {
MediaPath: mediaPathRefs[0].path,
MediaPaths: mediaPathRefs.map((ref) => ref.path),
MediaType: mediaPathRefs[0].mimeType,
MediaTypes: mediaPathRefs.map((ref) => ref.mimeType)
};
const stagedSources = (await stageSandboxMedia({
ctx: stagingCtx,
sessionCtx: stagingCtx,
cfg: params.cfg,
sessionKey: params.sessionKey,
workspaceDir
})).staged;
const missing = mediaPathRefs.filter((ref) => !stagedSources.has(ref.path));
if (missing.length > 0) throw new Error(`attachment staging incomplete: ${stagedSources.size}/${mediaPathRefs.length} paths staged into sandbox workspace (missing: ${missing.map((ref) => ref.path).join(", ")})`);
return {
paths: stagingCtx.MediaPaths ?? [],
types: stagingCtx.MediaTypes ?? mediaPathRefs.map((ref) => ref.mimeType),
workspaceDir: sandbox.workspaceDir
};
} catch (err) {
await Promise.allSettled(params.offloadedRefs.map((ref) => deleteMediaBuffer(ref.id, "inbound")));
if (err instanceof MediaOffloadError) throw err;
if (err instanceof UnsupportedAttachmentError) throw err;
throw new MediaOffloadError(`[Gateway Error] Failed to stage attachments into agent workspace: ${formatErrorMessage(err)}`, { cause: err });
}
}
function resolveChatSendManagedMediaFields(savedImages) {
const mediaPaths = savedImages.map((entry) => entry.path);
if (mediaPaths.length === 0) return {};
const mediaTypes = savedImages.map((entry) => entry.contentType ?? "application/octet-stream");
return {
MediaPath: mediaPaths[0],
MediaPaths: mediaPaths,
MediaType: mediaTypes[0],
MediaTypes: mediaTypes
};
}
function applyChatSendManagedMediaFields(ctx, fields) {
if (!ctx.MediaStaged) {
Object.assign(ctx, fields);
return;
}
if (ctx.MediaPath === void 0 && fields.MediaPath !== void 0) ctx.MediaPath = fields.MediaPath;
if (ctx.MediaPaths === void 0 && fields.MediaPaths !== void 0) ctx.MediaPaths = fields.MediaPaths;
if (ctx.MediaType === void 0 && fields.MediaType !== void 0) ctx.MediaType = fields.MediaType;
if (ctx.MediaTypes === void 0 && fields.MediaTypes !== void 0) ctx.MediaTypes = fields.MediaTypes;
}
function buildChatSendUserTurnMedia(savedMedia) {
return savedMedia.map((entry) => ({
path: entry.path,
contentType: entry.contentType
}));
}
function buildOversizedHistoryPlaceholder(message) {
const role = message && typeof message === "object" && typeof message.role === "string" ? message.role : "assistant";
const timestamp = message && typeof message === "object" && typeof message.timestamp === "number" ? message.timestamp : Date.now();
const rawMetadata = message && typeof message === "object" ? message["__openclaw"] : void 0;
const metadata = rawMetadata && typeof rawMetadata === "object" && !Array.isArray(rawMetadata) ? rawMetadata : {};
const metadataId = typeof metadata.id === "string" ? metadata.id : void 0;
const metadataSeq = typeof metadata.seq === "number" ? metadata.seq : void 0;
return {
role,
timestamp,
content: [{
type: "text",
text: CHAT_HISTORY_OVERSIZED_PLACEHOLDER
}],
__openclaw: {
...metadataId ? { id: metadataId } : {},
...metadataSeq !== void 0 ? { seq: metadataSeq } : {},
truncated: true,
reason: "oversized"
}
};
}
function replaceOversizedChatHistoryMessages(params) {
const { messages, maxSingleMessageBytes } = params;
if (messages.length === 0) return {
messages,
replacedCount: 0
};
let replacedCount = 0;
const next = messages.map((message) => {
if (jsonUtf8Bytes(message) <= maxSingleMessageBytes) return message;
replacedCount += 1;
return buildOversizedHistoryPlaceholder(message);
});
return {
messages: replacedCount > 0 ? next : messages,
replacedCount
};
}
function enforceChatHistoryFinalBudget(params) {
const { messages, maxBytes } = params;
if (messages.length === 0) return {
messages,
placeholderCount: 0
};
if (jsonUtf8Bytes(messages) <= maxBytes) return {
messages,
placeholderCount: 0
};
const last = messages.at(-1);
if (last && jsonUtf8Bytes([last]) <= maxBytes) return {
messages: [last],
placeholderCount: 0
};
const placeholder = buildOversizedHistoryPlaceholder(last);
if (jsonUtf8Bytes([placeholder]) <= maxBytes) return {
messages: [placeholder],
placeholderCount: 1
};
return {
messages: [],
placeholderCount: 0
};
}
function resolveTranscriptPath(params) {
const { sessionId, storePath, sessionFile, agentId } = params;
if (!storePath && !sessionFile) return null;
try {
const sessionsDir = storePath ? path.dirname(storePath) : void 0;
return resolveSessionFilePath(sessionId, sessionFile ? { sessionFile } : void 0, sessionsDir || agentId ? {
sessionsDir,
agentId
} : void 0);
} catch {
return null;
}
}
function ensureTranscriptFile(params) {
if (fs.existsSync(params.transcriptPath)) return { ok: true };
try {
fs.mkdirSync(path.dirname(params.transcriptPath), { recursive: true });
const header = {
type: "session",
version: 3,
id: params.sessionId,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
cwd: process.cwd()
};
fs.writeFileSync(params.transcriptPath, `${JSON.stringify(header)}\n`, {
encoding: "utf-8",
mode: 384
});
return { ok: true };
} catch (err) {
return {
ok: false,
error: err instanceof Error ? err.message : String(err)
};
}
}
async function findAssistantTranscriptMessageByIdempotencyKey(transcriptPath, idempotencyKey) {
const trimmedIdempotencyKey = idempotencyKey.trim();
if (!trimmedIdempotencyKey) return null;
const target = (await readSessionTranscriptIndex(transcriptPath))?.entries.toReversed().find((entry) => {
const message = entry.record.message;
return message?.role === "assistant" && message.idempotencyKey === trimmedIdempotencyKey;
});
const message = target?.record.message;
if (!target || !message) return null;
return {
messageId: target.id ?? trimmedIdempotencyKey,
message
};
}
async function findSourceReplyTranscriptMirrorByIdempotencyKey(transcriptPath, idempotencyKey) {
const found = await findAssistantTranscriptMessageByIdempotencyKey(transcriptPath, idempotencyKey);
if (found?.message.provider !== "openclaw" || found.message.model !== "delivery-mirror") return null;
return found;
}
function extractAssistantTranscriptText(message) {
const content = message.content;
if (!Array.isArray(content)) return;
return content.map((block) => block && typeof block === "object" && block.type === "text" && typeof block.text === "string" ? block.text.trim() ?? "" : "").filter(Boolean).join("\n").trim() || void 0;
}
async function findSourceReplyTranscriptMirrorByMetadata(params) {
const byIdempotencyKey = await findSourceReplyTranscriptMirrorByIdempotencyKey(params.transcriptPath, params.idempotencyKey);
if (byIdempotencyKey) return byIdempotencyKey;
const expectedText = resolveMirroredTranscriptText({
text: params.metadata?.text,
mediaUrls: params.metadata?.mediaUrls
});
if (!expectedText) return null;
const target = (await readSessionTranscriptIndex(params.transcriptPath))?.entries.toReversed().find((entry) => {
const message = entry.record.message;
return typeof entry.id === "string" && entry.id.trim().length > 0 && message?.role === "assistant" && message.provider === "openclaw" && message.model === "delivery-mirror" && extractAssistantTranscriptText(message) === expectedText;
});
const message = target?.record.message;
if (!target?.id || !message) return null;
return {
messageId: target.id,
message
};
}
async function appendAssistantTranscriptMessage(params) {
const transcriptPath = resolveTranscriptPath({
sessionId: params.sessionId,
storePath: params.storePath,
sessionFile: params.sessionFile,
agentId: params.agentId
});
if (!transcriptPath) return {
ok: false,
error: "transcript path not resolved"
};
if (!fs.existsSync(transcriptPath)) {
if (!params.createIfMissing) return {
ok: false,
error: "transcript file not found"
};
const ensured = ensureTranscriptFile({
transcriptPath,
sessionId: params.sessionId
});
if (!ensured.ok) return {
ok: false,
error: ensured.error ?? "failed to create transcript file"
};
}
if (params.idempotencyKey) {
const existing = await findAssistantTranscriptMessageByIdempotencyKey(transcriptPath, params.idempotencyKey);
if (existing) return {
ok: true,
messageId: existing.messageId,
message: existing.message
};
}
const appended = await appendInjectedAssistantMessageToTranscript({
transcriptPath,
sessionKey: params.sessionKey,
...params.agentId ? { agentId: params.agentId } : {},
message: params.message,
label: params.label,
content: params.content,
idempotencyKey: params.idempotencyKey,
abortMeta: params.abortMeta,
ttsSupplement: params.ttsSupplement,
config: params.cfg
});
if (appended.ok) await advanceSessionTranscriptMarker({
storePath: params.storePath,
sessionKey: params.sessionKey,
sessionId: params.sessionId
});
return appended;
}
async function advanceSessionTranscriptMarker(params) {
if (!params.storePath) return;
const transcriptMarkerUpdatedAt = Date.now();
await updateSessionStoreEntry({
storePath: params.storePath,
sessionKey: params.sessionKey,
update: (current) => current.sessionId === params.sessionId ? { updatedAt: transcriptMarkerUpdatedAt } : null
});
}
function collectSessionAbortPartials(params) {
const out = [];
for (const [runId, active] of params.chatAbortControllers) {
if (!params.runIds.has(runId)) continue;
const text = params.chatRunBuffers.get(runId);
if (!text || !text.trim()) continue;
out.push({
runId,
sessionId: active.sessionId,
agentId: active.agentId,
text,
abortOrigin: params.abortOrigin
});
}
return out;
}
async function persistAbortedPartials(params) {
if (params.snapshots.length === 0) return;
for (const snapshot of params.snapshots) {
const sessionLoadOptions = params.sessionKey === "global" && snapshot.agentId ? { agentId: snapshot.agentId } : void 0;
const { cfg, storePath, entry } = loadSessionEntry(params.sessionKey, sessionLoadOptions);
const sessionId = entry?.sessionId ?? snapshot.sessionId ?? snapshot.runId;
const appended = await appendAssistantTranscriptMessage({
sessionKey: params.sessionKey,
message: snapshot.text,
sessionId,
storePath,
sessionFile: entry?.sessionFile,
...snapshot.agentId ? { agentId: snapshot.agentId } : {},
createIfMissing: true,
idempotencyKey: `${snapshot.runId}:assistant`,
cfg,
abortMeta: {
aborted: true,
origin: snapshot.abortOrigin,
runId: snapshot.runId
}
});
if (!appended.ok) params.context.logGateway.warn(`chat.abort transcript append failed: ${appended.error ?? "unknown error"}`);
}
}
function createChatAbortOps(context) {
return {
chatAbortControllers: context.chatAbortControllers,
chatRunBuffers: context.chatRunBuffers,
chatAbortedRuns: context.chatAbortedRuns,
clearChatRunState: context.clearChatRunState,
removeChatRun: context.removeChatRun,
agentRunSeq: context.agentRunSeq,
getRuntimeConfig: context.getRuntimeConfig,
broadcast: context.broadcast,
nodeSendToSession: context.nodeSendToSession
};
}
function normalizeOptionalText(value) {
return value?.trim() || void 0;
}
function normalizeExplicitChatSendOrigin(params) {
const originatingChannel = normalizeOptionalText(params.originatingChannel);
const originatingTo = normalizeOptionalText(params.originatingTo);
const accountId = normalizeOptionalText(params.accountId);
const messageThreadId = normalizeOptionalText(params.messageThreadId);
if (!Boolean(originatingChannel || originatingTo || accountId || messageThreadId)) return { ok: true };
const normalizedChannel = normalizeMessageChannel(originatingChannel);
if (!normalizedChannel) return {
ok: false,
error: "originatingChannel is required when using originating route fields"
};
if (!originatingTo) return {
ok: false,
error: "originatingTo is required when using originating route fields"
};
return {
ok: true,
value: {
originatingChannel: normalizedChannel,
originatingTo,
...accountId ? { accountId } : {},
...messageThreadId ? { messageThreadId } : {}
}
};
}
function resolveChatAbortRequester(client) {
const scopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : [];
return {
connId: normalizeOptionalText(client?.connId),
deviceId: normalizeOptionalText(client?.connect?.device?.id),
isAdmin: scopes.includes(ADMIN_SCOPE)
};
}
function canRequesterAbortChatRun(entry, requester) {
if (requester.isAdmin) return true;
const ownerDeviceId = normalizeOptionalText(entry.ownerDeviceId);
const ownerConnId = normalizeOptionalText(entry.ownerConnId);
if (!ownerDeviceId && !ownerConnId) return true;
if (ownerDeviceId && requester.deviceId && ownerDeviceId === requester.deviceId) return true;
if (ownerConnId && requester.connId && ownerConnId === requester.connId) return true;
return false;
}
function canRequesterAbortChatRunWithoutSessionMatch(entry, requester) {
if (requester.isAdmin) return true;
const ownerDeviceId = normalizeOptionalText(entry.ownerDeviceId);
const ownerConnId = normalizeOptionalText(entry.ownerConnId);
return Boolean(ownerDeviceId && requester.deviceId && ownerDeviceId === requester.deviceId || ownerConnId && requester.connId && ownerConnId === requester.connId);
}
function readPreRegisteredAgentDedupePayloadForSession(params) {
if (!params.entry?.ok) return;
const payload = params.entry.payload;
if (payload?.status !== "accepted") return;
if (!params.includeHidden && payload.controlUiVisible === false) return;
const payloadRunId = normalizeUnknownText(payload.runId);
if (payloadRunId && payloadRunId !== params.runId) return;
if (normalizeUnknownText(payload.sessionKey) !== params.sessionKey) return;
const agentId = normalizeOptionalText(params.agentId)?.toLowerCase();
if (agentId) {
const parsed = parseAgentSessionKey(params.sessionKey);
const sessionAgentId = params.sessionKey === "global" ? resolveStoredGlobalRunAgentId(normalizeUnknownText(payload.agentId), params.defaultAgentId) : parsed?.agentId ? normalizeAgentId(parsed.agentId) : void 0;
if (sessionAgentId && sessionAgentId !== agentId) return;
}
return payload;
}
function readPreRegisteredAgentRun(params) {
if (!params.key.startsWith("agent:") || !params.entry?.ok) return;
const payload = params.entry.payload;
if (payload?.status !== "accepted") return;
if (payload.controlUiVisible === false) return;
const runId = normalizeUnknownText(payload.runId) ?? normalizeOptionalText(params.key.slice(6));
const sessionKey = normalizeUnknownText(payload.sessionKey);
if (!runId || !sessionKey) return;
return {
runId,
sessionKey,
payload
};
}
function canRequesterAbortPreRegisteredAgentRun(payload, requester) {
return canRequesterAbortChatRun({
controller: new AbortController(),
sessionId: "",
sessionKey: normalizeUnknownText(payload.sessionKey) ?? "",
startedAtMs: 0,
expiresAtMs: 0,
ownerConnId: normalizeUnknownText(payload.ownerConnId),
ownerDeviceId: normalizeUnknownText(payload.ownerDeviceId),
controlUiVisible: payload.controlUiVisible === false ? false : void 0,
kind: "agent"
}, requester);
}
function resolvePreRegisteredAgentDedupeKeys(payload, runId) {
const keys = [`agent:${runId}`];
const payloadKeys = Array.isArray(payload.dedupeKeys) ? payload.dedupeKeys : [];
for (const key of payloadKeys) {
const normalized = normalizeUnknownText(key);
if (normalized?.startsWith("agent:")) keys.push(normalized);
}
return uniqueStrings(keys);
}
function resolveStoredGlobalRunAgentId(agentId, defaultAgentId) {
return normalizeOptionalText(agentId)?.toLowerCase() ?? defaultAgentId.toLowerCase();
}
function writePreRegisteredAgentAbort(params) {
const endedAt = params.endedAt ?? Date.now();
const payloadAgentId = normalizeUnknownText(params.payload.agentId);
for (const key of resolvePreRegisteredAgentDedupeKeys(params.payload, params.runId)) setGatewayDedupeEntry({
dedupe: params.context.dedupe,
key,
entry: {
ts: endedAt,
ok: true,
payload: {
runId: params.runId,
sessionKey: params.sessionKey,
...payloadAgentId ? { agentId: payloadAgentId } : {},
...params.payload.controlUiVisible === false ? { controlUiVisible: false } : {},
status: "timeout",
summary: "aborted",
stopReason: params.stopReason,
endedAt
}
}
});
}
function resolveAuthorizedPreRegisteredAgentRunsForSessionKeys(params) {
const sessionKeys = new Set(Array.from(params.sessionKeys, (sessionKey) => normalizeOptionalText(sessionKey)).filter((sessionKey) => Boolean(sessionKey)));
const authorizedByRunId = /* @__PURE__ */ new Map();
let matchedSessionRuns = 0;
for (const [key, entry] of params.context.dedupe) {
const run = readPreRegisteredAgentRun({
key,
entry
});
if (!run || !sessionKeys.has(run.sessionKey)) continue;
if (params.context.chatAbortControllers.has(run.runId)) continue;
const agentId = normalizeOptionalText(params.agentId)?.toLowerCase();
if (agentId && run.sessionKey === "global" && resolveStoredGlobalRunAgentId(normalizeUnknownText(run.payload.agentId), params.defaultAgentId) !== agentId) continue;
matchedSessionRuns += 1;
if (canRequesterAbortPreRegisteredAgentRun(run.payload, params.requester)) authorizedByRunId.set(run.runId, run);
}
return {
matchedSessionRuns,
authorizedRuns: [...authorizedByRunId.values()]
};
}
function resolveAuthorizedRunsForSessionKeys(params) {
const sessionKeys = new Set(Array.from(params.sessionKeys, (sessionKey) => normalizeOptionalText(sessionKey)).filter((sessionKey) => Boolean(sessionKey)));
const sessionIds = new Set(Array.from(params.sessionIds ?? [], (sessionId) => normalizeOptionalText(sessionId)).filter((sessionId) => Boolean(sessionId)));
const agentId = normalizeOptionalText(params.agentId)?.toLowerCase();
const authorizedRuns = [];
let matchedSessionRuns = 0;
for (const [runId, active] of params.chatAbortControllers) {
if (active.controlUiVisible === false) continue;
if (!sessionKeys.has(active.sessionKey) && !sessionIds.has(active.sessionId)) continue;
if (agentId && active.sessionKey === "global" && resolveStoredGlobalRunAgentId(active.agentId, params.defaultAgentId) !== agentId) continue;
matchedSessionRuns += 1;
if (canRequesterAbortChatRun(active, params.requester)) authorizedRuns.push({
runId,
sessionKey: active.sessionKey
});
}
return {
matchedSessionRuns,
authorizedRuns
};
}
async function abortChatRunsForSessionKeyWithPartials(params) {
const sessionKeys = [params.sessionKey, ...params.sessionKeyAliases ?? []];
const { matchedSessionRuns, authorizedRuns } = resolveAuthorizedRunsForSessionKeys({
chatAbortControllers: params.context.chatAbortControllers,
sessionKeys,
sessionIds: [params.sessionId],
agentId: params.agentId,
defaultAgentId: params.defaultAgentId,
requester: params.requester
});
const { matchedSessionRuns: matchedPendingAgentRuns, authorizedRuns: authorizedPendingAgentRuns } = resolveAuthorizedPreRegisteredAgentRunsForSessionKeys({
context: params.context,
sessionKeys,
agentId: params.agentId,
defaultAgentId: params.defaultAgentId,
requester: params.requester
});
if (authorizedRuns.length === 0 && authorizedPendingAgentRuns.length === 0) return {
aborted: false,
runIds: [],
unauthorized: matchedSessionRuns > 0 || matchedPendingAgentRuns > 0
};
const authorizedRunIdSet = new Set(authorizedRuns.map((run) => run.runId));
const snapshots = collectSessionAbortPartials({
chatAbortControllers: params.context.chatAbortControllers,
chatRunBuffers: params.context.chatRunBuffers,
runIds: authorizedRunIdSet,
abortOrigin: params.abortOrigin
});
const runIds = [];
for (const { runId, sessionKey } of authorizedRuns) if (abortChatRunById(params.ops, {
runId,
sessionKey,
stopReason: params.stopReason
}).aborted) runIds.push(runId);
const endedAt = Date.now();
const stopReason = params.stopReason ?? "rpc";
for (const { runId, sessionKey, payload } of authorizedPendingAgentRuns) {
writePreRegisteredAgentAbort({
context: params.context,
runId,
sessionKey,
payload,
stopReason,
endedAt
});
runIds.push(runId);
}
const res = {
aborted: runIds.length > 0,
runIds,
unauthorized: false
};
if (res.aborted) await persistAbortedPartials({
context: params.context,
sessionKey: params.persistSessionKey ?? params.sessionKey,
snapshots
});
return res;
}
function nextChatSeq(context, runId) {
const next = (context.agentRunSeq.get(runId) ?? 0) + 1;
context.agentRunSeq.set(runId, next);
return next;
}
function broadcastChatFinal(params) {
const seq = nextChatSeq({ agentRunSeq: params.context.agentRunSeq }, params.runId);
const payloadAgentId = params.sessionKey === "global" ? params.agentId : void 0;
const payload = {
runId: params.runId,
sessionKey: params.sessionKey,
...payloadAgentId ? { agentId: payloadAgentId } : {},
seq,
state: "final",
message: projectChatDisplayMessage(params.message)
};
params.context.broadcast("chat", payload);
sendGlobalAwareNodeChatPayload({
context: params.context,
sessionKey: params.sessionKey,
agentId: payloadAgentId,
event: "chat",
payload
});
params.context.agentRunSeq.delete(params.runId);
}
function isBtwReplyPayload(payload) {
return typeof payload?.btw?.question === "string" && payload.btw.question.trim().length > 0 && typeof payload.text === "string" && payload.text.trim().length > 0;
}
function broadcastSideResult(params) {
const seq = nextChatSeq({ agentRunSeq: params.context.agentRunSeq }, params.payload.runId);
const payloadAgentId = params.payload.sessionKey === "global" ? params.payload.agentId : void 0;
const payload = {
...params.payload,
...payloadAgentId ? { agentId: payloadAgentId } : {},
seq
};
params.context.broadcast("chat.side_result", payload);
sendGlobalAwareNodeChatPayload({
context: params.context,
sessionKey: params.payload.sessionKey,
agentId: payloadAgentId,
event: "chat.side_result",
payload
});
}
function broadcastChatError(params) {
const seq = nextChatSeq({ agentRunSeq: params.context.agentRunSeq }, params.runId);
const payloadAgentId = params.sessionKey === "global" ? params.agentId : void 0;
const errorText = params.errorMessage?.trim();
const payload = {
runId: params.runId,
sessionKey: params.sessionKey,
...payloadAgentId ? { agentId: payloadAgentId } : {},
seq,
state: "error",
errorMessage: params.errorMessage,
...errorText ? { message: {
role: "assistant",
content: [{
type: "text",
text: errorText.startsWith("⚠️") || errorText.startsWith("Error:") ? errorText : `Error: ${errorText}`
}],
timestamp: Date.now()
} } : {}
};
params.context.broadcast("chat", payload);
sendGlobalAwareNodeChatPayload({
context: params.context,
sessionKey: params.sessionKey,
agentId: payloadAgentId,
event: "chat",
payload
});
params.context.agentRunSeq.delete(params.runId);
}
function sendGlobalAwareNodeChatPayload(params) {
const deliveryKeys = resolveGlobalAwareNodeChatDeliveryKeys({
cfg: params.context.getRuntimeConfig?.() ?? {},
sessionKey: params.sessionKey,
agentId: params.agentId
});
for (const deliveryKey of deliveryKeys) params.context.nodeSendToSession(deliveryKey, params.event, params.payload);
}
function resolveGlobalAwareNodeChatDeliveryKeys(params) {
if (params.sessionKey !== "global") return [params.sessionKey];
const defaultAgentId = resolveDefaultAgentId(params.cfg);
const scopedAgentId = params.agentId ?? defaultAgentId;
const keys = [`agent:${scopedAgentId}:global`];
if (scopedAgentId === defaultAgentId) keys.push("global");
return keys;
}
function isSourceReplyTranscriptMirrorPayload(payload) {
return Boolean(payload && getReplyPayloadMetadata(payload)?.sourceReplyTranscriptMirror);
}
function readChatHistoryMessageId(message) {
const metadata = asOptionalRecord(asOptionalRecord(message)?.["__openclaw"]);
return typeof metadata?.id === "string" ? metadata.id : void 0;
}
async function isChatMessageIdVisibleAfterHistoryFilters(params) {
if (params.sessionStartedAt === void 0) return true;
return dropPreSessionStartAnnouncePairs(await readSessionMessagesAsync(params.sessionId, params.storePath, params.sessionFile, {
mode: "full",
reason: "chat.message.get visibility"
}), params.sessionStartedAt).some((message) => readChatHistoryMessageId(message) === params.messageId);
}
function dropLocalHistoryOverreadContextMessage(messages, contextMessage) {
if (contextMessage === void 0) return messages;
const index = messages.indexOf(contextMessage);
if (index < 0) return messages;
return [...messages.slice(0, index), ...messages.slice(index + 1)];
}
async function handleChatHistoryRequest({ params, respond, context, method, includeAgentsList, includeMetadata }) {
if (!validateChatHistoryParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid ${method} params: ${formatValidationErrors(validateChatHistoryParams.errors)}`));
return;
}
const { sessionKey, limit, maxChars } = params;
const agentIdOverride = normalizeOptionalText(params.agentId);
const requestedAgentId = resolveRequestedChatAgentId({
cfg: context.getRuntimeConfig?.(),
requestedSessionKey: sessionKey,
agentId: agentIdOverride
});
const { cfg, storePath, store, entry, canonicalKey } = loadSessionEntry(sessionKey, requestedAgentId ? { agentId: requestedAgentId } : void 0);
const selectedAgent = validateChatSelectedAgent({
cfg,
requestedSessionKey: sessionKey,
agentId: requestedAgentId
});
if (!selectedAgent.ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, selectedAgent.error));
return;
}
const startupModelCatalogLoad = method === "chat.startup" ? startOptionalServerMethodModelCatalogLoad(context) : void 0;
const modelCatalogPromise = measureDiagnosticsTimelineSpan(`gateway.${method}.model_catalog`, () => startupModelCatalogLoad ? loadOptionalServerMethodModelCatalog(context, method, {
logOnceKey: "chat.startup",
startedLoad: startupModelCatalogLoad,
timeoutMs: CHAT_STARTUP_OPTIONAL_MODEL_CATALOG_TIMEOUT_MS
}) : loadOptionalServerMethodModelCatalog(context, method), {
config: cfg,
phase: method
});
if (startupModelCatalogLoad) modelCatalogPromise.catch(() => void 0);
const sessionId = entry?.sessionId;
const sessionAgentId = resolveSessionAgentId({
sessionKey,
config: cfg,
agentId: selectedAgent.agentId
});
const resolvedSessionModel = resolveSessionModelRef(cfg, entry, sessionAgentId);
const max = Math.min(1e3, typeof limit === "number" ? limit : 200);
const maxHistoryBytes = getMaxChatHistoryMessagesBytes();
const rawHistoryWindow = resolveSessionHistoryTailReadOptions(max);
const localHistoryReadOptions = {
maxMessages: rawHistoryWindow.maxMessages + 1,
maxLines: rawHistoryWindow.maxLines + 1
};
const localMessages = sessionId && storePath ? await readRecentSessionMessagesAsync(sessionId, storePath, entry?.sessionFile, {
...localHistoryReadOptions,
maxBytes: Math.max(maxHistoryBytes * 2, 1024 * 1024)
}) : [];
const overreadContextMessage = localMessages.length > rawHistoryWindow.maxMessages ? localMessages[0] : void 0;
const localMessagesWithBoundaryFilter = dropLocalHistoryOverreadContextMessage(dropPreSessionStartAnnouncePairs(localMessages, typeof entry?.sessionStartedAt === "number" ? entry.sessionStartedAt : void 0), overreadContextMessage);
const normalized = augmentChatHistoryWithCanvasBlocks(projectRecentChatDisplayMessages(dropPreSessionStartAnnouncePairs(augmentChatHistoryWithCliSessionImports({
entry,
provider: resolvedSessionModel.provider,
localMessages: localMessagesWithBoundaryFilter
}), typeof entry?.sessionStartedAt === "number" ? entry.sessionStartedAt : void 0), {
maxChars: resolveEffectiveChatHistoryMaxChars(cfg, maxChars),
maxMessages: max
}));
const replaced = replaceOversizedChatHistoryMessages({
messages: normalized,
maxSingleMessageBytes: Math.min(CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES, maxHistoryBytes)
});
scheduleChatHistoryManagedImageCleanup({
sessionKey,
...selectedAgent.agentId ? { agentId: selectedAgent.agentId } : {},
context
});
const capped = capArrayByJsonBytes(replaced.messages, maxHistoryBytes).items;
const bounded = enforceChatHistoryFinalBudget({
messages: capped,
maxBytes: maxHistoryBytes
});
const placeholderCount = replaced.replacedCount + bounded.placeholderCount;
if (placeholderCount > 0) {
chatHistoryPlaceholderEmitCount += placeholderCount;
logLargePayload({
surface: "gateway.chat.history",
action: "truncated",
bytes: jsonUtf8Bytes(normalized),
limitBytes: maxHistoryBytes,
count: placeholderCount,
reason: "chat_history_budget"
});
context.logGateway.debug(`chat.history omitted oversized payloads placeholders=${placeholderCount} total=${chatHistoryPlaceholderEmitCount}`);
}
const modelCatalog = await modelCatalogPromise;
const defaultAgentId = resolveDefaultAgentId(cfg);
const startupMetadata = includeMetadata ? await buildChatStartupMetadataResult({
cfg,
context,
agentId: sessionAgentId,
modelCatalog
}) : void 0;
const sessionInfo = buildGatewaySessionInfo({
cfg,
storePath,
store,
key: canonicalKey,
entry,
agentId: selectedAgent.agentId,
modelCatalog
});
const activeRunAgentId = canonicalKey === "global" ? selectedAgent.agentId ?? defaultAgentId : selectedAgent.agentId;
sessionInfo.hasActiveRun = hasTrackedActiveSessionRun({
context,
requestedKey: sessionKey,
canonicalKey,
...activeRunAgentId ? { agentId: activeRunAgentId } : {},
defaultAgentId
});
const defaults = getSessionDefaults(cfg, modelCatalog, { allowPluginNormalization: false });
const thinkingLevel = sessionInfo.thinkingLevel ?? sessionInfo.thinkingDefault;
const verboseLevel = entry?.verboseLevel ?? cfg.agents?.defaults?.verboseDefault;
sessionInfo.verboseLevel = verboseLevel;
const boundedInFlightRun = boundInFlightRunSnapshotForChatHistory({
snapshot: resolveInFlightRunSnapshot({
chatAbortControllers: context.chatAbortControllers,
chatRunBuffers: context.chatRunBuffers,
requestedSessionKey: sessionKey,
canonicalSessionKey: resolveSessionStoreKey({
cfg,
sessionKey
}),
agentId: activeRunAgentId,
defaultAgentId
}),
messages: bounded.messages,
maxBytes: maxHistoryBytes
});
respond(true, {
sessionKey,
sessionId,
messages: bounded.messages,
defaults,
sessionInfo,
thinkingLevel,
fastMode: entry?.fastMode,
verboseLevel,
...boundedInFlightRun ? { inFlightRun: boundedInFlightRun } : {},
...includeAgentsList ? { agentsList: listAgentsForGateway(cfg, modelCatalog) } : {},
...startupMetadata ? { metadata: startupMetadata } : {}
});
}
const chatHandlers = {
"chat.history": async (opts) => {
await handleChatHistoryRequest({
...opts,
method: "chat.history"
});
},
"chat.startup": async (opts) => {
await handleChatHistoryRequest({
...opts,
method: "chat.startup",
includeAgentsList: true,
includeMetadata: true
});
},
"chat.metadata": handleChatMetadataRequest,
"chat.message.get": async ({ params, respond, context }) => {
if (!validateChatMessageGetParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid chat.message.get params: ${formatValidationErrors(validateChatMessageGetParams.errors)}`));
return;
}
const { sessionKey, messageId, maxChars } = params;
const agentIdOverride = normalizeOptionalText(params.agentId);
const requestedAgentId = resolveRequestedChatAgentId({
cfg: context.getRuntimeConfig?.(),
requestedSessionKey: sessionKey,
agentId: agentIdOverride
});
const { cfg, storePath, entry } = loadSessionEntry(sessionKey, requestedAgentId ? { agentId: requestedAgentId } : void 0);
const selectedAgent = validateChatSelectedAgent({
cfg,
requestedSessionKey: sessionKey,
agentId: requestedAgentId
});
if (!selectedAgent.ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, selectedAgent.error));
return;
}
const sessionId = entry?.sessionId;
if (!sessionId) {
respond(true, {
ok: false,
unavailableReason: "not_found"
});
return;
}
const resolved = await readSessionMessageByIdAsync(sessionId, storePath, entry?.sessionFile, messageId);
if (!resolved.found) {
respond(true, {
ok: false,
unavailableReason: "not_found"
});
return;
}
if (!await isChatMessageIdVisibleAfterHistoryFilters({
sessionId,
storePath,
sessionFile: entry?.sessionFile,
messageId,
sessionStartedAt: typeof entry?.sessionStartedAt === "number" ? entry.sessionStartedAt : void 0
})) {
respond(true, {
ok: false,
unavailableReason: "not_found"
});
return;
}
if (resolved.oversized) {
respond(true, {
ok: false,
unavailableReason: "oversized"
});
return;
}
const effectiveMaxChars = typeof maxChars === "number" ? maxChars : Math.min(MAX_PAYLOAD_BYTES, 1e6);
const projectedMessage = resolved.message ? projectChatDisplayMessage(resolved.message, { maxChars: effectiveMaxChars }) : void 0;
const projected = projectedMessage ? augmentChatHistoryWithCanvasBlocks([projectedMessage])[0] : void 0;
if (!projected) {
respond(true, {
ok: false,
unavailableReason: "not_visible"
});
return;
}
respond(true, {
ok: true,
message: projected
});
},
"chat.abort": async ({ params, respond, context, client }) => {
if (!validateChatAbortParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid chat.abort params: ${formatValidationErrors(validateChatAbortParams.errors)}`));
return;
}
const { sessionKey: rawSessionKey, runId } = params;
const agentIdOverride = normalizeOptionalText(params.agentId);
const abortCfg = context.getRuntimeConfig();
const defaultAgentId = resolveDefaultAgentId(abortCfg);
const parsedAbortSessionKey = parseAgentSessionKey(rawSessionKey);
const abortSessionResolvesGlobal = resolveSessionStoreKey({
cfg: abortCfg,
sessionKey: rawSessionKey
}) === "global";
const inferredGlobalAgentId = !agentIdOverride && parsedAbortSessionKey && abortSessionResolvesGlobal ? normalizeAgentId(parsedAbortSessionKey.agentId) : void 0;
const abortAgentId = agentIdOverride ?? inferredGlobalAgentId ?? (abortSessionResolvesGlobal ? defaultAgentId : void 0);
if (agentIdOverride && parsedAbortSessionKey && normalizeAgentId(parsedAbortSessionKey.agentId) !== normalizeAgentId(agentIdOverride)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `agentId "${agentIdOverride}" does not match session key "${rawSessionKey}"`));
return;
}
const canonicalAbortSessionKey = abortAgentId && abortSessionResolvesGlobal ? "global" : rawSessionKey;
const ops = createChatAbortOps(context);
const requester = resolveChatAbortRequester(client);
if (!runId) {
const res = await abortChatRunsForSessionKeyWithPartials({
context,
ops,
sessionKey: canonicalAbortSessionKey,
sessionKeyAliases: canonicalAbortSessionKey === rawSessionKey ? void 0 : [rawSessionKey],
agentId: abortAgentId,
defaultAgentId,
abortOrigin: "rpc",
stopReason: "rpc",
requester
});
if (res.unauthorized) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized"));
return;
}
respond(true, {
ok: true,
aborted: res.aborted,
runIds: res.runIds
});
return;
}
const normalizedAgentIdOverride = abortAgentId?.toLowerCase();
const active = context.chatAbortControllers.get(runId);
if (!active) {
const pendingAgentEntry = context.dedupe.get(`agent:${runId}`);
const pendingAgentMatch = (() => {
const canonicalMatch = readPreRegisteredAgentDedupePayloadForSession({
entry: pendingAgentEntry,
runId,
sessionKey: canonicalAbortSessionKey,
agentId: abortAgentId,
defaultAgentId,
includeHidden: true
});
if (canonicalMatch) return {
sessionKey: canonicalAbortSessionKey,
payload: canonicalMatch
};
if (rawSessionKey === canonicalAbortSessionKey) return;
const aliasMatch = readPreRegisteredAgentDedupePayloadForSession({
entry: pendingAgentEntry,
runId,
sessionKey: rawSessionKey,
agentId: abortAgentId,
defaultAgentId,
includeHidden: true
});
return aliasMatch ? {
sessionKey: rawSessionKey,
payload: aliasMatch
} : void 0;
})();
if (pendingAgentMatch) {
const pendingAgentPayload = pendingAgentMatch.payload;
if (!canRequesterAbortPreRegisteredAgentRun(pendingAgentPayload, requester)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized"));
return;
}
writePreRegisteredAgentAbort({
context,
runId,
sessionKey: pendingAgentMatch.sessionKey,
payload: pendingAgentPayload,
stopReason: "rpc"
});
respond(true, {
ok: true,
aborted: true,
runIds: [runId]
});
return;
}
respond(true, {
ok: true,
aborted: false,
runIds: []
});
return;
}
if (!new Set([rawSessionKey, canonicalAbortSessionKey]).has(active.sessionKey) && !canRequesterAbortChatRunWithoutSessionMatch(active, requester)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "runId does not match sessionKey"));
return;
}
if (normalizedAgentIdOverride && active.sessionKey === "global" && resolveStoredGlobalRunAgentId(active.agentId, defaultAgentId) !== normalizedAgentIdOverride) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "runId does not match agentId"));
return;
}
if (!canRequesterAbortChatRun(active, requester)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized"));
return;
}
const partialText = context.chatRunBuffers.get(runId);
const res = abortChatRunById(ops, {
runId,
sessionKey: active.sessionKey,
stopReason: "rpc"
});
if (res.aborted && active.controlUiVisible !== false && partialText && partialText.trim()) await persistAbortedPartials({
context,
sessionKey: active.sessionKey,
snapshots: [{
runId,
sessionId: active.sessionId,
agentId: active.agentId,
text: partialText,
abortOrigin: "rpc"
}]
});
respond(true, {
ok: true,
aborted: res.aborted,
runIds: res.aborted ? [runId] : []
});
},
"chat.send": async ({ params, respond, context, client }) => {
const chatSendReceivedAtMs = performance.now();
if (!validateChatSendParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid chat.send params: ${formatValidationErrors(validateChatSendParams.errors)}`));
return;
}
const p = params;
const suppressCommandInterpretation = p.suppressCommandInterpretation === true;
const explicitOriginResult = normalizeExplicitChatSendOrigin({
originatingChannel: p.originatingChannel,
originatingTo: p.originatingTo,
accountId: p.originatingAccountId,
messageThreadId: p.originatingThreadId
});
if (!explicitOriginResult.ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, explicitOriginResult.error));
return;
}
if ((p.systemInputProvenance || p.systemProvenanceReceipt || suppressCommandInterpretation || explicitOriginResult.value) && !canInjectSystemProvenance(client)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, p.systemInputProvenance || p.systemProvenanceReceipt || suppressCommandInterpretation ? "system provenance fields require admin scope" : "originating route fields require admin scope"));
return;
}
const sanitizedMessageResult = sanitizeChatSendMessageInput(p.message);
if (!sanitizedMessageResult.ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, sanitizedMessageResult.error));
return;
}
const systemReceiptResult = normalizeOptionalChatSystemReceipt(p.systemProvenanceReceipt);
if (!systemReceiptResult.ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, systemReceiptResult.error));
return;
}
const inboundMessage = sanitizedMessageResult.message;
const systemInputProvenance = normalizeInputProvenance(p.systemInputProvenance);
const systemProvenanceReceipt = systemReceiptResult.receipt;
const systemDedupeScope = systemInputProvenance || systemProvenanceReceipt ? JSON.stringify([systemProvenanceReceipt ?? null, systemInputProvenance ?? null]) : void 0;
const stopCommand = !suppressCommandInterpretation && isChatStopCommandText(inboundMessage);
const normalizedAttachments = normalizeRpcAttachmentsToChatAttachments(p.attachments);
const rawMessage = inboundMessage.trim();
if (!rawMessage && normalizedAttachments.length === 0) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "message or attachment required"));
return;
}
const rawSessionKey = p.sessionKey;
const agentIdOverride = normalizeOptionalText(p.agentId);
const clientRunId = p.idempotencyKey;
const requestedAgentId = resolveRequestedChatAgentId({
cfg: context.getRuntimeConfig?.(),
requestedSessionKey: rawSessionKey,
agentId: agentIdOverride
});
const sessionLoadOptions = requestedAgentId ? { agentId: requestedAgentId } : void 0;
const sessionLoadStartedAtMs = performance.now();
const sessionLoadResult = measureDiagnosticsTimelineSpanSync("gateway.chat_send.load_session", () => loadSessionEntry(rawSessionKey, sessionLoadOptions), {
phase: "agent-turn",
attributes: {
runId: clientRunId,
hasAttachments: normalizedAttachments.length > 0,
hasExplicitOrigin: explicitOriginResult.value !== void 0
}
});
const sessionLoadMs = roundedChatSendTimingMs(performance.now() - sessionLoadStartedAtMs);
const { cfg, entry, canonicalKey: sessionKey, legacyKey } = sessionLoadResult;
const selectedAgent = validateChatSelectedAgent({
cfg,
requestedSessionKey: rawSessionKey,
agentId: requestedAgentId
});
if (!selectedAgent.ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, selectedAgent.error));
return;
}
const requestedSessionId = normalizeOptionalText(p.sessionId);
const backingSessionId = entry?.sessionId ?? requestedSessionId;
const deletedAgentId = resolveDeletedAgentIdFromSessionKey(cfg, sessionKey, entry, { acpMetadataSessionKey: legacyKey ?? sessionKey });
if (deletedAgentId !== null) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Agent "${deletedAgentId}" no longer exists in configuration`));
return;
}
const agentId = resolveSessionAgentId({
sessionKey,
config: cfg,
agentId: selectedAgent.agentId
});
const activeRunScopeKey = resolveChatSendActiveScopeKey({
sessionKey,
agentId: selectedAgent.agentId,
mainKey: cfg.session?.mainKey
});
const resolvedSessionModel = resolveSessionModelRef(cfg, entry, agentId);
const resolvedSessionAuthProvider = resolveProviderIdForAuth(resolvedSessionModel.provider, { config: cfg });
let parsedMessage = inboundMessage;
let parsedImages = [];
let imageOrder = [];
let offloadedRefs = [];
let mediaPathOffloadPaths = [];
let mediaPathOffloadTypes = [];
let mediaPathOffloadWorkspaceDir;
const timeoutMs = resolveAgentTimeoutMs({
cfg,
overrideMs: p.timeoutMs
});
const now = Date.now();
if (resolveSendPolicy({
cfg,
entry,
sessionKey,
channel: entry?.channel,
chatType: entry?.chatType
}) === "deny") {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "send blocked by session policy"));
return;
}
if (stopCommand) {
const defaultAgentId = resolveDefaultAgentId(cfg);
const stopAgentId = sessionKey === "global" ? selectedAgent.agentId ?? defaultAgentId : selectedAgent.agentId;
const res = await abortChatRunsForSessionKeyWithPartials({
context,
ops: createChatAbortOps(context),
sessionKey: rawSessionKey,
sessionKeyAliases: sessionKey === rawSessionKey ? void 0 : [sessionKey],
agentId: stopAgentId,
sessionId: entry?.sessionId,
persistSessionKey: sessionKey,
defaultAgentId,
abortOrigin: "stop-command",
stopReason: "stop",
requester: resolveChatAbortRequester(client)
});
if (res.unauthorized) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized"));
return;
}
respond(true, {
ok: true,
aborted: res.aborted,
runIds: res.runIds
});
return;
}
const cached = context.dedupe.get(`chat:${clientRunId}`);
if (cached) {
respond(cached.ok, cached.payload, cached.error, { cached: true });
return;
}
const abortedAt = context.chatAbortedRuns.get(clientRunId);
if (abortedAt !== void 0) {
const payload = buildAbortedChatSendPayload({
runId: clientRunId,
endedAt: abortedAt
});
setGatewayDedupeEntry({
dedupe: context.dedupe,
key: `chat:${clientRunId}`,
entry: {
ts: abortedAt,
ok: true,
payload
}
});
respond(true, payload, void 0, {
cached: true,
runId: clientRunId
});
return;
}
if (context.chatAbortControllers.get(clientRunId)) {
respond(true, {
runId: clientRunId,
status: "in_flight"
}, void 0, {
cached: true,
runId: clientRunId
});
return;
}
const clientInfo = client?.connect?.client;
const chatSendTraceAttributes = {
runId: clientRunId,
sessionKey,
agentId: selectedAgent.agentId ?? agentId,
provider: resolvedSessionModel.provider,
model: resolvedSessionModel.model,
hasAttachments: normalizedAttachments.length > 0,
hasExplicitOrigin: explicitOriginResult.value !== void 0,
hasConnectedClient: client?.connect !== void 0
};
const originatingRoute = resolveChatSendOriginatingRoute({
client: clientInfo,
deliver: p.deliver,
entry,
explicitOrigin: explicitOriginResult.value,
hasConnectedClient: client?.connect !== void 0,
mainKey: cfg.session?.mainKey,
sessionKey
});
const activeChatSendDedupeKey = buildActiveChatSendDedupeKey({
attachmentCount: normalizedAttachments.length,
explicitDeliverRoute: originatingRoute.explicitDeliverRoute,
message: rawMessage,
originatingChannel: originatingRoute.originatingChannel,
sessionKey: activeRunScopeKey,
systemScope: systemDedupeScope
});
if (activeChatSendDedupeKey) {
const activeRunId = resolveActiveChatSendRunId(context.dedupe.get(activeChatSendDedupeKey)?.payload);
if (activeRunId && context.chatAbortControllers.has(activeRunId)) {
respond(true, {
runId: activeRunId,
status: "in_flight"
}, void 0, {
cached: true,
runId: activeRunId
});
return;
}
}
const activeRunAbort = registerChatAbortController({
chatAbortControllers: context.chatAbortControllers,
runId: clientRunId,
sessionId: backingSessionId ?? clientRunId,
sessionKey,
agentId: selectedAgent.agentId,
timeoutMs,
now,
ownerConnId: normalizeOptionalText(client?.connId),
ownerDeviceId: normalizeOptionalText(client?.connect?.device?.id),
providerId: resolvedSessionModel.provider,
authProviderId: resolvedSessionAuthProvider,
kind: "chat-send"
});
if (!activeRunAbort.registered) {
respond(true, {
runId: clientRunId,
status: "in_flight"
}, void 0, {
cached: true,
runId: clientRunId
});
return;
}
if (activeChatSendDedupeKey) context.dedupe.set(activeChatSendDedupeKey, {
ts: now,
ok: true,
payload: { runId: clientRunId }
});
const explicitOriginTargetsPlugin = explicitOriginTargetsPluginBinding(explicitOriginResult.value);
let prepareAttachmentsMs;
if (normalizedAttachments.length > 0) {
const prepareAttachmentsStartedAtMs = performance.now();
try {
await measureDiagnosticsTimelineSpan("gateway.chat_send.prepare_attachments", async () => {
const supportsSessionModelImages = await resolveGatewayModelSupportsImages({
loadGatewayModelCatalog: context.loadGatewayModelCatalog,
provider: resolvedSessionModel.provider,
model: resolvedSessionModel.model
});
const explicitOriginSupportsInlineImages = explicitOriginTargetsAcpSession(explicitOriginResult.value) || explicitOriginTargetsPlugin;
const supportsImages = supportsSessionModelImages || explicitOriginSupportsInlineImages;
const routeImageOffloadsAsMediaPaths = !supportsImages;
const parsed = await parseMessageWithAttachments(inboundMessage, normalizedAttachments, {
maxBytes: resolveChatAttachmentMaxBytes(cfg),
log: context.logGateway,
supportsImages,
acceptNonImage: true
});
parsedMessage = stripTrailingOffloadedMediaMarkers(parsed.message, routeImageOffloadsAsMediaPaths ? parsed.offloadedRefs.filter((ref) => ref.mimeType.startsWith("image/")) : []);
parsedImages = parsed.images;
imageOrder = routeImageOffloadsAsMediaPaths ? [] : parsed.imageOrder;
offloadedRefs = parsed.offloadedRefs;
({paths: mediaPathOffloadPaths, types: mediaPathOffloadTypes, workspaceDir: mediaPathOffloadWorkspaceDir} = await prestageMediaPathOffloads({
offloadedRefs,
includeImageRefs: routeImageOffloadsAsMediaPaths,
cfg,
sessionKey,
agentId
}));
}, {
phase: "agent-turn",
config: cfg,
attributes: {
...chatSendTraceAttributes,
attachmentCount: normalizedAttachments.length
}
});
prepareAttachmentsMs = roundedChatSendTimingMs(performance.now() - prepareAttachmentsStartedAtMs);
} catch (err) {
activeRunAbort.cleanup();
clearActiveChatSendDedupeRun(context.dedupe, activeChatSendDedupeKey, clientRunId);
logAttachmentFailure(context.logGateway, "chat.send attachment parse/stage failed", err);
respond(false, void 0, errorShape(err instanceof MediaOffloadError ? ErrorCodes.UNAVAILABLE : ErrorCodes.INVALID_REQUEST, String(err)));
return;
}
}
if (activeRunAbort.controller.signal.aborted) {
const stopReason = activeRunAbort.entry?.abortStopReason ?? "rpc";
const endedAt = Date.now();
const payload = buildAbortedChatSendPayload({
runId: clientRunId,
stopReason,
endedAt
});
clearActiveChatSendDedupeRun(context.dedupe, activeChatSendDedupeKey, clientRunId);
setGatewayDedupeEntry({
dedupe: context.dedupe,
key: `chat:${clientRunId}`,
entry: {
ts: endedAt,
ok: true,
payload
}
});
respond(true, payload, void 0, { runId: clientRunId });
return;
}
try {
const serverTiming = shouldIncludeChatSendAckServerTiming(clientInfo) ? {
receivedToAckMs: roundedChatSendTimingMs(performance.now() - chatSendReceivedAtMs),
loadSessionMs: sessionLoadMs,
...prepareAttachmentsMs !== void 0 ? { prepareAttachmentsMs } : {}
} : void 0;
const chatSendTiming = serverTiming && typeof client?.connId === "string" && client.connId.trim() ? {
ackedAtMs: performance.now(),
connId: client.connId.trim(),
receivedAtMs: chatSendReceivedAtMs
} : void 0;
context.addChatRun(clientRunId, {
sessionKey,
agentId: selectedAgent.agentId,
clientRunId,
...chatSendTiming ? { chatSendTiming } : {}
});
const ackPayload = {
runId: clientRunId,
status: "started",
...serverTiming ? { serverTiming } : {}
};
emitDiagnosticsTimelineEvent({
type: "mark",
name: "gateway.chat_send.ack_ready",
phase: "agent-turn",
attributes: {
...chatSendTraceAttributes,
ackStatus: ackPayload.status,
...chatSendAckServerTimingAttributes(serverTiming)
}
}, { config: cfg });
respond(true, ackPayload, void 0, { runId: clientRunId });
const chatSendAckedAtMs = chatSendTiming?.ackedAtMs ?? performance.now();
const persistedImagesPromise = persistChatSendImages({
images: parsedImages,
imageOrder,
offloadedRefs,
client,
logGateway: context.logGateway
});
let persistedMediaForTranscript;
const getPersistedMediaForTranscript = async () => {
if (!persistedMediaForTranscript) persistedMediaForTranscript = await persistedImagesPromise;
return persistedMediaForTranscript;
};
const preparedUserTurnMediaPromise = normalizedAttachments.length > 0 ? getPersistedMediaForTranscript() : Promise.resolve([]);
const userTurnMediaPromise = preparedUserTurnMediaPromise.then(buildChatSendUserTurnMedia);
const baseUserTurnInput = {
text: rawMessage,
timestamp: now,
idempotencyKey: `${clientRunId}:user`,
...systemInputProvenance ? { provenance: systemInputProvenance } : {}
};
const userTurnInputPromise = userTurnMediaPromise.then((media) => ({
...baseUserTurnInput,
...media.length > 0 ? {
media,
mediaOnlyText: "[User sent media without caption]"
} : {}
}));
const pluginBoundMediaFieldsPromise = explicitOriginTargetsPlugin && parsedImages.length > 0 ? preparedUserTurnMediaPromise.then(resolveChatSendManagedMediaFields) : Promise.resolve({});
const trimmedMessage = parsedMessage.trim();
const commandBody = Boolean(p.thinking && trimmedMessage && !trimmedMessage.startsWith("/")) ? `/think ${p.thinking} ${parsedMessage}` : parsedMessage;
const commandSource = !suppressCommandInterpretation && trimmedMessage.startsWith("/") ? "text" : void 0;
const messageForAgent = systemProvenanceReceipt ? [systemProvenanceReceipt, parsedMessage].filter(Boolean).join("\n\n") : parsedMessage;
const { originatingChannel, originatingTo, accountId, messageThreadId, explicitDeliverRoute } = originatingRoute;
const ctx = {
Body: messageForAgent,
BodyForAgent: messageForAgent,
BodyForCommands: commandBody,
RawBody: parsedMessage,
CommandBody: commandBody,
InputProvenance: systemInputProvenance,
SessionKey: sessionKey,
AgentId: agentId,
Provider: INTERNAL_MESSAGE_CHANNEL,
Surface: INTERNAL_MESSAGE_CHANNEL,
OriginatingChannel: originatingChannel,
OriginatingTo: originatingTo,
ExplicitDeliverRoute: explicitDeliverRoute,
AccountId: accountId,
MessageThreadId: messageThreadId,
ChatType: "direct",
...commandSource ? { CommandSource: commandSource } : {},
CommandAuthorized: !suppressCommandInterpretation,
CommandTurn: commandSource ? {
kind: "text-slash",
source: commandSource,
authorized: true,
body: commandBody
} : {
kind: "normal",
source: "message",
authorized: false,
body: commandBody
},
MessageSid: clientRunId,
...!isOperatorUiClient(clientInfo) ? {
SenderId: clientInfo?.id,
SenderName: clientInfo?.displayName,
SenderUsername: clientInfo?.displayName
} : {},
GatewayClientScopes: client?.connect?.scopes ?? []
};
if (mediaPathOffloadPaths.length > 0) {
ctx.MediaPath = mediaPathOffloadPaths[0];
ctx.MediaPaths = mediaPathOffloadPaths;
ctx.MediaType = mediaPathOffloadTypes[0];
ctx.MediaTypes = mediaPathOffloadTypes;
ctx.MediaWorkspaceDir = mediaPathOffloadWorkspaceDir;
ctx.MediaStaged = true;
}
const replyOptionImages = mediaPathOffloadTypes.some((type) => type.startsWith("image/")) ? void 0 : parsedImages.length > 0 ? parsedImages : void 0;
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
cfg,
agentId,
channel: INTERNAL_MESSAGE_CHANNEL
});
const deliveredReplies = [];
let appendedWebchatAgentMedia = false;
let agentRunStarted = false;
const userTurnRecorder = createUserTurnTranscriptRecorder({
input: baseUserTurnInput,
resolveInput: () => userTurnInputPromise,
target: () => {
const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(sessionKey, sessionLoadOptions);
const resolvedSessionId = latestEntry?.sessionId ?? backingSessionId;
if (!resolvedSessionId) return;
return {
sessionId: resolvedSessionId,
sessionKey,
sessionEntry: latestEntry ?? entry,
storePath: latestStorePath,
agentId,
config: cfg
};
},
errorContext: "gateway chat user turn transcript",
beforeMessageWrite: runAgentHarnessBeforeMessageWriteHook,
onPersistenceError: (error) => {
context.logGateway.warn(`gateway user transcript persistence failed: ${formatForLog(error)}`);
}
});
const persistGatewayUserTurnTranscript = async () => {
await measureDiagnosticsTimelineSpan("gateway.chat_send.persist_user_transcript", async () => {
await userTurnRecorder.persistFallback();
}, {
phase: "agent-turn",
config: cfg,
attributes: chatSendTraceAttributes
});
};
const persistGatewayUserTurnTranscriptBestEffort = async () => {
await persistGatewayUserTurnTranscript().catch(() => void 0);
};
const appendWebchatAgentMediaTranscriptIfNeeded = async (payload) => {
if (!agentRunStarted || appendedWebchatAgentMedia || !isMediaBearingPayload(payload)) return;
if (isSourceReplyTranscriptMirrorPayload(payload)) return;
const ttsSupplementMarker = buildTtsSupplementTranscriptMarker(payload);
const [transcriptPayload] = await normalizeWebchatReplyMediaPathsForDisplay({
cfg,
sessionKey,
agentId,
accountId,
payloads: [stripVisibleTextFromTtsSupplement(payload)]
});
if (!transcriptPayload) return;
const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(sessionKey, sessionLoadOptions);
const sessionId = latestEntry?.sessionId ?? backingSessionId ?? clientRunId;
const resolvedTranscriptPath = resolveTranscriptPath({
sessionId,
storePath: latestStorePath,
sessionFile: latestEntry?.sessionFile ?? entry?.sessionFile,
agentId
});
const mediaLocalRoots = appendLocalMediaParentRoots(getAgentScopedMediaLocalRoots(cfg, agentId), resolvedTranscriptPath ? [resolvedTranscriptPath] : void 0);
const assistantContent = await buildAssistantDisplayContentFromReplyPayloads({
sessionKey,
agentId,
payloads: [transcriptPayload],
managedImageLocalRoots: mediaLocalRoots,
includeSensitiveMedia: transcriptPayload.sensitiveMedia !== true,
onLocalAudioAccessDenied: (message) => {
context.logGateway.warn(`webchat audio embedding denied local path: ${message}`);
},
onManagedImagePrepareError: (message) => {
context.logGateway.warn(`webchat image embedding skipped attachment: ${message}`);
}
});
const mediaMessage = await buildWebchatAssistantMediaMessage([transcriptPayload], {
localRoots: mediaLocalRoots,
onLocalAudioAccessDenied: (message) => {
context.logGateway.warn(`webchat audio embedding denied local path: ${message}`);
}
});
const persistedAssistantContent = replaceAssistantContentTextBlocks(assistantContent, mediaMessage);
const persistedContentForAppend = hasAssistantDisplayMediaContent(persistedAssistantContent) ? persistedAssistantContent : void 0;
if (!persistedContentForAppend?.length) return;
const transcriptReply = mediaMessage?.transcriptText ?? extractAssistantDisplayTextFromContent(assistantContent) ?? buildTranscriptReplyText([transcriptPayload]);
if (!transcriptReply && !persistedAssistantContent?.length && !assistantContent?.length) return;
const appended = await appendAssistantTranscriptMessage({
sessionKey,
message: transcriptReply,
...persistedContentForAppend?.length ? { content: persistedContentForAppend } : {},
sessionId,
storePath: latestStorePath,
sessionFile: latestEntry?.sessionFile,
agentId,
createIfMissing: true,
idempotencyKey: `${clientRunId}:assistant-media`,
ttsSupplement: ttsSupplementMarker,
cfg
});
if (appended.ok) {
if (appended.messageId && assistantContent?.length) await attachManagedOutgoingImagesToMessage({
messageId: appended.messageId,
blocks: assistantContent
});
appendedWebchatAgentMedia = true;
return;
}
context.logGateway.warn(`webchat transcript append failed for media reply: ${appended.error ?? "unknown error"}`);
};
const dispatcher = createReplyDispatcher({
...replyPipeline,
onError: (err) => {
context.logGateway.warn(`webchat dispatch failed: ${formatForLog(err)}`);
},
deliver: async (payload, info) => {
if (getReplyPayloadMetadata(payload)?.beforeAgentRunBlocked === true) userTurnRecorder.markBlocked();
switch (info.kind) {
case "block":
case "final":
deliveredReplies.push({
payload,
kind: info.kind
});
await appendWebchatAgentMediaTranscriptIfNeeded(payload);
break;
case "tool":
if (isMediaBearingPayload(payload)) deliveredReplies.push({
payload: {
...payload,
text: void 0
},
kind: "final"
});
break;
}
}
});
const emitServerTiming = (phase, extra, dispatchStartedAtMs) => {
emitOperatorChatSendServerTiming({
context,
client,
phase,
runId: clientRunId,
sessionKey,
agentId,
receivedAtMs: chatSendReceivedAtMs,
ackedAtMs: chatSendAckedAtMs,
dispatchStartedAtMs,
extra
});
};
const dispatchStartedAtMs = performance.now();
if (chatSendTiming) chatSendTiming.dispatchStartedAtMs = dispatchStartedAtMs;
emitServerTiming("dispatch-started");
let firstAssistantServerTimingEmitted = false;
const emitFirstAssistantServerTiming = () => {
if (firstAssistantServerTimingEmitted || chatSendTiming?.firstAssistantEventSent) return;
firstAssistantServerTimingEmitted = true;
if (chatSendTiming) chatSendTiming.firstAssistantEventSent = true;
emitServerTiming("first-assistant-event", void 0, dispatchStartedAtMs);
};
measureDiagnosticsTimelineSpan("gateway.chat_send.dispatch_inbound", async () => {
applyChatSendManagedMediaFields(ctx, await pluginBoundMediaFieldsPromise);
const dispatchResult = await dispatchInboundMessage({
ctx,
cfg,
dispatcher,
onSessionMetadataChanges: (changes) => {
for (const change of changes) emitSessionsChanged(context, change);
},
replyOptions: {
runId: clientRunId,
...isOperatorUiClient(clientInfo) ? { promptCacheKey: resolveWebchatPromptCacheKey({
agentId,
provider: resolvedSessionModel.provider,
model: resolvedSessionModel.model,
sessionKey: activeRunScopeKey
}) } : {},
abortSignal: activeRunAbort.controller.signal,
images: replyOptionImages,
imageOrder: imageOrder.length > 0 ? imageOrder : void 0,
thinkingLevelOverride: p.thinking,
fastModeOverride: p.fastMode,
userTurnTranscriptRecorder: userTurnRecorder,
onAgentRunStart: (runId) => {
agentRunStarted = true;
emitServerTiming("agent-run-started", runId !== clientRunId ? { agentRunId: runId } : void 0, dispatchStartedAtMs);
const connId = typeof client?.connId === "string" ? client.connId : void 0;
const wantsToolEvents = hasGatewayClientCap(client?.connect?.caps, GATEWAY_CLIENT_CAPS.TOOL_EVENTS);
if (connId && wantsToolEvents) {
context.registerToolEventRecipient(runId, connId);
const defaultAgentId = resolveDefaultAgentId(cfg);
const selectedGlobalAgentId = sessionKey === "global" ? selectedAgent.agentId ?? defaultAgentId : void 0;
for (const [activeRunId, active] of context.chatAbortControllers) {
const activeGlobalAgentId = active.sessionKey === "global" ? active.agentId ?? defaultAgentId : void 0;
const sameSelectedGlobalAgent = sessionKey === "global" && selectedGlobalAgentId !== void 0 && activeGlobalAgentId === selectedGlobalAgentId;
const sameSession = active.sessionKey === sessionKey && (sessionKey !== "global" || sameSelectedGlobalAgent);
if (activeRunId !== runId && sameSession) context.registerToolEventRecipient(activeRunId, connId);
}
}
},
onModelSelected: (modelSelection) => {
updateChatRunProvider(context.chatAbortControllers, {
runId: clientRunId,
providerId: modelSelection.provider,
authProviderId: resolveProviderIdForAuth(modelSelection.provider, { config: cfg })
});
onModelSelected(modelSelection);
emitServerTiming("model-selected", {
provider: modelSelection.provider,
model: modelSelection.model
}, dispatchStartedAtMs);
}
}
});
if (dispatchResult.beforeAgentRunBlocked === true) userTurnRecorder.markBlocked();
return dispatchResult;
}, {
phase: "agent-turn",
config: cfg,
attributes: chatSendTraceAttributes
}).then(async () => {
emitServerTiming("dispatch-completed", void 0, dispatchStartedAtMs);
const postDispatchStartedAtMs = performance.now();
await measureDiagnosticsTimelineSpan("gateway.chat_send.post_dispatch", async () => {
const returnedAgentErrorPayloads = agentRunStarted ? deliveredReplies.map((entryInner) => entryInner.payload).filter((payload) => payload.isError) : [];
const returnedAgentErrorMessage = returnedAgentErrorPayloads.map((payload) => payload.text?.trim()).filter((text) => Boolean(text)).join(" | ") || void 0;
if (agentRunStarted && returnedAgentErrorPayloads.length > 0 && !userTurnRecorder.hasPersisted() && !userTurnRecorder.isBlocked()) await persistGatewayUserTurnTranscriptBestEffort();
if (agentRunStarted && returnedAgentErrorPayloads.length === 0 && !userTurnRecorder.hasPersisted() && !userTurnRecorder.isBlocked() && userTurnRecorder.hasRuntimePersistencePending()) await persistGatewayUserTurnTranscriptBestEffort();
let broadcastedSourceReplyFinal = false;
if (!agentRunStarted) {
const btwReplies = deliveredReplies.map((entryScoped) => entryScoped.payload).filter(isBtwReplyPayload);
const btwText = btwReplies.map((payload) => payload.text.trim()).filter(Boolean).join("\n\n").trim();
if (btwReplies.length > 0 && btwText) {
broadcastSideResult({
context,
payload: {
kind: "btw",
runId: clientRunId,
sessionKey,
...sessionKey === "global" && agentId ? { agentId } : {},
question: btwReplies[0].btw.question.trim(),
text: btwText,
isError: btwReplies.some((payload) => payload.isError),
ts: Date.now()
}
});
broadcastChatFinal({
context,
runId: clientRunId,
sessionKey,
agentId
});
} else {
await persistGatewayUserTurnTranscriptBestEffort();
const finalPayloads = await normalizeWebchatReplyMediaPathsForDisplay({
cfg,
sessionKey,
agentId,
accountId,
payloads: appendedWebchatAgentMedia ? [] : deliveredReplies.filter((entryItem) => entryItem.kind === "final").map((entryCandidate) => entryCandidate.payload)
});
const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(sessionKey, sessionLoadOptions);
const sessionId = latestEntry?.sessionId ?? backingSessionId ?? clientRunId;
const resolvedTranscriptPath = resolveTranscriptPath({
sessionId,
storePath: latestStorePath,
sessionFile: latestEntry?.sessionFile ?? entry?.sessionFile,
agentId
});
const mediaLocalRoots = appendLocalMediaParentRoots(getAgentScopedMediaLocalRoots(cfg, agentId), resolvedTranscriptPath ? [resolvedTranscriptPath] : void 0);
const assistantContent = await buildAssistantDisplayContentFromReplyPayloads({
sessionKey,
agentId,
payloads: finalPayloads,
managedImageLocalRoots: mediaLocalRoots,
includeSensitiveMedia: false,
onLocalAudioAccessDenied: (message) => {
context.logGateway.warn(`webchat audio embedding denied local path: ${message}`);
},
onManagedImagePrepareError: (message) => {
context.logGateway.warn(`webchat image embedding skipped attachment: ${message}`);
}
});
const mediaMessage = await buildWebchatAssistantMediaMessage(finalPayloads, {
localRoots: mediaLocalRoots,
onLocalAudioAccessDenied: (message) => {
context.logGateway.warn(`webchat audio embedding denied local path: ${message}`);
}
});
const hasSensitiveMedia = hasSensitiveMediaPayload(finalPayloads);
const ttsSupplementMarker = finalPayloads.map((payload) => buildMediaOnlyTtsSupplementTranscriptMarker(payload)).find((marker) => Boolean(marker));
const persistedAssistantContent = replaceAssistantContentTextBlocks(hasSensitiveMedia ? await buildAssistantDisplayContentFromReplyPayloads({
sessionKey,
agentId,
payloads: finalPayloads,
managedImageLocalRoots: mediaLocalRoots,
includeSensitiveMedia: false,
onLocalAudioAccessDenied: (message) => {
context.logGateway.warn(`webchat audio embedding denied local path: ${message}`);
},
onManagedImagePrepareError: (message) => {
context.logGateway.warn(`webchat image embedding skipped attachment: ${message}`);
}
}) : assistantContent, mediaMessage);
const persistedContentForAppend = hasAssistantDisplayMediaContent(persistedAssistantContent) ? persistedAssistantContent : void 0;
const broadcastAssistantContent = hasAssistantDisplayMediaContent(assistantContent) ? assistantContent : hasAssistantDisplayMediaContent(mediaMessage?.content) ? mediaMessage?.content : assistantContent;
const displayReply = extractAssistantDisplayTextFromContent(assistantContent) ?? buildTranscriptReplyText(finalPayloads);
const transcriptReply = mediaMessage?.transcriptText || buildTranscriptReplyText(finalPayloads) || displayReply;
let message;
if (transcriptReply || persistedContentForAppend?.length || assistantContent?.length) {
const appended = await appendAssistantTranscriptMessage({
sessionKey,
message: transcriptReply,
...persistedContentForAppend?.length ? { content: persistedContentForAppend } : {},
sessionId,
storePath: latestStorePath,
sessionFile: latestEntry?.sessionFile,
agentId,
createIfMissing: true,
idempotencyKey: clientRunId,
ttsSupplement: ttsSupplementMarker,
cfg
});
if (appended.ok) {
if (appended.messageId && assistantContent?.length) await attachManagedOutgoingImagesToMessage({
messageId: appended.messageId,
blocks: assistantContent
});
message = broadcastAssistantContent?.length ? {
...appended.message,
content: broadcastAssistantContent
} : appended.message;
} else {
context.logGateway.warn(`webchat transcript append failed: ${appended.error ?? "unknown error"}`);
const fallbackAssistantContent = stripManagedOutgoingAssistantContentBlocks(persistedAssistantContent) ?? stripManagedOutgoingAssistantContentBlocks(assistantContent);
const fallbackText = extractAssistantDisplayText(fallbackAssistantContent) ?? displayReply;
const nowValue = Date.now();
message = {
role: "assistant",
...fallbackAssistantContent?.length ? { content: fallbackAssistantContent } : fallbackText ? { content: [{
type: "text",
text: fallbackText
}] } : {},
...fallbackText ? { text: fallbackText } : {},
timestamp: nowValue,
...ttsSupplementMarker ? { openclawTtsSupplement: ttsSupplementMarker } : {},
stopReason: "stop",
usage: {
input: 0,
output: 0,
totalTokens: 0
}
};
}
}
if (hasVisibleAssistantFinalMessage(message)) emitFirstAssistantServerTiming();
broadcastChatFinal({
context,
runId: clientRunId,
sessionKey,
agentId,
message
});
}
} else {
const hasReturnedAgentErrorPayloads = returnedAgentErrorPayloads.length > 0;
const agentRunReplyPayloads = deliveredReplies.filter((entryEntry) => entryEntry.kind === "final").map((entryResult) => entryResult.payload).filter((payload) => isSourceReplyTranscriptMirrorPayload(payload) || !hasReturnedAgentErrorPayloads && isReplyPayloadStatusNotice(payload));
if (agentRunReplyPayloads.length > 0) {
const hasSourceReplyTranscriptMirror = agentRunReplyPayloads.some(isSourceReplyTranscriptMirrorPayload);
const finalPayloads = await normalizeWebchatReplyMediaPathsForDisplay({
cfg,
sessionKey,
agentId,
accountId,
payloads: agentRunReplyPayloads
});
const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(sessionKey, sessionLoadOptions);
const sessionId = latestEntry?.sessionId ?? backingSessionId ?? clientRunId;
const resolvedTranscriptPath = resolveTranscriptPath({
sessionId,
storePath: latestStorePath,
sessionFile: latestEntry?.sessionFile ?? entry?.sessionFile,
agentId
});
const mediaLocalRoots = appendLocalMediaParentRoots(getAgentScopedMediaLocalRoots(cfg, agentId), resolvedTranscriptPath ? [resolvedTranscriptPath] : void 0);
const buildReplyAssistantContent = async (payloads) => await buildAssistantDisplayContentFromReplyPayloads({
sessionKey,
agentId,
payloads,
managedImageLocalRoots: mediaLocalRoots,
includeSensitiveMedia: false,
onLocalAudioAccessDenied: (message) => {
context.logGateway.warn(`webchat audio embedding denied local path: ${message}`);
},
onManagedImagePrepareError: (message) => {
context.logGateway.warn(`webchat image embedding skipped attachment: ${message}`);
}
});
const buildReplyMediaMessage = async (payloads) => await buildWebchatAssistantMediaMessage(payloads, {
localRoots: mediaLocalRoots,
onLocalAudioAccessDenied: (message) => {
context.logGateway.warn(`webchat audio embedding denied local path: ${message}`);
}
});
const combinedAssistantContent = agentRunReplyPayloads.length === 1 ? await buildReplyAssistantContent(finalPayloads) : void 0;
const combinedMediaMessage = agentRunReplyPayloads.length === 1 ? await buildReplyMediaMessage(finalPayloads) : void 0;
const sourceReplyContentStates = [];
const sourceReplyBroadcastContent = [];
for (const [replyIndex] of agentRunReplyPayloads.entries()) {
const finalPayload = finalPayloads[replyIndex];
if (!finalPayload) continue;
const replyAssistantContent = agentRunReplyPayloads.length === 1 ? combinedAssistantContent : await buildReplyAssistantContent([finalPayload]);
const replyMediaMessage = agentRunReplyPayloads.length === 1 ? combinedMediaMessage : await buildReplyMediaMessage([finalPayload]);
const replyBroadcastContent = hasAssistantDisplayMediaContent(replyAssistantContent) ? replyAssistantContent : hasAssistantDisplayMediaContent(replyMediaMessage?.content) ? replyMediaMessage?.content : replyAssistantContent;
const persistedContent = replaceAssistantContentTextBlocks(replyAssistantContent, replyMediaMessage ?? null);
const state = {
broadcastContent: replyBroadcastContent ? [...replyBroadcastContent] : [],
persistedContent: persistedContent ? [...persistedContent] : [],
hasManagedOutgoingContent: hasManagedOutgoingAssistantContent(persistedContent),
backedManagedOutgoingContent: false
};
sourceReplyContentStates[replyIndex] = state;
if (state.broadcastContent.length > 0) sourceReplyBroadcastContent.push(...state.broadcastContent);
}
const displayReply = extractAssistantDisplayTextFromContent(sourceReplyBroadcastContent) ?? buildTranscriptReplyText(finalPayloads);
if (sourceReplyBroadcastContent.length || displayReply) {
const sourceReplyPersistenceRequests = [];
for (const [replyIndex, sourceReplyPayload] of agentRunReplyPayloads.entries()) {
const state = sourceReplyContentStates[replyIndex];
if (!state || !hasAssistantDisplayMediaContent(state.persistedContent)) continue;
const mirrorMetadata = getReplyPayloadMetadata(sourceReplyPayload)?.sourceReplyTranscriptMirror;
const mirrorIdempotencyKey = mirrorMetadata?.idempotencyKey;
if (typeof mirrorIdempotencyKey !== "string" || mirrorIdempotencyKey.trim().length === 0) continue;
if (!state.hasManagedOutgoingContent) state.backedManagedOutgoingContent = true;
sourceReplyPersistenceRequests.push({
idempotencyKey: mirrorIdempotencyKey,
metadata: mirrorMetadata,
state
});
}
const attachSourceReplyManagedImages = async (paramsLocal) => {
if (!paramsLocal.request.state.hasManagedOutgoingContent) {
paramsLocal.request.state.backedManagedOutgoingContent = true;
return;
}
if (!paramsLocal.messageId) return;
await attachManagedOutgoingImagesToMessage({
messageId: paramsLocal.messageId,
blocks: paramsLocal.request.state.persistedContent
});
paramsLocal.request.state.backedManagedOutgoingContent = true;
};
if (resolvedTranscriptPath && sourceReplyPersistenceRequests.length > 0) {
const allowedSourceReplyMirrorIds = /* @__PURE__ */ new Set();
for (const [replyIndex, sourceReplyPayload] of agentRunReplyPayloads.entries()) {
if (!sourceReplyContentStates[replyIndex]) continue;
const mirrorIdempotencyKey = getReplyPayloadMetadata(sourceReplyPayload)?.sourceReplyTranscriptMirror?.idempotencyKey;
const mirrorMetadata = getReplyPayloadMetadata(sourceReplyPayload)?.sourceReplyTranscriptMirror;
if (typeof mirrorIdempotencyKey !== "string" || mirrorIdempotencyKey.trim().length === 0 || !mirrorMetadata) continue;
const target = await findSourceReplyTranscriptMirrorByMetadata({
transcriptPath: resolvedTranscriptPath,
idempotencyKey: mirrorIdempotencyKey,
metadata: mirrorMetadata
});
if (target) allowedSourceReplyMirrorIds.add(target.messageId);
}
const rewriteTargets = [];
for (const request of sourceReplyPersistenceRequests) {
const target = await findSourceReplyTranscriptMirrorByMetadata({
transcriptPath: resolvedTranscriptPath,
idempotencyKey: request.idempotencyKey,
metadata: request.metadata
});
if (target) rewriteTargets.push({
request,
...target
});
}
if (rewriteTargets.length > 0) {
const rewriteTargetIds = new Set(rewriteTargets.map((target) => target.messageId));
const rewriteIndex = await readSessionTranscriptIndex(resolvedTranscriptPath);
const firstRewriteEntryIndex = rewriteIndex?.entries.findIndex((entryValue) => typeof entryValue.id === "string" && rewriteTargetIds.has(entryValue.id)) ?? -1;
if (firstRewriteEntryIndex >= 0 && rewriteIndex?.entries.slice(firstRewriteEntryIndex).every((entryLocal) => typeof entryLocal.id !== "string" || allowedSourceReplyMirrorIds.has(entryLocal.id)) === true) {
if ((await rewriteTranscriptEntriesInSessionFile({
sessionFile: resolvedTranscriptPath,
sessionKey,
agentId,
config: cfg,
request: {
allowedRewriteSuffixEntryIds: [...allowedSourceReplyMirrorIds],
replacements: rewriteTargets.map((target) => ({
entryId: target.messageId,
message: {
...target.message,
idempotencyKey: target.request.idempotencyKey,
content: target.request.state.persistedContent
}
}))
}
})).changed) {
await advanceSessionTranscriptMarker({
storePath: latestStorePath,
sessionKey,
sessionId
});
for (const target of rewriteTargets) await attachSourceReplyManagedImages({
messageId: (await findSourceReplyTranscriptMirrorByIdempotencyKey(resolvedTranscriptPath, target.request.idempotencyKey))?.messageId,
request: target.request
});
}
}
}
}
const sourceReplyContent = sourceReplyContentStates.flatMap((state) => {
if (state.hasManagedOutgoingContent && !state.backedManagedOutgoingContent) {
const stripped = stripManagedOutgoingAssistantContentBlocks(state.broadcastContent);
return stripped?.length ? stripped : [{
type: "text",
text: "Media reply could not be displayed."
}];
}
return state.broadcastContent;
}).filter((block) => Boolean(block));
const sourceReplyText = extractAssistantDisplayTextFromContent(sourceReplyContent) ?? (sourceReplyContent.length === 0 ? displayReply : void 0);
const nowLocal = Date.now();
const message = {
role: "assistant",
...sourceReplyContent?.length ? { content: sourceReplyContent } : sourceReplyText ? { content: [{
type: "text",
text: sourceReplyText
}] } : {},
...sourceReplyText ? { text: sourceReplyText } : {},
timestamp: nowLocal,
stopReason: "stop",
usage: {
input: 0,
output: 0,
totalTokens: 0
}
};
if (hasVisibleAssistantFinalMessage(message)) emitFirstAssistantServerTiming();
broadcastChatFinal({
context,
runId: clientRunId,
sessionKey,
agentId,
message
});
broadcastedSourceReplyFinal = hasSourceReplyTranscriptMirror;
}
}
}
const shouldBroadcastAgentError = returnedAgentErrorPayloads.length > 0 && !broadcastedSourceReplyFinal;
if (shouldBroadcastAgentError) broadcastChatError({
context,
runId: clientRunId,
sessionKey,
agentId,
errorMessage: returnedAgentErrorMessage
});
if (!context.chatAbortedRuns.has(clientRunId)) {
const returnedAgentError = shouldBroadcastAgentError ? errorShape(ErrorCodes.UNAVAILABLE, returnedAgentErrorMessage ?? "agent returned an error payload") : void 0;
setGatewayDedupeEntry({
dedupe: context.dedupe,
key: `chat:${clientRunId}`,
entry: {
ts: Date.now(),
ok: !shouldBroadcastAgentError,
payload: shouldBroadcastAgentError ? {
runId: clientRunId,
status: "error",
summary: returnedAgentErrorMessage ?? "agent returned an error payload"
} : {
runId: clientRunId,
status: "ok"
},
...returnedAgentError ? { error: returnedAgentError } : {}
}
});
}
}, {
phase: "agent-turn",
config: cfg,
attributes: chatSendTraceAttributes
});
emitServerTiming("post-dispatch-completed", { postDispatchMs: roundedChatSendTimingMs(performance.now() - postDispatchStartedAtMs) }, dispatchStartedAtMs);
}).catch(async (err) => {
await (userTurnRecorder.hasPersisted() || userTurnRecorder.isBlocked() ? Promise.resolve() : persistGatewayUserTurnTranscript()).catch((transcriptErr) => {
context.logGateway.warn(`webchat user transcript update failed after error: ${formatForLog(transcriptErr)}`);
});
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
setGatewayDedupeEntry({
dedupe: context.dedupe,
key: `chat:${clientRunId}`,
entry: {
ts: Date.now(),
ok: false,
payload: {
runId: clientRunId,
status: "error",
summary: String(err)
},
error
}
});
broadcastChatError({
context,
runId: clientRunId,
sessionKey,
agentId,
errorMessage: String(err)
});
}).finally(() => {
activeRunAbort.cleanup();
clearActiveChatSendDedupeRun(context.dedupe, activeChatSendDedupeKey, clientRunId);
context.removeChatRun(clientRunId, clientRunId, sessionKey);
});
} catch (err) {
activeRunAbort.cleanup();
clearActiveChatSendDedupeRun(context.dedupe, activeChatSendDedupeKey, clientRunId);
context.removeChatRun(clientRunId, clientRunId, sessionKey);
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
const payload = {
runId: clientRunId,
status: "error",
summary: String(err)
};
setGatewayDedupeEntry({
dedupe: context.dedupe,
key: `chat:${clientRunId}`,
entry: {
ts: Date.now(),
ok: false,
payload,
error
}
});
respond(false, payload, error, {
runId: clientRunId,
error: formatForLog(err)
});
broadcastChatError({
context,
runId: clientRunId,
sessionKey,
agentId,
errorMessage: String(err)
});
}
},
"chat.inject": async ({ params, respond, context }) => {
if (!validateChatInjectParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid chat.inject params: ${formatValidationErrors(validateChatInjectParams.errors)}`));
return;
}
const p = params;
const rawSessionKey = p.sessionKey;
const requestedAgentId = resolveRequestedChatAgentId({
cfg: context.getRuntimeConfig?.(),
requestedSessionKey: rawSessionKey,
agentId: p.agentId
});
const { cfg, storePath, entry, canonicalKey: sessionKey } = loadSessionEntry(rawSessionKey, requestedAgentId ? { agentId: requestedAgentId } : void 0);
const selectedAgent = validateChatSelectedAgent({
cfg,
requestedSessionKey: rawSessionKey,
agentId: requestedAgentId
});
if (!selectedAgent.ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, selectedAgent.error));
return;
}
const sessionId = entry?.sessionId;
if (!sessionId || !storePath) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "session not found"));
return;
}
const agentId = resolveSessionAgentId({
sessionKey,
config: cfg,
agentId: selectedAgent.agentId
});
const appended = await appendAssistantTranscriptMessage({
sessionKey,
message: p.message,
label: p.label,
sessionId,
storePath,
sessionFile: entry?.sessionFile,
agentId,
createIfMissing: true,
cfg
});
if (!appended.ok || !appended.messageId || !appended.message) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, `failed to write transcript: ${appended.error ?? "unknown error"}`));
return;
}
const message = projectChatDisplayMessage(appended.message, { maxChars: resolveEffectiveChatHistoryMaxChars(cfg) });
const chatPayload = {
runId: `inject-${appended.messageId}`,
sessionKey,
...sessionKey === "global" && agentId ? { agentId } : {},
seq: 0,
state: "final",
message
};
context.broadcast("chat", chatPayload);
sendGlobalAwareNodeChatPayload({
context,
sessionKey,
agentId,
event: "chat",
payload: chatPayload
});
respond(true, {
ok: true,
messageId: appended.messageId
});
}
};
//#endregion
export { replaceOversizedChatHistoryMessages as a, sanitizeChatSendMessageInput as c, enforceChatHistoryFinalBudget as i, buildOversizedHistoryPlaceholder as n, emitSessionsChanged as o, chatHandlers as r, hasTrackedActiveSessionRun as s, CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES as t };