UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

691 lines (690 loc) 33.9 kB
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 };