Skip to content

Commit 3b99a1a

Browse files
committed
fix: filter delivery-mirror entries from consumer history paths
1 parent dc249ed commit 3b99a1a

8 files changed

Lines changed: 59 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212

1313
### Fixes
1414

15+
- Sessions/WebChat: filter OpenClaw delivery-mirror transcript artifacts from LLM context, `chat.history`, and `sessions.get` so internal delivery audit rows do not appear as duplicate assistant messages. Carries forward #40716. Thanks @kiyoakii.
1516
- NVIDIA/NIM: persist the `NVIDIA_API_KEY` provider marker and mark bundled NVIDIA Chat Completions models as string-content compatible, so NIM models load from `models.json` and OpenAI-compatible subagent calls send plain text content. Fixes #73013 and #50107; refs #73014. Thanks @bautrey, @iot2edge, @ifearghal, and @futhgar.
1617
- Channels/Discord: let text-only configs drop the `GuildVoiceStates` gateway intent and expose a bounded `/gateway/bot` metadata timeout with rate-limited fallback logs, reducing idle CPU and warning floods. Fixes #73709 and #73585. Thanks @sanchezm86 and @trac3r00.
1718
- CLI/plugins: use plugin metadata snapshots for install slot selection and add opt-in plugin lifecycle timing traces, so plugin install avoids runtime-loading the plugin registry for metadata-only decisions. Thanks @shakkernerd.

docs/gateway/protocol.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,8 @@ enumeration of `src/gateway/server-methods/*.ts`.
398398
- `sessions.abort` aborts active work for a session.
399399
- `sessions.patch` updates session metadata/overrides.
400400
- `sessions.reset`, `sessions.delete`, and `sessions.compact` perform session maintenance.
401-
- `sessions.get` returns the full stored session row.
402-
- Chat execution still uses `chat.history`, `chat.send`, `chat.abort`, and `chat.inject`. `chat.history` is display-normalized for UI clients: inline directive tags are stripped from visible text, plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks) and leaked ASCII/full-width model control tokens are stripped, pure silent-token assistant rows such as exact `NO_REPLY` / `no_reply` are omitted, and oversized rows can be replaced with placeholders.
401+
- `sessions.get` returns the stored session messages after omitting internal OpenClaw delivery-mirror transcript artifacts.
402+
- Chat execution still uses `chat.history`, `chat.send`, `chat.abort`, and `chat.inject`. `chat.history` is display-normalized for UI clients: inline directive tags are stripped from visible text, plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks) and leaked ASCII/full-width model control tokens are stripped, internal OpenClaw delivery-mirror transcript artifacts and pure silent-token assistant rows such as exact `NO_REPLY` / `no_reply` are omitted, and oversized rows can be replaced with placeholders.
403403

404404
</Accordion>
405405

docs/web/webchat.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket.
3232
payloads (including `<tool_call>...</tool_call>`,
3333
`<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`,
3434
`<function_calls>...</function_calls>`, and truncated tool-call blocks), and
35-
leaked ASCII/full-width model control tokens are stripped from visible text,
36-
and assistant entries whose whole visible text is only the exact silent
37-
token `NO_REPLY` / `no_reply` are omitted.
35+
leaked ASCII/full-width model control tokens are stripped from visible text.
36+
Internal OpenClaw delivery-mirror transcript artifacts and assistant entries
37+
whose whole visible text is only the exact silent token `NO_REPLY` /
38+
`no_reply` are omitted.
3839
- Reasoning-flagged reply payloads (`isReasoning: true`) are excluded from WebChat assistant content, transcript replay text, and audio content blocks, so thinking-only payloads do not surface as visible assistant messages or playable audio.
3940
- `chat.inject` appends an assistant note directly to the transcript and broadcasts it to the UI (no agent run).
4041
- Aborted runs can keep partial assistant output visible in the UI.

src/agents/pi-embedded-runner/tool-result-context-guard.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AgentMessage } from "@mariozechner/pi-agent-core";
2-
import { isDeliveryMirrorMessage } from "../../config/sessions/transcript.js";
2+
import { filterDeliveryMirrorMessages } from "../../config/sessions/transcript.js";
33
import type { ContextEngine, ContextEngineRuntimeContext } from "../../context-engine/types.js";
44
import {
55
CHARS_PER_TOKEN_ESTIMATE,
@@ -320,9 +320,7 @@ export function installToolResultContextGuard(params: {
320320
: messages;
321321

322322
const sourceMessages = Array.isArray(transformed) ? transformed : messages;
323-
const visibleMessages = sourceMessages.some(isDeliveryMirrorMessage)
324-
? sourceMessages.filter((msg) => !isDeliveryMirrorMessage(msg))
325-
: sourceMessages;
323+
const visibleMessages = filterDeliveryMirrorMessages(sourceMessages);
326324
const contextMessages = toolResultsNeedTruncation({
327325
messages: visibleMessages,
328326
maxSingleToolResultChars,

src/config/sessions/sessions.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { evaluateSessionFreshness, resolveSessionResetPolicy } from "./reset.js"
1818
import { resolveAndPersistSessionFile } from "./session-file.js";
1919
import { clearSessionStoreCacheForTest, loadSessionStore, updateSessionStore } from "./store.js";
2020
import { useTempSessionsFixture } from "./test-helpers.js";
21-
import { isDeliveryMirrorMessage } from "./transcript.js";
21+
import { filterDeliveryMirrorMessages, isDeliveryMirrorMessage } from "./transcript.js";
2222
import { mergeSessionEntry, mergeSessionEntryWithPolicy, type SessionEntry } from "./types.js";
2323

2424
describe("session path safety", () => {
@@ -653,3 +653,42 @@ describe("isDeliveryMirrorMessage", () => {
653653
).toBe(false);
654654
});
655655
});
656+
657+
describe("filterDeliveryMirrorMessages", () => {
658+
it("removes only OpenClaw delivery-mirror assistant messages", () => {
659+
const visibleAssistant = {
660+
role: "assistant",
661+
provider: "anthropic",
662+
model: "delivery-mirror",
663+
content: [{ type: "text", text: "visible" }],
664+
};
665+
const mirrorAssistant = {
666+
role: "assistant",
667+
provider: "openclaw",
668+
model: "delivery-mirror",
669+
content: [{ type: "text", text: "mirror" }],
670+
};
671+
const userMessage = {
672+
role: "user",
673+
content: [{ type: "text", text: "hello" }],
674+
};
675+
676+
expect(filterDeliveryMirrorMessages([userMessage, mirrorAssistant, visibleAssistant])).toEqual([
677+
userMessage,
678+
visibleAssistant,
679+
]);
680+
});
681+
682+
it("preserves array identity when there are no delivery-mirror messages", () => {
683+
const messages = [
684+
{
685+
role: "assistant",
686+
provider: "anthropic",
687+
model: "claude-3",
688+
content: [{ type: "text", text: "visible" }],
689+
},
690+
];
691+
692+
expect(filterDeliveryMirrorMessages(messages)).toBe(messages);
693+
});
694+
});

src/config/sessions/transcript.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ export function isDeliveryMirrorMessage(msg: unknown): boolean {
172172
return msg.role === "assistant" && msg.provider === "openclaw" && msg.model === "delivery-mirror";
173173
}
174174

175+
export function filterDeliveryMirrorMessages<T>(messages: T[]): T[] {
176+
return messages.some(isDeliveryMirrorMessage)
177+
? messages.filter((message) => !isDeliveryMirrorMessage(message))
178+
: messages;
179+
}
180+
175181
export async function appendAssistantMessageToSessionTranscript(params: {
176182
agentId?: string;
177183
sessionKey: string;

src/gateway/server-methods/chat.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { stageSandboxMedia } from "../../auto-reply/reply/stage-sandbox-media.js
1313
import type { MsgContext, TemplateContext } from "../../auto-reply/templating.js";
1414
import { extractCanvasFromText } from "../../chat/canvas-render.js";
1515
import { resolveSessionFilePath } from "../../config/sessions.js";
16-
import { isDeliveryMirrorMessage } from "../../config/sessions/transcript.js";
16+
import { filterDeliveryMirrorMessages } from "../../config/sessions/transcript.js";
1717
import type { OpenClawConfig } from "../../config/types.openclaw.js";
1818
import { formatErrorMessage } from "../../infra/errors.js";
1919
import { jsonUtf8Bytes } from "../../infra/json-utf8-bytes.js";
@@ -1670,7 +1670,7 @@ export const chatHandlers: GatewayRequestHandlers = {
16701670
provider: resolvedSessionModel.provider,
16711671
localMessages,
16721672
});
1673-
const visibleMessages = rawMessages.filter((msg) => !isDeliveryMirrorMessage(msg));
1673+
const visibleMessages = filterDeliveryMirrorMessages(rawMessages);
16741674
const hardMax = 1000;
16751675
const defaultLimit = 200;
16761676
const requested = typeof limit === "number" ? limit : defaultLimit;

src/gateway/server-methods/sessions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
type SessionEntry,
2020
updateSessionStore,
2121
} from "../../config/sessions.js";
22-
import { isDeliveryMirrorMessage } from "../../config/sessions/transcript.js";
22+
import { filterDeliveryMirrorMessages } from "../../config/sessions/transcript.js";
2323
import type { OpenClawConfig } from "../../config/types.openclaw.js";
2424
import {
2525
hasInternalHookListeners,
@@ -1605,7 +1605,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
16051605
return;
16061606
}
16071607
const allMessages = readSessionMessages(entry.sessionId, storePath, entry.sessionFile);
1608-
const filtered = allMessages.filter((msg) => !isDeliveryMirrorMessage(msg));
1608+
const filtered = filterDeliveryMirrorMessages(allMessages);
16091609
const messages = limit < filtered.length ? filtered.slice(-limit) : filtered;
16101610
respond(true, { messages }, undefined);
16111611
},

0 commit comments

Comments
 (0)