openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
691 lines (690 loc) • 33.9 kB
JavaScript
import { j as resolveIntegerOption } from "./number-coercion-CLj0HTDM.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { n as sliceUtf16Safe, r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { d as redactSensitiveFieldValueWithConfig, g as redactToolPayloadTextWithConfig, r as isSensitiveFieldKey } from "./redact-BtvPPfTi.js";
import { i as jsonUtf8BytesOrInfinity, n as firstEnumerableOwnKeys, t as boundedJsonUtf8Bytes } from "./json-utf8-bytes-fm9i4b7G.js";
import { t as getGlobalHookRunner } from "./hook-runner-global-0kfmMG4T.js";
import { At as withRuntimeUserTurnTranscriptRecorder, Dt as attachRuntimeUserTurnTranscriptRecorder, Ot as takeRuntimeUserTurnTranscriptContext, kt as takeRuntimeUserTurnTranscriptRecorder } from "./sessions-BdNAJTEP.js";
import { t as acknowledgeInternalToolResult } from "./internal-hooks-CgPsiqhr.js";
import { a as applyInputProvenanceToUserMessage } from "./input-provenance-DiG-Cwnd.js";
import { o as mergePreparedUserTurnMessageForRuntime, p as restorePreparedUserTurnOperationalMetaForRuntime } from "./user-turn-transcript.message-LcwHBB2I.js";
import "./session-accessor-YsytfDtG.js";
import { H as prepareCodeModeSourceAppend, P as redactTranscriptMessage, W as withCodeModeSourceAppend, z as copyCodeModeSourceAppend } from "./session-accessor.sqlite-transcript-store-B0zw3fAU.js";
import { i as rewriteToolResultIds, n as extractToolResultId, t as extractToolCallsFromAssistant } from "./tool-call-id-DJzLQ9lS.js";
import { r as sanitizeToolCallInputs, t as makeMissingToolResult } from "./session-transcript-repair-CzBxxYMT.js";
import { _ as publishTranscriptUpdate } from "./session-accessor.sqlite-lifecycle-state-BTh4yZ7R.js";
import { s as resolveTerminalAssistantTranscriptRunId, t as attachSessionTranscriptRunId } from "./transcript-events-wgPr4ILk.js";
import { c as isTranscriptOnlyOpenClawAssistantModel } from "./transcript-only-openclaw-assistant-CVgy4bjA.js";
import { i as resolveLiveToolResultMaxChars, t as DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS } from "./tool-result-limits-CFk9DkYv.js";
import { n as runAgentHarnessBeforeMessageWriteHook } from "./hook-helpers-3JS9XC0_.js";
import { b as formatContextLimitTruncationNotice, d as truncateToolResultMessage } from "./tool-result-truncation-myeYtakZ.js";
import { n as getRawSessionAppendMessage, r as setRawSessionAppendMessage } from "./transcript-rewrite-DjLVrcIR.js";
import "./user-turn-transcript-DDGwFDzp.js";
import { t as projectAgentHarnessTranscriptMessageForDisplay } from "./transcript-visibility-DwJ8equj.js";
//#region src/agents/session-tool-result-state.ts
/** Tracks pending tool calls so sanitized transcript repair can flush in order. */
function createPendingToolCallState() {
const pending = /* @__PURE__ */ new Map();
return {
size: () => pending.size,
entries: () => pending.entries(),
getToolName: (id) => pending.get(id),
delete: (id) => {
pending.delete(id);
},
clear: () => {
pending.clear();
},
trackToolCalls: (calls) => {
for (const call of calls) pending.set(call.id, call.name);
},
getPendingIds: () => Array.from(pending.keys()),
shouldFlushForSanitizedDrop: () => pending.size > 0,
shouldFlushBeforeNonToolResult: (nextRole, toolCallCount) => pending.size > 0 && (toolCallCount === 0 || nextRole !== "assistant"),
shouldFlushBeforeNewToolCalls: (toolCallCount) => pending.size > 0 && toolCallCount > 0
};
}
//#endregion
//#region src/agents/session-tool-result-guard.ts
/**
* Session transcript guard for tool-call/result consistency.
*
* Caps large tool results, repairs missing results, applies redaction, and emits transcript update events.
*/
/**
* Truncate oversized text content blocks in a tool result message.
* Returns the original message if under the limit, or a new message with
* truncated text blocks otherwise.
*/
function capToolResultSize(msg, maxChars) {
if (msg.role !== "toolResult") return msg;
return truncateToolResultMessage(msg, maxChars, {
suffix: (truncatedChars) => formatContextLimitTruncationNotice(truncatedChars),
minKeepChars: 2e3
});
}
function resolveMaxToolResultChars(opts) {
return resolveIntegerOption(opts?.maxToolResultChars, DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS, { min: 1 });
}
function isUserAgentMessage(message) {
return message.role === "user";
}
function isExpectedCompactionAppend(entryId, appendedText) {
const lines = appendedText.trimEnd().split("\n").filter((line) => line.length > 0);
if (lines.length !== 1) return false;
try {
const line = lines.at(0);
if (!line) return false;
const entry = JSON.parse(line);
return typeof entry === "object" && entry !== null && Reflect.get(entry, "type") === "compaction" && Reflect.get(entry, "id") === entryId;
} catch {
return false;
}
}
function resolveEntryTranscriptSeq(sessionManager, entryId, seqByEntryId) {
if (!entryId) return 0;
const cached = seqByEntryId.get(entryId);
if (cached !== void 0) return cached;
let seq = 0;
for (const entry of sessionManager.getBranch(entryId)) {
if (entry.type === "message" || entry.type === "compaction") seq += 1;
seqByEntryId.set(entry.id, seq);
}
return seqByEntryId.get(entryId);
}
function resolveAppendedMessageSeq(params) {
if (typeof params.entryId !== "string") return;
const parentSeq = resolveEntryTranscriptSeq(params.sessionManager, params.parentEntryId, params.seqByEntryId);
if (parentSeq === void 0) return;
const messageSeq = parentSeq + 1;
params.seqByEntryId.set(params.entryId, messageSeq);
return messageSeq;
}
const MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES = 8192;
const MAX_PERSISTED_DETAIL_STRING_CHARS = 2e3;
const MAX_PERSISTED_DETAIL_SESSION_COUNT = 10;
const MAX_PERSISTED_DETAIL_FALLBACK_STRING_CHARS = 200;
const MAX_PERSISTED_DETAIL_REDACTION_LOOKAHEAD_CHARS = 1024;
const MAX_PERSISTED_DETAIL_BOUNDARY_OVERLAP_CHARS = 512;
const PERSISTED_DETAIL_REDACTION_BOUNDARY = "\0OPENCLAW_PERSISTED_DETAIL_BOUNDARY\0";
const PARTIAL_STRUCTURED_SECRET_VALUE_RE = /(?:["']?(?:api[-_]?key|apikey|token|secret|password|passwd|access[-_]?token|accesstoken|refresh[-_]?token|refreshtoken|auth[-_]?token|authtoken|client[-_]?secret|clientsecret|app[-_]?secret|appsecret|card[-_]?number|cardnumber|cvc|cvv)["']?\s*[:=]\s*["']?)(?!\*{3})(?=[^\s"',}\]]{8,})/i;
const PARTIAL_PRIVATE_KEY_BLOCK_RE = /-----BEGIN [A-Z0-9 ]*(?:PRIVATE KEY|OPENSSH PRIVATE KEY|RSA PRIVATE KEY|EC PRIVATE KEY|DSA PRIVATE KEY)-----/i;
function originalDetailsSizeFields(size) {
return size.complete ? { originalDetailsBytes: size.bytes } : { originalDetailsBytesAtLeast: size.bytes };
}
function redactPersistedDetailString(value, maxChars = MAX_PERSISTED_DETAIL_STRING_CHARS, redactionConfig) {
if (value.length <= maxChars) return redactToolPayloadTextWithConfig(value, redactionConfig);
const scan = `${sliceUtf16Safe(value, 0, maxChars)}${PERSISTED_DETAIL_REDACTION_BOUNDARY}${sliceUtf16Safe(value, maxChars, maxChars + MAX_PERSISTED_DETAIL_REDACTION_LOOKAHEAD_CHARS)}`;
const redactedScan = redactToolPayloadTextWithConfig(scan, redactionConfig);
const boundaryIndex = redactedScan.indexOf(PERSISTED_DETAIL_REDACTION_BOUNDARY);
const redactedPrefix = boundaryIndex >= 0 ? redactedScan.slice(0, boundaryIndex) : "[OpenClaw persisted detail redacted: boundary marker removed]";
const safePrefixChars = Math.max(0, maxChars - Math.min(maxChars, MAX_PERSISTED_DETAIL_BOUNDARY_OVERLAP_CHARS));
const initialPersistedPrefix = truncateUtf16Safe(redactedPrefix, safePrefixChars);
const persistedPrefix = PARTIAL_STRUCTURED_SECRET_VALUE_RE.test(initialPersistedPrefix) || PARTIAL_PRIVATE_KEY_BLOCK_RE.test(initialPersistedPrefix) ? "[OpenClaw persisted detail redacted: partial secret span omitted]" : initialPersistedPrefix;
return `${persistedPrefix}${persistedPrefix ? "\n" : ""}[OpenClaw persisted detail redacted: boundary overlap omitted]\n\n[OpenClaw persisted detail truncated: ${Math.max(0, value.length - maxChars)} original chars omitted]`;
}
function selectPersistedDetailRedactionKey(key, inheritedKey) {
return isSensitiveFieldKey(key) ? key : inheritedKey;
}
function redactedOriginalDetailKeys(src, redactionConfig) {
return firstEnumerableOwnKeys(src, 40).map((key) => redactToolPayloadTextWithConfig(key, redactionConfig));
}
function redactPersistedDetailValue(value, depth = 0, redactionKey, redactionConfig) {
if (typeof value === "string") return redactionKey ? redactSensitiveFieldValueWithConfig(redactionKey, value, redactionConfig) : redactToolPayloadTextWithConfig(value, redactionConfig);
if (redactionKey && (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")) return redactSensitiveFieldValueWithConfig(redactionKey, String(value), redactionConfig);
if (value === null || value === void 0 || typeof value !== "object") return value;
if (depth >= 8) return "[OpenClaw persisted detail redacted: max depth exceeded]";
if (Array.isArray(value)) {
let changed = false;
const next = value.map((item) => {
const redacted = redactPersistedDetailValue(item, depth + 1, redactionKey, redactionConfig);
changed ||= redacted !== item;
return redacted;
});
return changed ? next : value;
}
const source = value;
let changed = false;
const next = {};
for (const [key, field] of Object.entries(source)) {
const redactedKey = redactToolPayloadTextWithConfig(key, redactionConfig);
const redacted = redactPersistedDetailValue(field, depth + 1, selectPersistedDetailRedactionKey(key, redactionKey), redactionConfig);
changed ||= redactedKey !== key || redacted !== field;
next[redactedKey] = redacted;
}
return changed ? next : value;
}
function redactPersistedSummaryField(key, value, maxStringChars, redactionConfig) {
if (typeof value === "string") return redactPersistedDetailString(value, maxStringChars, redactionConfig);
return redactPersistedDetailValue(value, 0, selectPersistedDetailRedactionKey(key, void 0), redactionConfig);
}
function copyPersistedSummaryFields(params) {
for (const key of params.keys) {
const value = params.source[key];
if (value !== void 0) params.target[key] = redactPersistedSummaryField(key, value, params.maxChars, params.redactionConfig);
}
}
function sanitizePersistedSessionDetail(value, redactionConfig) {
if (!value || typeof value !== "object") return value;
const src = value;
const out = {};
copyPersistedSummaryFields({
target: out,
source: src,
keys: [
"sessionId",
"status",
"pid",
"startedAt",
"endedAt",
"runtimeMs",
"cwd",
"name",
"truncated",
"exitCode",
"exitSignal"
],
maxChars: 500,
redactionConfig
});
if (typeof src.command === "string") out.command = redactPersistedDetailString(src.command, 500, redactionConfig);
return out;
}
function copyPersistedResultStateFields(out, src, maxStringChars, redactionConfig) {
for (const key of [
"disabled",
"unavailable",
"success"
]) if (typeof src[key] === "boolean") out[key] = src[key];
if (typeof src.error === "string" && src.error) out.error = redactPersistedDetailString(src.error, maxStringChars, redactionConfig);
else if (src.error) out.error = true;
}
function buildPersistedDetailsFallback(src, originalSize, sanitizedBytes, redactionConfig) {
const fallback = {
persistedDetailsTruncated: true,
finalDetailsTruncated: true,
...originalDetailsSizeFields(originalSize)
};
if (sanitizedBytes !== void 0) fallback.sanitizedDetailsBytes = sanitizedBytes;
if (src) {
fallback.originalDetailKeys = redactedOriginalDetailKeys(src, redactionConfig);
copyPersistedSummaryFields({
target: fallback,
source: src,
keys: [
"status",
"sessionId",
"pid",
"exitCode",
"exitSignal",
"truncated",
"spill",
"fullOutputPath",
"spilledChars",
"spillTruncated"
],
maxChars: MAX_PERSISTED_DETAIL_FALLBACK_STRING_CHARS,
redactionConfig
});
copyPersistedResultStateFields(fallback, src, MAX_PERSISTED_DETAIL_FALLBACK_STRING_CHARS, redactionConfig);
}
return fallback;
}
function enforcePersistedDetailsByteCap(value, originalDetails, originalSize, redactionConfig) {
const sanitizedBytes = jsonUtf8BytesOrInfinity(value);
if (sanitizedBytes <= MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES) return value;
const fallback = isRecord(originalDetails) ? buildPersistedDetailsFallback(originalDetails, originalSize, sanitizedBytes, redactionConfig) : {
persistedDetailsTruncated: true,
finalDetailsTruncated: true,
...originalDetailsSizeFields(originalSize),
sanitizedDetailsBytes: sanitizedBytes
};
if (jsonUtf8BytesOrInfinity(fallback) <= MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES) return fallback;
return {
persistedDetailsTruncated: true,
finalDetailsTruncated: true,
...originalDetailsSizeFields(originalSize),
sanitizedDetailsBytes: sanitizedBytes
};
}
function sanitizeToolResultDetailsForPersistence(details, redactionConfig) {
if (details === void 0 || details === null) return details;
const originalSize = boundedJsonUtf8Bytes(details, MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES);
if (originalSize.complete && originalSize.bytes <= MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES) return enforcePersistedDetailsByteCap(redactPersistedDetailValue(details, 0, void 0, redactionConfig), details, originalSize, redactionConfig);
if (typeof details !== "object") return enforcePersistedDetailsByteCap({
persistedDetailsTruncated: true,
...originalDetailsSizeFields(originalSize),
valueType: typeof details
}, void 0, originalSize, redactionConfig);
const src = details;
const out = {
persistedDetailsTruncated: true,
...originalDetailsSizeFields(originalSize),
originalDetailKeys: redactedOriginalDetailKeys(src, redactionConfig)
};
copyPersistedSummaryFields({
target: out,
source: src,
keys: [
"status",
"sessionId",
"pid",
"startedAt",
"endedAt",
"cwd",
"name",
"exitCode",
"exitSignal",
"retryInMs",
"total",
"totalLines",
"totalChars",
"truncated",
"spill",
"fullOutputPath",
"spilledChars",
"spillTruncated",
"truncation"
],
maxChars: MAX_PERSISTED_DETAIL_STRING_CHARS,
redactionConfig
});
copyPersistedResultStateFields(out, src, MAX_PERSISTED_DETAIL_STRING_CHARS, redactionConfig);
if (typeof src.tail === "string") out.tail = redactPersistedDetailString(src.tail, MAX_PERSISTED_DETAIL_STRING_CHARS, redactionConfig);
if (Array.isArray(src.sessions)) {
out.sessions = src.sessions.slice(0, MAX_PERSISTED_DETAIL_SESSION_COUNT).map((session) => sanitizePersistedSessionDetail(session, redactionConfig));
if (src.sessions.length > MAX_PERSISTED_DETAIL_SESSION_COUNT) out.sessionsTruncated = src.sessions.length - MAX_PERSISTED_DETAIL_SESSION_COUNT;
}
return enforcePersistedDetailsByteCap(out, src, originalSize, redactionConfig);
}
function capToolResultForPersistence(msg, maxChars, redactionConfig) {
const capped = capToolResultSize(msg, maxChars);
if (capped.role !== "toolResult") return capped;
const details = capped.details;
const sanitizedDetails = sanitizeToolResultDetailsForPersistence(details, redactionConfig);
return sanitizedDetails === details ? capped : {
...capped,
details: sanitizedDetails
};
}
function normalizePersistedToolResultName(message, fallbackName, fallbackId) {
if (message.role !== "toolResult") return message;
const toolResult = message;
const rawToolName = toolResult.toolName;
const normalizedToolName = normalizeOptionalString(rawToolName);
const normalizedFallback = normalizeOptionalString(fallbackName);
const toolName = normalizedToolName ?? normalizedFallback ?? "unknown";
const rawToolCallIdValue = toolResult.toolCallId;
const rawToolCallId = typeof rawToolCallIdValue === "string" ? rawToolCallIdValue : void 0;
const toolCallId = rawToolCallId ?? normalizeOptionalString(fallbackId);
const isError = typeof toolResult.isError === "boolean" ? toolResult.isError : false;
if (rawToolName === toolName && rawToolCallId === toolCallId && toolResult.isError === isError) return toolResult;
return {
...toolResult,
...toolCallId ? { toolCallId } : {},
toolName,
isError
};
}
function isTranscriptOnlyOpenClawAssistantMessage(message) {
if (!message || message.role !== "assistant") return false;
const provider = normalizeOptionalString(message.provider) ?? "";
const model = normalizeOptionalString(message.model) ?? "";
return isTranscriptOnlyOpenClawAssistantModel(provider, model);
}
function extractPendingAssistantToolCalls(message) {
return message.role === "assistant" && message.stopReason !== "aborted" && message.stopReason !== "error" ? extractToolCallsFromAssistant(message) : [];
}
function installSessionToolResultGuard(sessionManager, opts) {
const originalAppend = getRawSessionAppendMessage(sessionManager);
const originalAppendWithTranscriptAnchor = sessionManager.appendMessageWithTranscriptAnchor.bind(sessionManager);
setRawSessionAppendMessage(sessionManager, originalAppend);
const pendingState = createPendingToolCallState();
const persistMessage = (message, sourceAppend) => {
const transformer = opts?.transformMessageForPersistence;
const persisted = transformer ? transformer(message) : message;
copyCodeModeSourceAppend(message, persisted, sourceAppend);
return persisted;
};
const persistToolResult = (message, meta) => {
const transformer = opts?.transformToolResultForPersistence;
return transformer ? transformer(message, meta) : message;
};
const allowSyntheticToolResults = opts?.allowSyntheticToolResults ?? true;
const missingToolResultText = opts?.missingToolResultText;
const beforeWrite = opts?.beforeMessageWriteHook;
const toolResultTransformerMayMutate = opts?.transformToolResultForPersistence !== void 0;
const redactionConfig = opts?.redactLoggingConfig;
const maxToolResultChars = resolveMaxToolResultChars(opts);
const transcriptSeqByEntryId = /* @__PURE__ */ new Map();
let transcriptRunId = opts?.runId;
let suppressNextUserMessagePersistence = opts?.suppressNextUserMessagePersistence === true;
const appendMessageAndCacheTranscriptSeq = (message, options, sourceAppend, acknowledgementSource = message) => {
const runOwnedMessage = attachSessionTranscriptRunId(message, transcriptRunId);
copyCodeModeSourceAppend(message, runOwnedMessage, sourceAppend);
const parentEntryId = sessionManager.getLeafId();
const { entryId, anchor, appended, message: persistedMessage } = withRuntimeUserTurnTranscriptRecorder(runOwnedMessage, () => originalAppendWithTranscriptAnchor(runOwnedMessage, sourceAppend ? prepareCodeModeSourceAppend(options ?? {}, runOwnedMessage, sourceAppend) : options));
acknowledgeInternalToolResult(acknowledgementSource);
const persistedId = persistedMessage.role === "toolResult" ? extractToolResultId(persistedMessage) : null;
if (persistedId) pendingState.delete(persistedId);
pendingState.trackToolCalls(extractPendingAssistantToolCalls(persistedMessage));
if (!appended) return {
entryId,
message: persistedMessage,
appended,
...anchor ? { anchor } : {}
};
opts?.onMessagePersisted?.(persistedMessage);
const sessionTarget = sessionManager.getSessionTarget();
if (!sessionTarget) return {
entryId,
message: persistedMessage,
appended,
...anchor ? { anchor } : {}
};
return {
entryId,
appended,
message: persistedMessage,
...anchor ? { anchor } : {},
sessionTarget,
messageSeq: resolveAppendedMessageSeq({
sessionManager,
entryId,
parentEntryId,
seqByEntryId: transcriptSeqByEntryId
})
};
};
const originalAppendCompaction = sessionManager.appendCompaction.bind(sessionManager);
const guardedAppendCompaction = ((...args) => {
args[5] = {
runId: transcriptRunId,
...args[5]
};
const append = () => originalAppendCompaction(...args);
return opts?.withCompactionPersistence ? opts.withCompactionPersistence(append, isExpectedCompactionAppend) : append();
});
/**
* Run the before_message_write hook. Returns the (possibly modified) message,
* or null if the message should be blocked.
*/
const applyBeforeWriteHook = (msg, sourceAppend) => {
if (!beforeWrite) return {
message: msg,
changed: false
};
const result = beforeWrite({ message: msg }, sourceAppend);
if (result?.block) return null;
if (result?.message) return {
message: result.message,
changed: true
};
return {
message: msg,
changed: false
};
};
const flushPendingToolResults = () => {
if (pendingState.size() === 0) return;
if (allowSyntheticToolResults) for (const [id, name] of pendingState.entries()) {
const synthetic = makeMissingToolResult({
toolCallId: id,
toolName: name,
text: missingToolResultText
});
const persistedSynthetic = persistMessage(synthetic);
const transformed = persistToolResult(persistedSynthetic, {
toolCallId: id,
toolName: name,
isSynthetic: true
});
const flushed = applyBeforeWriteHook(transformed);
if (flushed) {
const canonical = flushed.message.role === "toolResult" ? rewriteToolResultIds({
message: flushed.message,
resolveId: () => id
}) : flushed.message;
appendMessageAndCacheTranscriptSeq(capToolResultForPersistence(canonical, maxToolResultChars, redactionConfig), { invalidateSerializedPrefixCache: persistedSynthetic !== synthetic || toolResultTransformerMayMutate || canonical !== flushed.message || flushed.changed });
}
}
pendingState.clear();
};
const clearPendingToolResults = () => {
pendingState.clear();
};
const guardedAppend = (message, callerOptions, sourceAppend) => {
const callerInvalidatesCache = callerOptions?.invalidateSerializedPrefixCache === true;
let nextMessage = message;
if (message.role === "assistant") {
const sanitized = sanitizeToolCallInputs([message], { allowedToolNames: opts?.allowedToolNames });
if (sanitized.length === 0) {
if (pendingState.shouldFlushForSanitizedDrop()) flushPendingToolResults();
return;
}
const sanitizedMessage = sanitized.at(0);
if (!sanitizedMessage) return;
nextMessage = sanitizedMessage;
copyCodeModeSourceAppend(message, nextMessage, sourceAppend);
}
const nextRole = nextMessage.role;
if (nextRole === "toolResult") {
const id = extractToolResultId(nextMessage);
const toolName = id ? pendingState.getToolName(id) : void 0;
const normalizedToolResult = normalizePersistedToolResultName(nextMessage, toolName, id ?? void 0);
const persistedToolResult = persistMessage(normalizedToolResult);
const capped = capToolResultForPersistence(persistedToolResult, maxToolResultChars, redactionConfig);
const transformed = persistToolResult(capped, {
toolCallId: id ?? void 0,
toolName,
isSynthetic: false
});
const persisted = applyBeforeWriteHook(transformed);
if (!persisted) return;
return appendMessageAndCacheTranscriptSeq(capToolResultForPersistence(persisted.message, maxToolResultChars, redactionConfig), { invalidateSerializedPrefixCache: callerInvalidatesCache || persistedToolResult !== normalizedToolResult || toolResultTransformerMayMutate || persisted.changed }, void 0, message).entryId;
}
const toolCalls = extractPendingAssistantToolCalls(nextMessage);
if (!(nextRole === "custom" && "excludeFromContext" in nextMessage && nextMessage.excludeFromContext === true || nextRole === "assistant" && toolCalls.length === 0 && isTranscriptOnlyOpenClawAssistantMessage(nextMessage)) && pendingState.shouldFlushBeforeNonToolResult(nextRole, toolCalls.length)) flushPendingToolResults();
if (!allowSyntheticToolResults && pendingState.shouldFlushBeforeNewToolCalls(toolCalls.length)) flushPendingToolResults();
const transformedMessage = persistMessage(nextMessage, sourceAppend);
const finalWrite = applyBeforeWriteHook(transformedMessage, sourceAppend);
if (!finalWrite) {
if (isUserAgentMessage(transformedMessage)) opts?.onUserMessageBlocked?.(transformedMessage);
return;
}
const finalMessage = finalWrite.message;
const finalRole = finalMessage.role;
if (finalRole === "assistant" && toolCalls.length === 0 && opts?.suppressTranscriptOnlyAssistantPersistence === true) return;
if (finalRole === "assistant" && opts?.suppressAssistantErrorPersistence === true && finalMessage.stopReason === "error") return;
if (isUserAgentMessage(finalMessage) && suppressNextUserMessagePersistence) {
suppressNextUserMessagePersistence = false;
opts?.onUserMessagePersistenceSuppressed?.(finalMessage);
return;
}
const { anchor, appended, entryId: result, message: persistedMessage, messageSeq, sessionTarget } = appendMessageAndCacheTranscriptSeq(finalMessage, { invalidateSerializedPrefixCache: callerInvalidatesCache || transformedMessage !== nextMessage || finalWrite.changed }, sourceAppend, message);
if (sessionTarget) {
const runId = resolveTerminalAssistantTranscriptRunId(persistedMessage, transcriptRunId);
publishTranscriptUpdate(sessionTarget, {
message: persistedMessage,
messageId: typeof result === "string" ? result : void 0,
...messageSeq !== void 0 ? { messageSeq } : {},
...runId ? { runId } : {}
});
}
if (isUserAgentMessage(finalMessage) && isUserAgentMessage(persistedMessage)) opts?.onUserMessagePersisted?.(finalMessage, {
...anchor ? { anchor } : {},
appended,
entryId: result,
persistedMessage,
...sessionTarget ? { sessionTarget } : {}
});
if (finalRole === "assistant" && finalMessage.stopReason === "error") opts?.onAssistantErrorMessagePersisted?.(finalMessage);
return result;
};
sessionManager.appendMessage = ((message, options) => withCodeModeSourceAppend(message, options, (sourceAppend) => guardedAppend(message, options, sourceAppend)));
sessionManager.appendCompaction = guardedAppendCompaction;
return {
flushPendingToolResults,
clearPendingToolResults,
clearNextUserMessagePersistenceSuppression: () => {
suppressNextUserMessagePersistence = false;
},
getPendingIds: pendingState.getPendingIds,
setTranscriptRunId: (runId) => {
transcriptRunId = runId;
}
};
}
//#endregion
//#region src/agents/session-tool-result-guard-wrapper.ts
/**
* Apply the tool-result guard to a SessionManager exactly once and expose
* a flush method on the instance for easy teardown handling.
*/
function guardSessionManager(sessionManager, opts) {
const guardedSessionManager = sessionManager;
let prepareAssistantTranscriptMessage = opts?.trigger === "memory" ? void 0 : opts?.prepareAssistantTranscriptMessage;
let skipBeforeMessageWriteHooks = opts?.skipBeforeMessageWriteHooks;
if (typeof guardedSessionManager.flushPendingToolResults === "function") {
guardedSessionManager.setTranscriptRunContext?.(opts?.runId, prepareAssistantTranscriptMessage, skipBeforeMessageWriteHooks);
return guardedSessionManager;
}
const hookRunner = getGlobalHookRunner();
let pendingPreparedUserTurnMessage = opts?.preparedUserTurnMessage;
const preparedUserReplayKey = opts?.preparedUserTurnTranscriptRecorder?.getPersistedMessage?.()?.idempotencyKey === pendingPreparedUserTurnMessage?.idempotencyKey ? pendingPreparedUserTurnMessage?.idempotencyKey : void 0;
let queuedUserTurnTranscriptRecorder;
const runtimeUserMessageByPersistedMessage = /* @__PURE__ */ new WeakMap();
const beforeMessageWrite = (event, sourceAppend) => {
const runtimeUserMessage = runtimeUserMessageByPersistedMessage.get(event.message);
let message = event.message;
let changed = false;
const skipUserWriteHook = skipBeforeMessageWriteHooks || message.role === "user" && queuedUserTurnTranscriptRecorder?.getPendingInputMessage?.() !== void 0;
if (!skipUserWriteHook && hookRunner?.hasHooks("before_message_write") || prepareAssistantTranscriptMessage) {
const preparedMessage = message.role === "user" ? {
...message,
__openclaw: { ...Reflect.get(message, "__openclaw") }
} : void 0;
if (preparedMessage?.["__openclaw"].humanMentions !== void 0) {
preparedMessage.content = structuredClone(preparedMessage.content);
preparedMessage["__openclaw"].humanMentions = structuredClone(preparedMessage["__openclaw"].humanMentions);
}
const next = runAgentHarnessBeforeMessageWriteHook({
message,
agentId: opts?.agentId,
sessionKey: opts?.sessionKey,
prepareAssistantTranscriptMessage,
skipBeforeMessageWriteHooks: skipUserWriteHook
});
if (!next) {
runtimeUserMessageByPersistedMessage.delete(event.message);
queuedUserTurnTranscriptRecorder?.markBlocked();
queuedUserTurnTranscriptRecorder = void 0;
return { block: true };
}
message = restorePreparedUserTurnOperationalMetaForRuntime({
runtimeMessage: next,
preparedMessage
});
changed = true;
}
copyCodeModeSourceAppend(event.message, message, sourceAppend);
const redacted = redactTranscriptMessage(message, opts?.config, sourceAppend);
if (redacted !== message) {
message = redacted;
changed = true;
}
const projectedMessage = projectAgentHarnessTranscriptMessageForDisplay({
hidden: opts?.trigger === "memory",
message
});
if (projectedMessage !== message) {
copyCodeModeSourceAppend(message, projectedMessage, sourceAppend);
message = projectedMessage;
changed = true;
}
if (message.role !== "user" && queuedUserTurnTranscriptRecorder) {
queuedUserTurnTranscriptRecorder.markBlocked();
queuedUserTurnTranscriptRecorder = void 0;
}
if (message.role === "user" && queuedUserTurnTranscriptRecorder) {
message = attachRuntimeUserTurnTranscriptRecorder(message, queuedUserTurnTranscriptRecorder);
queuedUserTurnTranscriptRecorder = void 0;
}
if (runtimeUserMessage && message.role === "user") runtimeUserMessageByPersistedMessage.set(message, runtimeUserMessage);
return changed ? { message } : void 0;
};
const transform = hookRunner?.hasHooks("tool_result_persist") ? (message, meta) => {
return hookRunner.runToolResultPersist({
toolName: meta.toolName,
toolCallId: meta.toolCallId,
message,
isSynthetic: meta.isSynthetic
}, {
agentId: opts?.agentId,
sessionKey: opts?.sessionKey,
toolName: meta.toolName,
toolCallId: meta.toolCallId
})?.message ?? message;
} : void 0;
const guard = installSessionToolResultGuard(sessionManager, {
sessionKey: opts?.sessionKey,
agentId: opts?.agentId,
runId: opts?.runId,
transformMessageForPersistence: (message) => {
queuedUserTurnTranscriptRecorder = void 0;
const withProvenance = applyInputProvenanceToUserMessage(message, opts?.inputProvenance);
const runtimeContext = takeRuntimeUserTurnTranscriptContext(message);
if (message.role === "user" && preparedUserReplayKey !== void 0 && Reflect.get(runtimeContext?.message ?? message, "idempotencyKey") !== preparedUserReplayKey) pendingPreparedUserTurnMessage = void 0;
const prepared = runtimeContext?.message ?? pendingPreparedUserTurnMessage;
const recorder = runtimeContext?.recorder ?? (prepared !== void 0 && prepared === pendingPreparedUserTurnMessage ? opts?.preparedUserTurnTranscriptRecorder : void 0);
if (message.role === "user") opts?.onUserMessagePreparingForPersistence?.(message, recorder, prepared);
const merged = mergePreparedUserTurnMessageForRuntime({
runtimeMessage: withProvenance,
...prepared ? { preparedMessage: prepared } : {}
});
if (merged !== withProvenance) {
queuedUserTurnTranscriptRecorder = recorder;
if (!runtimeContext) pendingPreparedUserTurnMessage = void 0;
}
if (message.role === "user" && merged.role === "user") runtimeUserMessageByPersistedMessage.set(merged, message);
return merged;
},
transformToolResultForPersistence: transform,
allowSyntheticToolResults: opts?.allowSyntheticToolResults,
missingToolResultText: opts?.missingToolResultText,
allowedToolNames: opts?.allowedToolNames,
beforeMessageWriteHook: beforeMessageWrite,
redactLoggingConfig: opts?.config?.logging,
maxToolResultChars: typeof opts?.contextWindowTokens === "number" ? resolveLiveToolResultMaxChars({ contextWindowTokens: opts.contextWindowTokens }) : void 0,
suppressNextUserMessagePersistence: preparedUserReplayKey === void 0 && opts?.suppressNextUserMessagePersistence,
suppressTranscriptOnlyAssistantPersistence: opts?.suppressTranscriptOnlyAssistantPersistence,
suppressAssistantErrorPersistence: opts?.suppressAssistantErrorPersistence,
onMessagePersisted: opts?.onMessagePersisted,
withCompactionPersistence: opts?.withCompactionPersistence,
onUserMessagePersisted: async (message, persistence) => {
const runtimeMessage = runtimeUserMessageByPersistedMessage.get(message);
runtimeUserMessageByPersistedMessage.delete(message);
takeRuntimeUserTurnTranscriptRecorder(message)?.markRuntimePersisted(persistence.persistedMessage, persistence.anchor, { appended: persistence.appended });
await opts?.onUserMessagePersisted?.(persistence.persistedMessage, runtimeMessage);
},
onUserMessagePersistenceSuppressed: async (message) => {
const runtimeMessage = runtimeUserMessageByPersistedMessage.get(message);
runtimeUserMessageByPersistedMessage.delete(message);
await opts?.onUserMessagePersistenceSuppressed?.(message, runtimeMessage);
},
onUserMessageBlocked: opts?.onUserMessageBlocked,
onAssistantErrorMessagePersisted: opts?.onAssistantErrorMessagePersisted
});
guardedSessionManager.flushPendingToolResults = guard.flushPendingToolResults;
guardedSessionManager.clearPendingToolResults = guard.clearPendingToolResults;
guardedSessionManager.clearNextUserMessagePersistenceSuppression = guard.clearNextUserMessagePersistenceSuppression;
guardedSessionManager.setTranscriptRunContext = (runId, prepare, skipHooks) => {
guard.setTranscriptRunId(runId);
prepareAssistantTranscriptMessage = prepare;
skipBeforeMessageWriteHooks = skipHooks;
};
return guardedSessionManager;
}
//#endregion
export { guardSessionManager as t };