From 043e8a6216e7785b170b239d686e7d4297f63fca Mon Sep 17 00:00:00 2001 From: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Wed, 6 May 2026 00:08:12 +1000 Subject: [PATCH] fix: keep before-agent blocks redacted --- .../ai/openclaw/app/chat/ChatController.kt | 44 +---- .../java/ai/openclaw/app/chat/ChatModels.kt | 1 - .../openclaw/app/ui/chat/ChatMessageViews.kt | 38 ---- .../Chat/IOSGatewayChatTransport.swift | 28 +-- .../Sources/OpenClaw/GatewayConnection.swift | 24 +-- .../OpenClawProtocol/GatewayModels.swift | 6 +- .../OpenClawChatUI/ChatMessageViews.swift | 9 +- .../Sources/OpenClawChatUI/ChatModels.swift | 22 +-- .../OpenClawChatUI/ChatViewModel.swift | 32 +--- .../OpenClawProtocol/GatewayModels.swift | 6 +- .../OpenClawKitTests/ChatViewModelTests.swift | 2 - src/agents/cli-runner.reliability.test.ts | 9 +- src/agents/cli-runner.ts | 4 +- src/agents/cli-runner/session-history.ts | 24 +-- src/agents/model-fallback.test.ts | 22 +++ .../result-fallback-classifier.ts | 3 + .../run.incomplete-turn.test.ts | 13 +- src/agents/pi-embedded-runner/run.ts | 7 +- .../pi-embedded-runner/run/attempt.test.ts | 7 +- src/agents/pi-embedded-runner/run/attempt.ts | 39 +--- src/gateway/chat-display-projection.ts | 8 - src/gateway/protocol/schema/logs-chat.ts | 1 - .../chat.directive-tags.test.ts | 176 ------------------ src/gateway/server-methods/chat.ts | 10 +- .../server-methods/server-methods.test.ts | 25 --- src/gateway/server-session-events.ts | 3 +- src/gateway/session-history-state.test.ts | 60 ------ src/gateway/session-history-state.ts | 13 +- src/gateway/session-message-events.test.ts | 15 +- src/gateway/session-utils.fs.test.ts | 18 +- src/gateway/session-utils.fs.ts | 54 +----- src/gateway/session-utils.ts | 1 - .../sessions-history-http.revocation.test.ts | 125 +------------ src/gateway/sessions-history-http.ts | 25 +-- ui/src/ui/chat/grouped-render.test.ts | 6 +- ui/src/ui/chat/grouped-render.ts | 3 +- ui/src/ui/chat/message-extract.ts | 24 --- ui/src/ui/chat/message-normalizer.test.ts | 23 --- ui/src/ui/chat/message-normalizer.ts | 13 +- ui/src/ui/types/chat-types.ts | 1 - 40 files changed, 94 insertions(+), 850 deletions(-) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt b/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt index b14d30393dc..7f1c8f8ef59 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt @@ -334,30 +334,12 @@ class ChatController( } private suspend fun requestChatHistoryJson(sessionKey: String): String { - val params = + return session.request( + "chat.history", buildJsonObject { put("sessionKey", JsonPrimitive(sessionKey)) - put("includeBlockedOriginalContent", JsonPrimitive(true)) - } - val response = session.requestDetailed("chat.history", params.toString()) - if (response.ok) return response.payloadJson ?: "" - val error = response.error - if ( - error?.code == "INVALID_REQUEST" && - error.message.contains("includeBlockedOriginalContent") - ) { - val legacyParams = - buildJsonObject { - put("sessionKey", JsonPrimitive(sessionKey)) - } - val legacyResponse = session.requestDetailed("chat.history", legacyParams.toString()) - if (legacyResponse.ok) return legacyResponse.payloadJson ?: "" - val legacyError = legacyResponse.error - throw IllegalStateException( - "${legacyError?.code ?: "UNAVAILABLE"}: ${legacyError?.message ?: "request failed"}", - ) - } - throw IllegalStateException("${error?.code ?: "UNAVAILABLE"}: ${error?.message ?: "request failed"}") + }.toString(), + ) } private suspend fun pollHealthIfNeeded(force: Boolean) { @@ -535,21 +517,11 @@ class ChatController( val obj = item.asObjectOrNull() ?: return@mapNotNull null val role = obj["role"].asStringOrNull() ?: return@mapNotNull null val content = obj["content"].asArrayOrNull()?.mapNotNull(::parseMessageContent) ?: emptyList() - val originalBlockedContent = - obj["__openclaw"] - .asObjectOrNull() - ?.get("originalBlockedContent") - .asObjectOrNull() - ?.get("content") - .asArrayOrNull() - ?.mapNotNull(::parseMessageContent) - ?: emptyList() val ts = obj["timestamp"].asLongOrNull() ChatMessage( id = UUID.randomUUID().toString(), role = role, content = content, - originalBlockedContent = originalBlockedContent, timestampMs = ts, ) } @@ -677,14 +649,6 @@ internal fun messageIdentityKey(message: ChatMessage): String? { .orEmpty(), ).joinToString(separator = "\u001F") } - val blockedFingerprint = - message.originalBlockedContent.joinToString(separator = "\u001E") { part -> - listOf( - part.type.trim().lowercase(), - part.text?.trim().orEmpty(), - ).joinToString(separator = "\u001F") - } - if (timestamp.isEmpty() && contentFingerprint.isEmpty()) return null return listOf(role, timestamp, contentFingerprint, blockedFingerprint).joinToString(separator = "|") } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatModels.kt b/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatModels.kt index 822546ef582..f6d08c535c5 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatModels.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatModels.kt @@ -4,7 +4,6 @@ data class ChatMessage( val id: String, val role: String, val content: List, - val originalBlockedContent: List = emptyList(), val timestampMs: Long?, ) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageViews.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageViews.kt index 23a142e91e6..5b204d57317 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageViews.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageViews.kt @@ -57,44 +57,6 @@ fun ChatMessageBubble(message: ChatMessage) { val role = message.role.trim().lowercase(Locale.US) val style = bubbleStyle(role) val displayContent = - if (role == "user" && message.originalBlockedContent.isNotEmpty()) { - message.originalBlockedContent - } else { - message.content - } - - // Filter to only displayable content parts (text with content, or base64 images). - val displayableContent = - displayContent.filter { part -> - when (part.type) { - "text" -> !part.text.isNullOrBlank() - else -> part.base64 != null - } - } - - if (displayableContent.isEmpty()) return - - ChatBubbleContainer(style = style, roleLabel = roleLabel(role)) { - ChatMessageBody(content = displayableContent, textColor = mobileText) - if (role == "user" && message.originalBlockedContent.isNotEmpty()) { - Surface( - color = Color.Transparent, - border = BorderStroke(0.dp, Color.Transparent), - modifier = Modifier.fillMaxWidth(), - ) { - Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { - androidx.compose.material3.HorizontalDivider( - color = mobileText.copy(alpha = 0.18f), - thickness = 1.dp, - ) - Text( - text = "The agent cannot read this message.", - style = mobileCaption1.copy(fontWeight = FontWeight.Medium), - color = mobileText.copy(alpha = 0.68f), - ) - } - } - } } } diff --git a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift index 51ee02ea5f8..d5add00d2db 100644 --- a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift +++ b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift @@ -54,35 +54,13 @@ struct IOSGatewayChatTransport: OpenClawChatTransport { } func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload { - struct ParamsWithBlockedOriginals: Codable { - var sessionKey: String - var includeBlockedOriginalContent: Bool - } - struct LegacyParams: Codable { - var sessionKey: String - } - let encoder = JSONEncoder() - let data = try encoder.encode( - ParamsWithBlockedOriginals(sessionKey: sessionKey, includeBlockedOriginalContent: true)) + struct Params: Codable { var sessionKey: String } + let data = try JSONEncoder().encode(Params(sessionKey: sessionKey)) let json = String(data: data, encoding: .utf8) - let res: Data - do { - res = try await self.gateway.request(method: "chat.history", paramsJSON: json, timeoutSeconds: 15) - } catch { - guard Self.isUnsupportedBlockedOriginalHistoryParam(error) else { throw error } - let legacyData = try encoder.encode(LegacyParams(sessionKey: sessionKey)) - let legacyJson = String(data: legacyData, encoding: .utf8) - res = try await self.gateway.request(method: "chat.history", paramsJSON: legacyJson, timeoutSeconds: 15) - } + let res = try await self.gateway.request(method: "chat.history", paramsJSON: json, timeoutSeconds: 15) return try JSONDecoder().decode(OpenClawChatHistoryPayload.self, from: res) } - private static func isUnsupportedBlockedOriginalHistoryParam(_ error: Error) -> Bool { - guard let response = error as? GatewayResponseError else { return false } - guard response.code == ErrorCode.invalidRequest.rawValue else { return false } - return response.message.contains("includeBlockedOriginalContent") - } - func sendMessage( sessionKey: String, message: String, diff --git a/apps/macos/Sources/OpenClaw/GatewayConnection.swift b/apps/macos/Sources/OpenClaw/GatewayConnection.swift index 7f876cbfb4d..73b9297002d 100644 --- a/apps/macos/Sources/OpenClaw/GatewayConnection.swift +++ b/apps/macos/Sources/OpenClaw/GatewayConnection.swift @@ -630,30 +630,14 @@ extension GatewayConnection { let resolvedKey = self.canonicalizeSessionKey(sessionKey) var params: [String: AnyCodable] = [ "sessionKey": AnyCodable(resolvedKey), - "includeBlockedOriginalContent": AnyCodable(true), ] if let limit { params["limit"] = AnyCodable(limit) } if let maxChars { params["maxChars"] = AnyCodable(maxChars) } let timeout = timeoutMs.map { Double($0) } - do { - return try await self.requestDecoded( - method: .chatHistory, - params: params, - timeoutMs: timeout) - } catch { - guard Self.isUnsupportedBlockedOriginalHistoryParam(error) else { throw error } - params.removeValue(forKey: "includeBlockedOriginalContent") - return try await self.requestDecoded( - method: .chatHistory, - params: params, - timeoutMs: timeout) - } - } - - private static func isUnsupportedBlockedOriginalHistoryParam(_ error: Error) -> Bool { - guard let response = error as? GatewayResponseError else { return false } - guard response.code == ErrorCode.invalidRequest.rawValue else { return false } - return response.message.contains("includeBlockedOriginalContent") + return try await self.requestDecoded( + method: .chatHistory, + params: params, + timeoutMs: timeout) } func chatSend( diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift index 31c2535d7eb..a76e925f931 100644 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift @@ -5554,25 +5554,21 @@ public struct ChatHistoryParams: Codable, Sendable { public let sessionkey: String public let limit: Int? public let maxchars: Int? - public let includeblockedoriginalcontent: Bool? public init( sessionkey: String, limit: Int?, - maxchars: Int?, - includeblockedoriginalcontent: Bool? = nil) + maxchars: Int?) { self.sessionkey = sessionkey self.limit = limit self.maxchars = maxchars - self.includeblockedoriginalcontent = includeblockedoriginalcontent } private enum CodingKeys: String, CodingKey { case sessionkey = "sessionKey" case limit case maxchars = "maxChars" - case includeblockedoriginalcontent = "includeBlockedOriginalContent" } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift index e03d16a41b6..c623a06fb78 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift @@ -275,14 +275,7 @@ private struct ChatMessageBody: View { } private var displayContent: [OpenClawChatMessageContent] { - if self.isBlockedUserMessage, let original = self.message.originalBlockedContent { - return original - } - return self.message.content - } - - private var isBlockedUserMessage: Bool { - self.isUser && !(self.message.originalBlockedContent?.isEmpty ?? true) + self.message.content } private var toolCalls: [OpenClawChatMessageContent] { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift index c82e46f4ff1..1b6a9e08568 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift @@ -139,25 +139,15 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable { public var id: UUID = .init() public let role: String public let content: [OpenClawChatMessageContent] - public let originalBlockedContent: [OpenClawChatMessageContent]? public let timestamp: Double? public let toolCallId: String? public let toolName: String? public let usage: OpenClawChatUsage? public let stopReason: String? - private struct OpenClawMetadata: Codable, Sendable { - let originalBlockedContent: OriginalBlockedContent? - } - - private struct OriginalBlockedContent: Codable, Sendable { - let content: [OpenClawChatMessageContent]? - } - enum CodingKeys: String, CodingKey { case role case content - case openclawMetadata = "__openclaw" case timestamp case toolCallId case tool_call_id @@ -171,7 +161,6 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable { id: UUID = .init(), role: String, content: [OpenClawChatMessageContent], - originalBlockedContent: [OpenClawChatMessageContent]? = nil, timestamp: Double?, toolCallId: String? = nil, toolName: String? = nil, @@ -181,7 +170,6 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable { self.id = id self.role = role self.content = content - self.originalBlockedContent = originalBlockedContent self.timestamp = timestamp self.toolCallId = toolCallId self.toolName = toolName @@ -201,9 +189,6 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable { container.decodeIfPresent(String.self, forKey: .tool_name) self.usage = try container.decodeIfPresent(OpenClawChatUsage.self, forKey: .usage) self.stopReason = try container.decodeIfPresent(String.self, forKey: .stopReason) - let metadata = try container.decodeIfPresent(OpenClawMetadata.self, forKey: .openclawMetadata) - self.originalBlockedContent = metadata?.originalBlockedContent?.content - if let decoded = try? container.decode([OpenClawChatMessageContent].self, forKey: .content) { self.content = decoded return @@ -239,12 +224,7 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable { try container.encodeIfPresent(self.usage, forKey: .usage) try container.encodeIfPresent(self.stopReason, forKey: .stopReason) try container.encode(self.content, forKey: .content) - if let originalBlockedContent = self.originalBlockedContent { - try container.encode( - OpenClawMetadata( - originalBlockedContent: OriginalBlockedContent(content: originalBlockedContent)), - forKey: .openclawMetadata) - } + } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift index 28433f52dee..8219bbf067e 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift @@ -281,27 +281,12 @@ public final class OpenClawChatViewModel { name: content.name, arguments: content.arguments) } - let sanitizedOriginalBlockedContent = message.originalBlockedContent?.map { content -> OpenClawChatMessageContent in - guard let text = content.text else { return content } - let cleaned = ChatMarkdownPreprocessor.preprocess(markdown: text).cleaned - return OpenClawChatMessageContent( - type: content.type, - text: cleaned, - thinking: content.thinking, - thinkingSignature: content.thinkingSignature, - mimeType: content.mimeType, - fileName: content.fileName, - content: content.content, - id: content.id, - name: content.name, - arguments: content.arguments) - } + return OpenClawChatMessage( id: message.id, role: message.role, content: sanitizedContent, - originalBlockedContent: sanitizedOriginalBlockedContent, timestamp: message.timestamp, toolCallId: message.toolCallId, toolName: message.toolName, @@ -318,21 +303,11 @@ public final class OpenClawChatViewModel { let fileName = (item.fileName ?? "").trimmingCharacters(in: .whitespacesAndNewlines) return [type, text, id, name, fileName].joined(separator: "\\u{001F}") }.joined(separator: "\\u{001E}") - let originalBlockedFingerprint = (message.originalBlockedContent ?? []).map { item in - let type = (item.type ?? "text").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - let text = (item.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - return [type, text].joined(separator: "\\u{001F}") - }.joined(separator: "\\u{001E}") - return [contentFingerprint, originalBlockedFingerprint].joined(separator: "\\u{001D}") + return contentFingerprint } private static func userVisibleContentFingerprint(for message: OpenClawChatMessage) -> String { - let content = { - if let originalBlockedContent = message.originalBlockedContent, !originalBlockedContent.isEmpty { - return originalBlockedContent - } - return message.content - }() + let content = message.content return content.map { item in let type = (item.type ?? "text").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() let text = (item.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) @@ -404,7 +379,6 @@ public final class OpenClawChatViewModel { id: reusedId, role: message.role, content: message.content, - originalBlockedContent: message.originalBlockedContent, timestamp: message.timestamp, toolCallId: message.toolCallId, toolName: message.toolName, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index 31c2535d7eb..a76e925f931 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -5554,25 +5554,21 @@ public struct ChatHistoryParams: Codable, Sendable { public let sessionkey: String public let limit: Int? public let maxchars: Int? - public let includeblockedoriginalcontent: Bool? public init( sessionkey: String, limit: Int?, - maxchars: Int?, - includeblockedoriginalcontent: Bool? = nil) + maxchars: Int?) { self.sessionkey = sessionkey self.limit = limit self.maxchars = maxchars - self.includeblockedoriginalcontent = includeblockedoriginalcontent } private enum CodingKeys: String, CodingKey { case sessionkey = "sessionKey" case limit case maxchars = "maxChars" - case includeblockedoriginalcontent = "includeBlockedOriginalContent" } } diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift index 4aea35ff208..9a528f0f2d1 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift @@ -16,7 +16,6 @@ private func chatBlockedUserMessage(redactedText: String, originalText: String, "role": "user", "content": [["type": "text", "text": redactedText]], "__openclaw": [ - "originalBlockedContent": [ "content": [["type": "text", "text": originalText]], ], ], @@ -630,7 +629,6 @@ extension TestChatTransportState { message.role == "user" && message.content.compactMap(\.text).joined(separator: "\n") == "The agent cannot read this message." && - message.originalBlockedContent?.compactMap(\.text).joined(separator: "\n") == "hello from mac webchat" } let hasAssistant = vm.messages.contains { message in diff --git a/src/agents/cli-runner.reliability.test.ts b/src/agents/cli-runner.reliability.test.ts index cab64853532..419d916115e 100644 --- a/src/agents/cli-runner.reliability.test.ts +++ b/src/agents/cli-runner.reliability.test.ts @@ -707,9 +707,12 @@ describe("runCliAgent reliability", () => { const lines = fs.readFileSync(sessionFile, "utf-8").trim().split("\n"); const blockedLine = JSON.parse(lines[lines.length - 1]); expect(blockedLine.message.content[0].text).toBe("The agent cannot read this message."); - expect(blockedLine.message.__openclaw.originalBlockedContent.content[0].text).toBe( - "secret prompt", - ); + expect(JSON.stringify(blockedLine)).not.toContain("secret prompt"); + expect(blockedLine.message.__openclaw.beforeAgentRunBlocked).toMatchObject({ + blockedBy: "policy-plugin", + reason: "contains protected content", + }); + expect(Object.hasOwn(blockedLine.message.__openclaw, "beforeAgentRunBlocked")).toBe(true); } finally { fs.rmSync(dir, { recursive: true, force: true }); } diff --git a/src/agents/cli-runner.ts b/src/agents/cli-runner.ts index 3369f862a3b..1fb2cc20d08 100644 --- a/src/agents/cli-runner.ts +++ b/src/agents/cli-runner.ts @@ -246,7 +246,6 @@ export async function runPreparedCliAgent( }): Promise => { try { const nowMs = Date.now(); - const originalText = params.transcriptPrompt ?? params.prompt; const sessionManager = SessionManager.open(params.sessionFile); sessionManager.appendMessage({ role: "user", @@ -254,8 +253,7 @@ export async function runPreparedCliAgent( timestamp: nowMs, idempotencyKey: `hook-block:before_agent_run:user:${params.runId}`, __openclaw: { - originalBlockedContent: { - content: originalText ? [{ type: "text", text: originalText }] : [], + beforeAgentRunBlocked: { blockedBy: block.pluginId, reason: block.reason, blockedAt: nowMs, diff --git a/src/agents/cli-runner/session-history.ts b/src/agents/cli-runner/session-history.ts index 41bdebb1f1b..dca82e48b6b 100644 --- a/src/agents/cli-runner/session-history.ts +++ b/src/agents/cli-runner/session-history.ts @@ -28,26 +28,6 @@ type HistoryEntry = { summary?: unknown; }; -function stripBlockedOriginalContentMeta(message: unknown): unknown { - if (!message || typeof message !== "object" || Array.isArray(message)) { - return message; - } - const record = message as Record; - const openclaw = - record.__openclaw && typeof record.__openclaw === "object" && !Array.isArray(record.__openclaw) - ? (record.__openclaw as Record) - : undefined; - if (!openclaw || !Object.hasOwn(openclaw, "originalBlockedContent")) { - return message; - } - const { originalBlockedContent: _originalBlockedContent, ...remainingOpenClaw } = openclaw; - const { __openclaw: _openclaw, ...remainingMessage } = record; - if (Object.keys(remainingOpenClaw).length === 0) { - return remainingMessage; - } - return { ...remainingMessage, __openclaw: remainingOpenClaw }; -} - function coerceHistoryText(content: unknown): string { if (typeof content === "string") { return content.trim(); @@ -199,7 +179,7 @@ export async function loadCliSessionHistoryMessages(params: { }): Promise { const history = (await loadCliSessionEntries(params)).flatMap((entry) => { const candidate = entry as HistoryEntry; - return candidate.type === "message" ? [stripBlockedOriginalContentMeta(candidate.message)] : []; + return candidate.type === "message" ? [candidate.message] : []; }); return limitAgentHookHistoryMessages(history, MAX_CLI_SESSION_HISTORY_MESSAGES); } @@ -228,7 +208,7 @@ export async function loadCliSessionReseedMessages(params: { const tailMessages = entries.slice(latestCompactionIndex + 1).flatMap((entry) => { const candidate = entry as HistoryEntry; - return candidate.type === "message" ? [stripBlockedOriginalContentMeta(candidate.message)] : []; + return candidate.type === "message" ? [candidate.message] : []; }); return [ { diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 1148cbb3fb3..29f47cd286b 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -698,6 +698,28 @@ describe("runWithModelFallback", () => { ).toBeNull(); }); + it("keeps before_agent_run hook blocks out of empty-result fallback", () => { + const runResult: EmbeddedPiRunResult = { + payloads: [], + meta: { + durationMs: 1, + livenessState: "blocked", + error: { + kind: "hook_block", + message: "Blocked by before-run policy.", + }, + }, + }; + + expect( + classifyEmbeddedPiRunResultForModelFallback({ + provider: "atlassian-ai-gateway-openai", + model: "gpt-5.5-2026-04-23", + result: runResult, + }), + ).toBeNull(); + }); + it("uses harness-owned terminal classification for GPT-5 fallback", () => { const runResult: EmbeddedPiRunResult = { payloads: [], diff --git a/src/agents/pi-embedded-runner/result-fallback-classifier.ts b/src/agents/pi-embedded-runner/result-fallback-classifier.ts index 27742d3362b..124787dc213 100644 --- a/src/agents/pi-embedded-runner/result-fallback-classifier.ts +++ b/src/agents/pi-embedded-runner/result-fallback-classifier.ts @@ -18,6 +18,9 @@ function isEmbeddedPiRunResult(value: unknown): value is EmbeddedPiRunResult { } function hasDeliberateSilentTerminalReply(result: EmbeddedPiRunResult): boolean { + if (result.meta.error?.kind === "hook_block") { + return true; + } return [result.meta.finalAssistantRawText, result.meta.finalAssistantVisibleText].some( (text) => typeof text === "string" && isSilentReplyPayloadText(text), ); diff --git a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts index c57fa58501a..f17e2371530 100644 --- a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts +++ b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts @@ -49,7 +49,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { mockedGlobalHookRunner.hasHooks.mockImplementation(() => false); }); - it("surfaces before_agent_run hook block messages instead of generic prompt failure text", async () => { + it("does not emit a duplicate agent payload when before_agent_run blocks", async () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce( makeAttemptResult({ assistantTexts: [], @@ -64,12 +64,11 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1); - expect(result.payloads).toEqual([ - { - text: "Blocked by before-run policy.", - isError: true, - }, - ]); + expect(result.payloads).toEqual([]); + expect(result.meta).toMatchObject({ + livenessState: "blocked", + error: { kind: "hook_block", message: "Blocked by before-run policy." }, + }); expect(result.meta?.error).toEqual({ kind: "hook_block", message: "Blocked by before-run policy.", diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 46d3f826d6f..e51d462694b 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -1834,12 +1834,7 @@ export async function runEmbeddedPiAgent( livenessState: "blocked", }); return { - payloads: [ - { - text: errorText, - isError: true, - }, - ], + payloads: [], meta: { durationMs: Date.now() - started, agentMeta: buildErrorAgentMeta({ diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index bc73c8d4ba9..1ed11223731 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -151,15 +151,14 @@ describe("normalizeMessagesForLlmBoundary", () => { ); }); - it("strips blocked original content metadata from the LLM boundary", () => { + it("keeps only safe blocked metadata at the LLM boundary", () => { const input = [ { role: "user", content: [{ type: "text", text: "The agent cannot read this message." }], timestamp: 1, __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret prompt" }], + beforeAgentRunBlocked: { blockedBy: "policy-plugin", reason: "contains protected content", blockedAt: 1, @@ -175,7 +174,7 @@ describe("normalizeMessagesForLlmBoundary", () => { expect(output[0]?.content).toEqual([ { type: "text", text: "The agent cannot read this message." }, ]); - expect(output[0]).not.toHaveProperty("__openclaw"); + expect(output[0]).toHaveProperty("__openclaw.beforeAgentRunBlocked"); expect(JSON.stringify(output)).not.toContain("secret prompt"); expect(input[0]).toHaveProperty("__openclaw"); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 753a3d5516d..5b4b38fb006 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -492,39 +492,13 @@ function summarizeSessionContext(messages: AgentMessage[]): { export function normalizeMessagesForLlmBoundary(messages: AgentMessage[]): AgentMessage[] { const normalized = stripToolResultDetails(normalizeAssistantReplayContent(messages)); - return stripBlockedOriginalContentFromMessages( - stripHistoricalRuntimeContextCustomMessages(normalized), - ); + return stripHistoricalRuntimeContextCustomMessages(normalized); } function cloneHookMessages(messages: AgentMessage[]): AgentMessage[] { return messages.map((message) => structuredClone(message)); } -function stripBlockedOriginalContentFromMessages(messages: AgentMessage[]): AgentMessage[] { - return messages.map(stripBlockedOriginalContentFromMessage); -} - -function stripBlockedOriginalContentFromMessage(message: AgentMessage): AgentMessage { - const record = message as AgentMessage & { __openclaw?: unknown }; - const meta = - record.__openclaw && typeof record.__openclaw === "object" && !Array.isArray(record.__openclaw) - ? (record.__openclaw as Record) - : undefined; - if (!meta || !Object.hasOwn(meta, "originalBlockedContent")) { - return message; - } - const { originalBlockedContent: _originalBlockedContent, ...remainingMeta } = meta; - const { __openclaw: _openclaw, ...remainingMessage } = record; - if (Object.keys(remainingMeta).length === 0) { - return remainingMessage as AgentMessage; - } - return { - ...remainingMessage, - __openclaw: remainingMeta, - } as unknown as AgentMessage; -} - function sessionMessagesContainIdempotencyKey( messages: AgentMessage[], idempotencyKey: string, @@ -2829,18 +2803,13 @@ export async function runEmbeddedAttempt( return true; } const nowMs = Date.now(); - const originalBlockedContent = - blockedTranscriptPrompt.length > 0 - ? [{ type: "text" as const, text: blockedTranscriptPrompt }] - : []; const redactedUserMessage = { role: "user" as const, content: [{ type: "text" as const, text: block.message }], timestamp: nowMs, idempotencyKey, __openclaw: { - originalBlockedContent: { - content: originalBlockedContent, + beforeAgentRunBlocked: { blockedBy: block.pluginId, reason: block.reason, blockedAt: nowMs, @@ -3352,9 +3321,7 @@ export async function runEmbeddedAttempt( ); } } - messagesSnapshot = stripBlockedOriginalContentFromMessages( - snapshotSelection.messagesSnapshot, - ); + messagesSnapshot = snapshotSelection.messagesSnapshot; sessionIdUsed = snapshotSelection.sessionIdUsed; lastAssistant = messagesSnapshot diff --git a/src/gateway/chat-display-projection.ts b/src/gateway/chat-display-projection.ts index cfcb62a5ac0..9b710d3bb79 100644 --- a/src/gateway/chat-display-projection.ts +++ b/src/gateway/chat-display-projection.ts @@ -347,14 +347,6 @@ function sanitizeChatHistoryMessage( } } - if ("__openclaw" in entry) { - const sanitized = sanitizeBlockedOriginalContentMeta(entry.__openclaw, maxChars); - if (sanitized.changed) { - entry.__openclaw = sanitized.meta; - changed = true; - } - } - return { message: changed ? entry : message, changed }; } diff --git a/src/gateway/protocol/schema/logs-chat.ts b/src/gateway/protocol/schema/logs-chat.ts index 301199c2baa..01468e0c230 100644 --- a/src/gateway/protocol/schema/logs-chat.ts +++ b/src/gateway/protocol/schema/logs-chat.ts @@ -28,7 +28,6 @@ export const ChatHistoryParamsSchema = Type.Object( sessionKey: NonEmptyString, limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })), maxChars: Type.Optional(Type.Integer({ minimum: 1, maximum: 500_000 })), - includeBlockedOriginalContent: Type.Optional(Type.Boolean()), }, { additionalProperties: false }, ); diff --git a/src/gateway/server-methods/chat.directive-tags.test.ts b/src/gateway/server-methods/chat.directive-tags.test.ts index 7c9e47a63ec..6ee624d66be 100644 --- a/src/gateway/server-methods/chat.directive-tags.test.ts +++ b/src/gateway/server-methods/chat.directive-tags.test.ts @@ -570,137 +570,6 @@ describe("chat directive tag stripping for non-streaming final payloads", () => mockState.dispatchBlockedByBeforeAgentRun = false; }); - it("includes blocked original content for scoped chat history callers", async () => { - createTranscriptFixture("openclaw-chat-history-blocked-original-"); - fs.writeFileSync( - mockState.transcriptPath, - [ - { - type: "session", - version: CURRENT_SESSION_VERSION, - id: mockState.sessionId, - timestamp: new Date(0).toISOString(), - cwd: "/tmp", - }, - { - type: "message", - id: "blocked-1", - parentId: null, - message: { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - timestamp: 1, - }, - originalBlockedContent: { - content: [{ type: "text", text: "secret blocked prompt" }], - blockedBy: "policy-plugin", - reason: "blocked by policy", - blockedAt: 1, - }, - }, - ] - .map((line) => JSON.stringify(line)) - .join("\n") + "\n", - "utf-8", - ); - - const scoped = await runChatHistory({ - client: createScopedCliClient(["operator.admin"]), - requestParams: { includeBlockedOriginalContent: true }, - }); - expect( - ( - scoped.messages?.[0] as { - __openclaw?: { originalBlockedContent?: { content?: Array<{ text?: string }> } }; - } - )?.__openclaw?.originalBlockedContent?.content?.[0]?.text, - ).toBe("secret blocked prompt"); - - const sensitiveScoped = await runChatHistory({ - client: createScopedCliClient(["operator.talk.secrets"]), - requestParams: { includeBlockedOriginalContent: true }, - }); - expect( - ( - sensitiveScoped.messages?.[0] as { - __openclaw?: { originalBlockedContent?: { content?: Array<{ text?: string }> } }; - } - )?.__openclaw?.originalBlockedContent?.content?.[0]?.text, - ).toBe("secret blocked prompt"); - - const writeScoped = await runChatHistory({ - client: createScopedCliClient(["operator.write"]), - requestParams: { includeBlockedOriginalContent: true }, - }); - expect( - ( - writeScoped.messages?.[0] as { - __openclaw?: { originalBlockedContent?: unknown }; - } - )?.__openclaw?.originalBlockedContent, - ).toBeUndefined(); - - const unscoped = await runChatHistory({ - client: createScopedCliClient(["operator.read"]), - requestParams: { includeBlockedOriginalContent: true }, - }); - expect( - ( - unscoped.messages?.[0] as { - __openclaw?: { originalBlockedContent?: unknown }; - } - )?.__openclaw?.originalBlockedContent, - ).toBeUndefined(); - }); - - it("applies chat history text caps to blocked original content", async () => { - createTranscriptFixture("openclaw-chat-history-blocked-original-maxchars-"); - fs.writeFileSync( - mockState.transcriptPath, - [ - { - type: "session", - version: CURRENT_SESSION_VERSION, - id: mockState.sessionId, - timestamp: new Date(0).toISOString(), - cwd: "/tmp", - }, - { - type: "message", - id: "blocked-1", - parentId: null, - message: { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - timestamp: 1, - }, - originalBlockedContent: { - content: [{ type: "text", text: "secret ".repeat(20) }], - blockedBy: "policy-plugin", - reason: "blocked by policy", - blockedAt: 1, - }, - }, - ] - .map((line) => JSON.stringify(line)) - .join("\n") + "\n", - "utf-8", - ); - - const scoped = await runChatHistory({ - client: createScopedCliClient(["operator.admin"]), - requestParams: { includeBlockedOriginalContent: true, maxChars: 24 }, - }); - - expect( - ( - scoped.messages?.[0] as { - __openclaw?: { originalBlockedContent?: { content?: Array<{ text?: string }> } }; - } - )?.__openclaw?.originalBlockedContent?.content?.[0]?.text, - ).toBe("secret secret secret sec\n...(truncated)..."); - }); - it("registers tool-event recipients for clients advertising tool-events capability", async () => { createTranscriptFixture("openclaw-chat-send-tool-events-"); mockState.finalText = "ok"; @@ -2277,51 +2146,6 @@ describe("chat directive tag stripping for non-streaming final payloads", () => }); }); - it("does not emit raw user transcript content after before_agent_run blocks", async () => { - createTranscriptFixture("openclaw-chat-send-user-transcript-blocked-gate-"); - mockState.finalText = "The agent cannot read this message."; - mockState.triggerAgentRunStart = true; - mockState.hasBeforeAgentRunHooks = true; - mockState.onAfterAgentRunStart = () => { - fs.appendFileSync( - mockState.transcriptPath, - `${JSON.stringify({ - type: "message", - message: { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - idempotencyKey: "hook-block:before_agent_run:user:idem-user-transcript-blocked-gate", - }, - originalBlockedContent: { - content: [{ type: "text", text: "secret prompt that was blocked" }], - blockedBy: "policy-plugin", - reason: "contains protected content", - blockedAt: 1, - }, - })}\n`, - "utf-8", - ); - }; - const respond = vi.fn(); - const context = createChatContext(); - - await runNonStreamingChatSend({ - context, - respond, - idempotencyKey: "idem-user-transcript-blocked-gate", - message: "secret prompt that was blocked", - expectBroadcast: false, - }); - - const userUpdates = mockState.emittedTranscriptUpdates.filter( - (update) => - typeof update.message === "object" && - update.message !== null && - (update.message as { role?: unknown }).role === "user", - ); - expect(userUpdates).toHaveLength(0); - }); - it("does not emit raw user transcript content when before_agent_run blocks without a persisted marker", async () => { createTranscriptFixture("openclaw-chat-send-user-transcript-blocked-live-signal-"); mockState.finalText = "The agent cannot read this message."; diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index af6aa34860a..4f936bfdb34 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -761,11 +761,6 @@ function canInjectSystemProvenance(client: GatewayRequestHandlerOptions["client" return scopes.includes(ADMIN_SCOPE); } -function canIncludeBlockedOriginalContent(client: GatewayRequestHandlerOptions["client"]): boolean { - const scopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : []; - return scopes.includes(ADMIN_SCOPE) || scopes.includes(TALK_SECRETS_SCOPE); -} - async function persistChatSendImages(params: { images: ChatImageContent[]; imageOrder: PromptImageOrderEntry[]; @@ -1731,11 +1726,10 @@ export const chatHandlers: GatewayRequestHandlers = { ); return; } - const { sessionKey, limit, maxChars, includeBlockedOriginalContent } = params as { + const { sessionKey, limit, maxChars } = params as { sessionKey: string; limit?: number; maxChars?: number; - includeBlockedOriginalContent?: boolean; }; const { cfg, storePath, entry } = loadSessionEntry(sessionKey); const sessionId = entry?.sessionId; @@ -1751,8 +1745,6 @@ export const chatHandlers: GatewayRequestHandlers = { ? await readRecentSessionMessagesAsync(sessionId, storePath, entry?.sessionFile, { maxMessages: max, maxBytes: Math.max(maxHistoryBytes * 2, 1024 * 1024), - includeBlockedOriginalContent: - includeBlockedOriginalContent === true && canIncludeBlockedOriginalContent(client), }) : []; const rawMessages = augmentChatHistoryWithCliSessionImports({ diff --git a/src/gateway/server-methods/server-methods.test.ts b/src/gateway/server-methods/server-methods.test.ts index cb79eae8d1d..29435f94769 100644 --- a/src/gateway/server-methods/server-methods.test.ts +++ b/src/gateway/server-methods/server-methods.test.ts @@ -455,31 +455,6 @@ describe("sanitizeChatHistoryMessages", () => { }, ]); }); - - it("truncates blocked original sidecar content with the chat history text cap", () => { - const result = sanitizeChatHistoryMessages( - [ - { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret ".repeat(20) }], - }, - }, - }, - ], - 24, - ); - - expect( - ( - result[0] as { - __openclaw?: { originalBlockedContent?: { content?: Array<{ text?: string }> } }; - } - ).__openclaw?.originalBlockedContent?.content?.[0]?.text, - ).toBe("secret secret secret sec\n...(truncated)..."); - }); }); describe("projectRecentChatDisplayMessages", () => { diff --git a/src/gateway/server-session-events.ts b/src/gateway/server-session-events.ts index 79e996e72cb..8baccf50699 100644 --- a/src/gateway/server-session-events.ts +++ b/src/gateway/server-session-events.ts @@ -12,7 +12,6 @@ import { loadGatewaySessionRow, loadSessionEntry, readSessionMessageCountAsync, - stripBlockedOriginalContentMeta, type GatewaySessionRow, } from "./session-utils.js"; @@ -127,7 +126,7 @@ async function handleTranscriptUpdateBroadcast( sessionRow: loadGatewaySessionRow(sessionKey, { transcriptUsageMaxBytes: 64 * 1024 }), includeSession: true, }); - const rawMessage = attachOpenClawTranscriptMeta(stripBlockedOriginalContentMeta(update.message), { + const rawMessage = attachOpenClawTranscriptMeta(update.message, { ...(typeof update.messageId === "string" ? { id: update.messageId } : {}), ...(typeof messageSeq === "number" ? { seq: messageSeq } : {}), }); diff --git a/src/gateway/session-history-state.test.ts b/src/gateway/session-history-state.test.ts index b80f5393e82..4847b8cb0f7 100644 --- a/src/gateway/session-history-state.test.ts +++ b/src/gateway/session-history-state.test.ts @@ -288,64 +288,4 @@ describe("SessionHistorySseState", () => { ).toBeNull(); expect(state.snapshot().messages).toHaveLength(1); }); - - test("strips blocked original content from inline SSE messages", () => { - const state = SessionHistorySseState.fromRawSnapshot({ - target: { sessionId: "sess-main" }, - rawMessages: [], - }); - - const appended = state.appendInlineMessage({ - message: { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret blocked prompt" }], - }, - }, - }, - messageId: "blocked-1", - }); - - expect( - ( - appended?.message as { - __openclaw?: { originalBlockedContent?: unknown }; - } - ).__openclaw?.originalBlockedContent, - ).toBeUndefined(); - expect(state.snapshot().messages[0]?.content).toEqual([ - { type: "text", text: "The agent cannot read this message." }, - ]); - }); - - test("keeps blocked original content for authorized inline SSE messages", () => { - const state = SessionHistorySseState.fromRawSnapshot({ - target: { sessionId: "sess-main" }, - rawMessages: [], - includeBlockedOriginalContent: true, - }); - - const appended = state.appendInlineMessage({ - message: { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret blocked prompt" }], - }, - }, - }, - messageId: "blocked-1", - }); - - expect( - ( - appended?.message as { - __openclaw?: { originalBlockedContent?: { content?: Array<{ text?: string }> } }; - } - ).__openclaw?.originalBlockedContent?.content?.[0]?.text, - ).toBe("secret blocked prompt"); - }); }); diff --git a/src/gateway/session-history-state.ts b/src/gateway/session-history-state.ts index 10e1f62f44e..6a9c070288d 100644 --- a/src/gateway/session-history-state.ts +++ b/src/gateway/session-history-state.ts @@ -6,7 +6,6 @@ import { attachOpenClawTranscriptMeta, readRecentSessionMessagesWithStatsAsync, readSessionMessagesAsync, - stripBlockedOriginalContentMeta, } from "./session-utils.js"; type SessionHistoryTranscriptMeta = { @@ -157,7 +156,6 @@ export class SessionHistorySseState { private readonly maxChars: number; private readonly limit: number | undefined; private readonly cursor: string | undefined; - private readonly includeBlockedOriginalContent: boolean; private sentHistory: PaginatedSessionHistory; private rawTranscriptSeq: number; @@ -169,14 +167,12 @@ export class SessionHistorySseState { maxChars?: number; limit?: number; cursor?: string; - includeBlockedOriginalContent?: boolean; }): SessionHistorySseState { return new SessionHistorySseState({ target: params.target, maxChars: params.maxChars, limit: params.limit, cursor: params.cursor, - includeBlockedOriginalContent: params.includeBlockedOriginalContent, initialRawMessages: params.rawMessages, rawTranscriptSeq: params.rawTranscriptSeq, totalRawMessages: params.totalRawMessages, @@ -188,7 +184,6 @@ export class SessionHistorySseState { maxChars?: number; limit?: number; cursor?: string; - includeBlockedOriginalContent?: boolean; initialRawMessages: unknown[]; rawTranscriptSeq?: number; totalRawMessages?: number; @@ -197,7 +192,6 @@ export class SessionHistorySseState { this.maxChars = params.maxChars ?? DEFAULT_CHAT_HISTORY_TEXT_MAX_CHARS; this.limit = params.limit; this.cursor = params.cursor; - this.includeBlockedOriginalContent = params.includeBlockedOriginalContent === true; const rawSnapshot = { rawMessages: params.initialRawMessages, ...(typeof params.rawTranscriptSeq === "number" @@ -235,10 +229,7 @@ export class SessionHistorySseState { return null; } this.rawTranscriptSeq += 1; - const projectedMessage = this.includeBlockedOriginalContent - ? update.message - : stripBlockedOriginalContentMeta(update.message); - const nextMessage = attachOpenClawTranscriptMeta(projectedMessage, { + const nextMessage = attachOpenClawTranscriptMeta(update.message, { ...(typeof update.messageId === "string" ? { id: update.messageId } : {}), seq: this.rawTranscriptSeq, }); @@ -286,7 +277,6 @@ export class SessionHistorySseState { this.target.sessionFile, { ...resolveSessionHistoryTailReadOptions(this.limit), - includeBlockedOriginalContent: this.includeBlockedOriginalContent, }, ); return { @@ -303,7 +293,6 @@ export class SessionHistorySseState { { mode: "full", reason: "session history cursor pagination", - includeBlockedOriginalContent: this.includeBlockedOriginalContent, }, ), }; diff --git a/src/gateway/session-message-events.test.ts b/src/gateway/session-message-events.test.ts index 7f122ae7dfc..ec137709e69 100644 --- a/src/gateway/session-message-events.test.ts +++ b/src/gateway/session-message-events.test.ts @@ -314,20 +314,18 @@ describe("session.message websocket events", () => { role: "user", content: [{ type: "text", text: "The agent cannot read this message." }], __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret blocked prompt" }], - }, + beforeAgentRunBlocked: { blockedBy: "policy-plugin", reason: "blocked", blockedAt: 1 }, }, }, }); const payload = messageEvent.payload as { - message?: { content?: unknown; __openclaw?: { originalBlockedContent?: unknown } }; + message?: { content?: unknown; __openclaw?: { beforeAgentRunBlocked?: unknown } }; }; expect(payload.message?.content).toEqual([ { type: "text", text: "The agent cannot read this message." }, ]); - expect(payload.message?.__openclaw?.originalBlockedContent).toBeUndefined(); + expect(JSON.stringify(payload.message)).not.toContain("secret blocked prompt"); }); }); @@ -353,8 +351,7 @@ describe("session.message websocket events", () => { role: "user", content: [{ type: "text", text: "The agent cannot read this message." }], __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret blocked prompt" }], + beforeAgentRunBlocked: { blockedBy: "policy-plugin", reason: "contains protected content", blockedAt: Date.now(), @@ -368,14 +365,14 @@ describe("session.message websocket events", () => { message?: { role?: unknown; content?: unknown; - __openclaw?: { originalBlockedContent?: unknown }; + __openclaw?: { beforeAgentRunBlocked?: unknown }; }; }; expect(payload.message?.role).toBe("user"); expect(payload.message?.content).toEqual([ { type: "text", text: "The agent cannot read this message." }, ]); - expect(payload.message?.__openclaw?.originalBlockedContent).toBeUndefined(); + expect(JSON.stringify(payload.message)).not.toContain("secret blocked prompt"); }); }); diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts index 4e0c62e4e44..bcc62b4c4cb 100644 --- a/src/gateway/session-utils.fs.test.ts +++ b/src/gateway/session-utils.fs.test.ts @@ -92,7 +92,7 @@ function appendBlockedUserMessageWithSessionManager(params: { timestamp: Date.now(), ...(params.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : {}), __openclaw: { - originalBlockedContent: { + beforeAgentRunBlocked: { content: params.originalText ? [{ type: "text", text: params.originalText }] : [], blockedBy: params.pluginId, reason: params.reason, @@ -1305,9 +1305,7 @@ describe("readSessionMessages", () => { }); expect(messageId).toBeTruthy(); - const out = readSessionMessages(sessionId, storePath, sessionFile, { - includeBlockedOriginalContent: true, - }); + const out = readSessionMessages(sessionId, storePath, sessionFile, {}); expect( out.map((message) => ({ role: (message as { role?: string }).role, @@ -1319,8 +1317,8 @@ describe("readSessionMessages", () => { { role: "user", text: [{ type: "text", text: "Blocked by HITL test hook." }] }, ]); expect( - (out[2] as { __openclaw?: { originalBlockedContent?: { content?: unknown } } }).__openclaw - ?.originalBlockedContent?.content, + (out[2] as { __openclaw?: { beforeAgentRunBlocked?: { content?: unknown } } }).__openclaw + ?.beforeAgentRunBlocked?.content, ).toEqual([{ type: "text", text: "[hitl:block] hello" }]); }); @@ -1359,17 +1357,15 @@ describe("readSessionMessages", () => { reason: "blocked by test policy", }); - const out = readSessionMessages(sessionId, storePath, sessionFile, { - includeBlockedOriginalContent: true, - }); + const out = readSessionMessages(sessionId, storePath, sessionFile, {}); expect( out.map((message) => ({ role: (message as { role?: string }).role, original: ( message as { - __openclaw?: { originalBlockedContent?: { content?: Array<{ text?: string }> } }; + __openclaw?: { beforeAgentRunBlocked?: { content?: Array<{ text?: string }> } }; } - ).__openclaw?.originalBlockedContent?.content?.[0]?.text, + ).__openclaw?.beforeAgentRunBlocked?.content?.[0]?.text, })), ).toEqual([ { role: "user", original: "[hitl:block] first" }, diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts index 632dab7a502..e7344ebe78b 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -139,28 +139,7 @@ export function attachOpenClawTranscriptMeta( }; } -export function stripBlockedOriginalContentMeta(message: unknown): unknown { - if (!message || typeof message !== "object" || Array.isArray(message)) { - return message; - } - const record = message as Record; - const existing = - record.__openclaw && typeof record.__openclaw === "object" && !Array.isArray(record.__openclaw) - ? (record.__openclaw as Record) - : null; - if (!existing || !("originalBlockedContent" in existing)) { - return message; - } - const { originalBlockedContent: _originalBlockedContent, ...remainingMeta } = existing; - return { - ...record, - __openclaw: remainingMeta, - }; -} - -type SessionMessageProjectionOptions = { - includeBlockedOriginalContent?: boolean; -}; +type SessionMessageProjectionOptions = Record; export function readSessionMessages( sessionId: string, @@ -605,7 +584,7 @@ export async function visitSessionMessagesAsync( storePath: string | undefined, sessionFile: string | undefined, visit: (message: unknown, seq: number) => void, - opts: { mode: "full"; reason: string; includeBlockedOriginalContent?: boolean }, + opts: { mode: "full"; reason: string }, ): Promise { const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); if (!filePath) { @@ -759,36 +738,9 @@ function parsedSessionEntryToMessage( } const entry = parsed as Record; if (entry.message) { - const messageRecord = - entry.message && typeof entry.message === "object" && !Array.isArray(entry.message) - ? (entry.message as Record) - : undefined; - const messageOpenClaw = - messageRecord?.__openclaw && - typeof messageRecord.__openclaw === "object" && - !Array.isArray(messageRecord.__openclaw) - ? (messageRecord.__openclaw as Record) - : undefined; - const originalBlockedContent = - opts?.includeBlockedOriginalContent === true && - !messageOpenClaw?.originalBlockedContent && - entry.originalBlockedContent && - typeof entry.originalBlockedContent === "object" && - !Array.isArray(entry.originalBlockedContent) - ? { - originalBlockedContent: { - content: (entry.originalBlockedContent as { content?: unknown }).content, - }, - } - : {}; - const projectedMessage = - opts?.includeBlockedOriginalContent === true - ? entry.message - : stripBlockedOriginalContentMeta(entry.message); - return attachOpenClawTranscriptMeta(projectedMessage, { + return attachOpenClawTranscriptMeta(entry.message, { ...(typeof entry.id === "string" ? { id: entry.id } : {}), seq, - ...originalBlockedContent, }); } diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 4713b42e111..be80b252324 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -118,7 +118,6 @@ export { readSessionTitleFieldsFromTranscriptAsync, readSessionPreviewItemsFromTranscript, readSessionMessagesAsync, - stripBlockedOriginalContentMeta, visitSessionMessagesAsync, resolveSessionTranscriptCandidates, } from "./session-utils.fs.js"; diff --git a/src/gateway/sessions-history-http.revocation.test.ts b/src/gateway/sessions-history-http.revocation.test.ts index 8d74a491027..a3a829ab7e5 100644 --- a/src/gateway/sessions-history-http.revocation.test.ts +++ b/src/gateway/sessions-history-http.revocation.test.ts @@ -106,28 +106,21 @@ vi.mock("./session-history-state.js", () => ({ history: { items: [], nextCursor: null, messages: [] }, }), SessionHistorySseState: { - fromRawSnapshot: (params: { includeBlockedOriginalContent?: boolean }) => ({ + fromRawSnapshot: (_params: unknown) => ({ snapshot: () => ({ items: [], nextCursor: null, messages: [] }), appendInlineMessage: ({ message, messageId }: { message: unknown; messageId?: string }) => ({ - message: - params.includeBlockedOriginalContent || !message || typeof message !== "object" - ? message - : (() => { - const clone = { ...(message as Record) }; - delete clone.__openclaw; - return clone; - })(), + message, messageSeq: 1, messageId, }), refreshAsync: async () => ({ items: [ - params.includeBlockedOriginalContent + false ? { role: "user", content: [{ type: "text", text: "The agent cannot read this message." }], __openclaw: { - originalBlockedContent: { + beforeAgentRunBlocked: { content: [{ type: "text", text: "secret blocked prompt" }], }, }, @@ -139,12 +132,12 @@ vi.mock("./session-history-state.js", () => ({ ], nextCursor: null, messages: [ - params.includeBlockedOriginalContent + false ? { role: "user", content: [{ type: "text", text: "The agent cannot read this message." }], __openclaw: { - originalBlockedContent: { + beforeAgentRunBlocked: { content: [{ type: "text", text: "secret blocked prompt" }], }, }, @@ -252,112 +245,6 @@ describe("session history SSE auth revocation", () => { expect(res.writableEnded).toBe(true); }); - it("closes original-content streams when admin scope is downgraded", async () => { - currentScopes = ["operator.read", "operator.admin"]; - const req = new MockReq("/sessions/agent%3Amain/history?includeBlockedOriginalContent=true"); - const res = new MockRes(); - - const handled = await handleSessionHistoryHttpRequest( - req as unknown as IncomingMessage, - res as unknown as ServerResponse, - { auth: { mode: "trusted-proxy" } as never }, - ); - - expect(handled).toBe(true); - expect(transcriptUpdateHandler).toBeTypeOf("function"); - - currentScopes = ["operator.read"]; - - transcriptUpdateHandler?.({ - sessionFile: "/tmp/session-1.jsonl", - message: { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret blocked prompt" }], - }, - }, - }, - messageId: "blocked-1", - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - - const joined = res.writes.join(""); - expect(joined).not.toContain("event: message"); - expect(joined).not.toContain("secret blocked prompt"); - expect(res.writableEnded).toBe(true); - }); - - it("refreshes authorized SSE history for redacted blocked update originals", async () => { - currentScopes = ["operator.read", "operator.talk.secrets"]; - const req = new MockReq("/sessions/agent%3Amain/history?includeBlockedOriginalContent=true"); - const res = new MockRes(); - - const handled = await handleSessionHistoryHttpRequest( - req as unknown as IncomingMessage, - res as unknown as ServerResponse, - { auth: { mode: "trusted-proxy" } as never }, - ); - - expect(handled).toBe(true); - expect(transcriptUpdateHandler).toBeTypeOf("function"); - - transcriptUpdateHandler?.({ - sessionFile: "/tmp/session-1.jsonl", - message: { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - }, - messageId: "blocked-1", - forceHistoryRefresh: true, - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - - const joined = res.writes.join(""); - expect(joined).toContain("event: history"); - expect(joined).toContain("secret blocked prompt"); - expect(res.writableEnded).toBe(false); - }); - - it("strips blocked originals on unscoped live inline SSE updates", async () => { - currentScopes = ["operator.read"]; - const req = new MockReq("/sessions/agent%3Amain/history"); - const res = new MockRes(); - - const handled = await handleSessionHistoryHttpRequest( - req as unknown as IncomingMessage, - res as unknown as ServerResponse, - { auth: { mode: "trusted-proxy" } as never }, - ); - - expect(handled).toBe(true); - expect(transcriptUpdateHandler).toBeTypeOf("function"); - - transcriptUpdateHandler?.({ - sessionFile: "/tmp/session-1.jsonl", - message: { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret blocked prompt" }], - }, - }, - }, - messageId: "blocked-1", - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - - const joined = res.writes.join(""); - expect(joined).toContain("event: message"); - expect(joined).not.toContain("secret blocked prompt"); - expect(res.writableEnded).toBe(false); - }); - it("rechecks SSE auth against live proxy config instead of startup fallbacks", async () => { const req = new MockReq("/sessions/agent%3Amain/history"); const res = new MockRes(); diff --git a/src/gateway/sessions-history-http.ts b/src/gateway/sessions-history-http.ts index 0805048d347..d32d11186a6 100644 --- a/src/gateway/sessions-history-http.ts +++ b/src/gateway/sessions-history-http.ts @@ -80,18 +80,6 @@ function resolveLimit(req: IncomingMessage): number | undefined { return Math.min(MAX_SESSION_HISTORY_LIMIT, Math.max(1, value)); } -function shouldIncludeBlockedOriginalContent( - req: IncomingMessage, - requestAuth: Parameters[1], -): boolean { - const raw = getRequestUrl(req).searchParams.get("includeBlockedOriginalContent"); - if (raw !== "1" && raw !== "true") { - return false; - } - const scopes = resolveTrustedHttpOperatorScopes(req, requestAuth); - return scopes.includes(ADMIN_SCOPE) || scopes.includes(TALK_SECRETS_SCOPE); -} - function canonicalizePath(value: string | undefined): string | undefined { const trimmed = normalizeOptionalString(value); if (!trimmed) { @@ -149,8 +137,7 @@ export async function handleSessionHistoryHttpRequest( if (!authResult) { return true; } - const { cfg, requestAuth } = authResult; - const includeBlockedOriginalContent = shouldIncludeBlockedOriginalContent(req, requestAuth); + const { cfg } = authResult; const target = resolveGatewaySessionStoreTarget({ cfg, key: sessionKey }); const store = loadSessionStore(target.storePath); @@ -179,7 +166,6 @@ export async function handleSessionHistoryHttpRequest( entry.sessionFile, { ...resolveSessionHistoryTailReadOptions(limit), - includeBlockedOriginalContent, }, ) : undefined; @@ -191,7 +177,6 @@ export async function handleSessionHistoryHttpRequest( ? await readSessionMessagesAsync(entry.sessionId, target.storePath, entry.sessionFile, { mode: "full", reason: "session history cursor pagination", - includeBlockedOriginalContent, }) : []); const historySnapshot = buildSessionHistorySnapshot({ @@ -238,7 +223,6 @@ export async function handleSessionHistoryHttpRequest( maxChars: effectiveMaxChars, limit, cursor, - includeBlockedOriginalContent, }); sentHistory = sseState.snapshot(); setSseHeaders(res); @@ -308,13 +292,6 @@ export async function handleSessionHistoryHttpRequest( return false; } const requestedScopes = resolveTrustedHttpOperatorScopes(req, currentRequestAuth.requestAuth); - if ( - includeBlockedOriginalContent && - !requestedScopes.includes(ADMIN_SCOPE) && - !requestedScopes.includes(TALK_SECRETS_SCOPE) - ) { - return false; - } return authorizeOperatorScopesForMethod("chat.history", requestedScopes).allowed; }; diff --git a/ui/src/ui/chat/grouped-render.test.ts b/ui/src/ui/chat/grouped-render.test.ts index aa2da92d5bb..45d4b86fa4f 100644 --- a/ui/src/ui/chat/grouped-render.test.ts +++ b/ui/src/ui/chat/grouped-render.test.ts @@ -664,11 +664,7 @@ describe("grouped chat rendering", () => { { role: "user", content: [{ type: "text", text: "The agent cannot read this message." }], - __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret blocked prompt" }], - }, - }, + __openclaw: {}, timestamp: 1000, }, "user", diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index 951291ffafd..b4896a82fe6 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -1433,8 +1433,7 @@ function renderGroupedMessage( const markdownBase = extractedText?.trim() ? extractedText : null; const reasoningMarkdown = extractedThinking ? formatReasoningMarkdown(extractedThinking) : null; const markdown = markdownBase; - const isBlockedUserMessage = - normalizedRole === "user" && normalizedMessage.isBlockedOriginalContent === true; + const isBlockedUserMessage = false; const canCopyMarkdown = role === "assistant" && Boolean(markdown?.trim()); const canExpand = role === "assistant" && Boolean(onOpenSidebar && markdown?.trim()); diff --git a/ui/src/ui/chat/message-extract.ts b/ui/src/ui/chat/message-extract.ts index d14d3c1fd23..a233a0d7c6e 100644 --- a/ui/src/ui/chat/message-extract.ts +++ b/ui/src/ui/chat/message-extract.ts @@ -22,30 +22,6 @@ function processMessageText(text: string, role: string): string { export function extractText(message: unknown): string | null { const m = message as Record; const role = typeof m.role === "string" ? m.role : ""; - // Sidecar `__openclaw.originalBlockedContent` is set on user messages - // that were blocked by `before_agent_run` (or any pre-LLM hook). The - // agent transcript only ever sees the redacted stub in `message.content`, - // but the SPA renders the human's original input from this sidecar so - // the user can see what they typed. NOTE: this is for HUMAN display only; - // never inject this back into agent context. - if (role === "user") { - const meta = m.__openclaw as Record | undefined; - const originalBlocked = meta?.originalBlockedContent as { content?: unknown } | undefined; - if (originalBlocked && Array.isArray(originalBlocked.content)) { - const parts = originalBlocked.content - .map((p) => { - const item = p as Record; - if (item.type === "text" && typeof item.text === "string") { - return item.text; - } - return null; - }) - .filter((v): v is string => typeof v === "string"); - if (parts.length > 0) { - return processMessageText(parts.join("\n"), role); - } - } - } const raw = role === "assistant" ? extractSharedAssistantVisibleText(message) : extractRawText(message); if (!raw) { diff --git a/ui/src/ui/chat/message-normalizer.test.ts b/ui/src/ui/chat/message-normalizer.test.ts index e75b04bdb49..d41bbe1a0ba 100644 --- a/ui/src/ui/chat/message-normalizer.test.ts +++ b/ui/src/ui/chat/message-normalizer.test.ts @@ -42,29 +42,6 @@ describe("message-normalizer", () => { expect(result.audioAsVoice).toBeUndefined(); }); - it("renders blocked originals without mutating the raw redacted message", () => { - const rawMessage = { - role: "user", - content: [{ type: "text", text: "The agent cannot read this message." }], - __openclaw: { - originalBlockedContent: { - content: [{ type: "text", text: "secret blocked prompt" }], - }, - }, - }; - - const result = normalizeMessage(rawMessage); - - expect(result).toMatchObject({ - role: "user", - content: [{ type: "text", text: "secret blocked prompt" }], - isBlockedOriginalContent: true, - }); - expect(rawMessage.content).toEqual([ - { type: "text", text: "The agent cannot read this message." }, - ]); - }); - it("normalizes message with array content", () => { const result = normalizeMessage({ role: "assistant", diff --git a/ui/src/ui/chat/message-normalizer.ts b/ui/src/ui/chat/message-normalizer.ts index 1057671da16..618dc16e15d 100644 --- a/ui/src/ui/chat/message-normalizer.ts +++ b/ui/src/ui/chat/message-normalizer.ts @@ -240,17 +240,7 @@ function expandTextContent(text: string): { export function normalizeMessage(message: unknown): NormalizedMessage { const m = message as Record; let role = typeof m.role === "string" ? m.role : "unknown"; - let isBlockedOriginalContent = false; - let contentRaw = m.content; - - if (role === "user") { - const oc = m.__openclaw as Record | undefined; - const obc = oc?.originalBlockedContent as { content?: unknown } | undefined; - if (obc && Array.isArray(obc.content) && obc.content.length > 0) { - contentRaw = obc.content; - isBlockedOriginalContent = true; - } - } + const contentRaw = m.content; // Detect tool messages by common gateway shapes. // Some tool events come through as assistant role with tool_* items in the content array. @@ -396,7 +386,6 @@ export function normalizeMessage(message: unknown): NormalizedMessage { timestamp, id, senderLabel, - ...(isBlockedOriginalContent ? { isBlockedOriginalContent: true } : {}), ...(audioAsVoice ? { audioAsVoice: true } : {}), ...(replyTarget ? { replyTarget } : {}), }; diff --git a/ui/src/ui/types/chat-types.ts b/ui/src/ui/types/chat-types.ts index f9df1825bd6..eb937635cf4 100644 --- a/ui/src/ui/types/chat-types.ts +++ b/ui/src/ui/types/chat-types.ts @@ -58,7 +58,6 @@ export type NormalizedMessage = { timestamp: number; id?: string; senderLabel?: string | null; - isBlockedOriginalContent?: boolean; audioAsVoice?: boolean; replyTarget?: | {