mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:00:50 +00:00
fix(webchat): hide heartbeat history artifacts
This commit is contained in:
@@ -77,6 +77,27 @@ describe("extractTextCached", () => {
|
||||
expect(extractText(message)).toBeNull();
|
||||
expect(extractTextCached(message)).toBeNull();
|
||||
});
|
||||
|
||||
it("strips internal runtime context blocks from user text", () => {
|
||||
const message = {
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
|
||||
"internal subagent payload",
|
||||
"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
|
||||
"",
|
||||
"visible ask",
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(extractText(message)).toBe("visible ask");
|
||||
expect(extractTextCached(message)).toBe("visible ask");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractThinkingCached", () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stripInternalRuntimeContext } from "../../../../src/agents/internal-runtime-context.js";
|
||||
import { stripInboundMetadata } from "../../../../src/auto-reply/reply/strip-inbound-meta.js";
|
||||
import { stripEnvelope } from "../../../../src/shared/chat-envelope.js";
|
||||
import { extractAssistantVisibleText as extractSharedAssistantVisibleText } from "../../../../src/shared/chat-message-content.js";
|
||||
@@ -9,12 +10,13 @@ const thinkingCache = new WeakMap<object, string | null>();
|
||||
|
||||
function processMessageText(text: string, role: string): string {
|
||||
const shouldStripInboundMetadata = normalizeLowercaseStringOrEmpty(role) === "user";
|
||||
const withoutInternalContext = stripInternalRuntimeContext(text);
|
||||
if (role === "assistant") {
|
||||
return stripThinkingTags(text);
|
||||
return stripThinkingTags(withoutInternalContext);
|
||||
}
|
||||
return shouldStripInboundMetadata
|
||||
? stripInboundMetadata(stripEnvelope(text))
|
||||
: stripEnvelope(text);
|
||||
? stripInboundMetadata(stripEnvelope(withoutInternalContext))
|
||||
: stripEnvelope(withoutInternalContext);
|
||||
}
|
||||
|
||||
export function extractText(message: unknown): string | null {
|
||||
|
||||
@@ -753,6 +753,39 @@ describe("loadChatHistory", () => {
|
||||
expect(state.lastError).toBeNull();
|
||||
});
|
||||
|
||||
it("filters heartbeat acknowledgements and internal-only user messages", async () => {
|
||||
const request = vi.fn().mockResolvedValue({
|
||||
messages: [
|
||||
{ role: "assistant", content: [{ type: "text", text: "HEARTBEAT_OK" }] },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
|
||||
"subagent completion payload",
|
||||
"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "visible answer" }] },
|
||||
],
|
||||
thinkingLevel: "low",
|
||||
});
|
||||
const state = createState({
|
||||
connected: true,
|
||||
client: { request } as unknown as ChatState["client"],
|
||||
});
|
||||
|
||||
await loadChatHistory(state);
|
||||
|
||||
expect(state.chatMessages).toEqual([
|
||||
{ role: "assistant", content: [{ type: "text", text: "visible answer" }] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows a targeted message when chat history is unauthorized", async () => {
|
||||
const request = vi.fn().mockRejectedValue(
|
||||
new GatewayRequestError({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isHeartbeatOkResponse } from "../../../../src/auto-reply/heartbeat-filter.js";
|
||||
import { resetToolStream } from "../app-tool-stream.ts";
|
||||
import { extractText } from "../chat/message-extract.ts";
|
||||
import { formatConnectError } from "../connect-error.ts";
|
||||
@@ -71,8 +72,67 @@ function isSyntheticTranscriptRepairToolResult(message: unknown): boolean {
|
||||
return typeof text === "string" && text.trim() === SYNTHETIC_TRANSCRIPT_REPAIR_RESULT;
|
||||
}
|
||||
|
||||
function isTextOnlyContent(content: unknown): boolean {
|
||||
if (typeof content === "string") {
|
||||
return true;
|
||||
}
|
||||
if (!Array.isArray(content)) {
|
||||
return false;
|
||||
}
|
||||
if (content.length === 0) {
|
||||
return true;
|
||||
}
|
||||
let sawText = false;
|
||||
for (const block of content) {
|
||||
if (!block || typeof block !== "object") {
|
||||
return false;
|
||||
}
|
||||
const entry = block as { type?: unknown; text?: unknown };
|
||||
if (entry.type !== "text") {
|
||||
return false;
|
||||
}
|
||||
sawText = true;
|
||||
if (typeof entry.text !== "string") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return sawText;
|
||||
}
|
||||
|
||||
function isEmptyUserTextOnlyMessage(message: unknown): boolean {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
}
|
||||
const entry = message as Record<string, unknown>;
|
||||
if (normalizeLowercaseStringOrEmpty(entry.role) !== "user") {
|
||||
return false;
|
||||
}
|
||||
if (!isTextOnlyContent(entry.content ?? entry.text)) {
|
||||
return false;
|
||||
}
|
||||
return (extractText(message)?.trim() ?? "") === "";
|
||||
}
|
||||
|
||||
function isAssistantHeartbeatAck(message: unknown): boolean {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
}
|
||||
const entry = message as Record<string, unknown>;
|
||||
const role = normalizeLowercaseStringOrEmpty(entry.role);
|
||||
if (role !== "assistant") {
|
||||
return false;
|
||||
}
|
||||
const content = entry.content ?? entry.text;
|
||||
return isHeartbeatOkResponse({ role, content });
|
||||
}
|
||||
|
||||
function shouldHideHistoryMessage(message: unknown): boolean {
|
||||
return isAssistantSilentReply(message) || isSyntheticTranscriptRepairToolResult(message);
|
||||
return (
|
||||
isAssistantSilentReply(message) ||
|
||||
isAssistantHeartbeatAck(message) ||
|
||||
isSyntheticTranscriptRepairToolResult(message) ||
|
||||
isEmptyUserTextOnlyMessage(message)
|
||||
);
|
||||
}
|
||||
|
||||
function isRetryableStartupUnavailable(err: unknown, method: string): err is GatewayRequestError {
|
||||
|
||||
Reference in New Issue
Block a user