mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(imessage): keep fenced role-marker mapping keys out of the outbound stripper (#117159)
* fix(imessage): stop the outbound sanitizer deleting fenced role keys sanitizeOutboundText strips standalone `user:`/`system:`/`assistant:` lines (leaked turn boundaries), `#+#` separators and `assistant to=` markers, but the three regexes had no code-fence awareness. A bare YAML mapping key like `user:` on its own line inside a ```yaml block matched ROLE_TURN_MARKER_RE and was silently deleted before iMessage delivery, reparenting its children under the wrong node — still-valid YAML, wrong meaning, and nothing signalling a line was removed. Skip matches whose offset falls inside a markdown code region for all three patterns, reusing findCodeRegions/isInsideCode already applied by the sibling reflection-guard on this channel. Regions are recomputed per pass because each strip shifts later offsets. Leaked markers in prose are still stripped. Refs: #116942 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(imessage): cover code-aware marker stripping --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -63,6 +63,39 @@ describe("sanitizeOutboundText", () => {
|
||||
expect(sanitizeOutboundText(text)).toBe("Hello\n\nWorld");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "backtick fence preserves user while stripping prose user",
|
||||
input: "user:\n```yaml\nuser:\n name: alice\n```",
|
||||
expected: "```yaml\nuser:\n name: alice\n```",
|
||||
},
|
||||
{
|
||||
name: "tilde fence preserves system while stripping prose system",
|
||||
input: "system:\n~~~yaml\nsystem:\n enabled: true\n~~~",
|
||||
expected: "~~~yaml\nsystem:\n enabled: true\n~~~",
|
||||
},
|
||||
{
|
||||
name: "indented code preserves assistant while stripping prose assistant",
|
||||
input: "keep\n\nassistant:\n\n assistant:\n enabled: true",
|
||||
expected: "keep\n\n assistant:\n enabled: true",
|
||||
},
|
||||
{
|
||||
name: "unterminated fence preserves every marker family after offset shifts",
|
||||
input: [
|
||||
"#+#+#",
|
||||
"assistant to=final",
|
||||
"user:",
|
||||
"```text",
|
||||
"#+#+#",
|
||||
"assistant to=tool",
|
||||
"user:",
|
||||
].join("\n"),
|
||||
expected: ["```text", "#+#+#", "assistant to=tool", "user:"].join("\n"),
|
||||
},
|
||||
] as const)("$name", ({ input, expected }) => {
|
||||
expect(sanitizeOutboundText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it("handles combined internal markers in one message", () => {
|
||||
const text = "<thinking>step 1</thinking>NO_REPLY +#+#+#+# assistant to=final\n\nActual reply";
|
||||
const result = sanitizeOutboundText(text);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// Imessage plugin module implements sanitize outbound behavior.
|
||||
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
||||
import {
|
||||
findCodeRegions,
|
||||
isInsideCode,
|
||||
sanitizeAssistantVisibleText,
|
||||
} from "openclaw/plugin-sdk/text-chunking";
|
||||
|
||||
/**
|
||||
* Patterns that indicate assistant-internal metadata leaked into text.
|
||||
@@ -11,6 +15,19 @@ const ASSISTANT_ROLE_MARKER_RE = /\bassistant\s+to\s*=\s*\w+/gi;
|
||||
// any line that merely ends with the word "user/system/assistant:" in prose.
|
||||
const ROLE_TURN_MARKER_RE = /^[ \t]*(?:user|system|assistant)\s*:\s*$/gm;
|
||||
|
||||
/**
|
||||
* Strip an internal-marker pattern from prose while leaving matches that fall
|
||||
* inside a fenced/indented/inline code region untouched: there a bare `user:`
|
||||
* mapping key or `#+#` line is the user's own content, not leaked scaffolding.
|
||||
* Regions are recomputed per call because the prior strip shifted later offsets.
|
||||
*/
|
||||
function stripMarkerOutsideCode(text: string, marker: RegExp): string {
|
||||
const codeRegions = findCodeRegions(text);
|
||||
return text.replace(marker, (match, offset: number) =>
|
||||
isInsideCode(offset, codeRegions) ? match : "",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip all assistant-internal scaffolding from outbound text before delivery.
|
||||
* Applies reasoning/thinking tag removal, memory tag removal, and
|
||||
@@ -23,9 +40,9 @@ export function sanitizeOutboundText(text: string): string {
|
||||
|
||||
let cleaned = sanitizeAssistantVisibleText(text);
|
||||
|
||||
cleaned = cleaned.replace(INTERNAL_SEPARATOR_RE, "");
|
||||
cleaned = cleaned.replace(ASSISTANT_ROLE_MARKER_RE, "");
|
||||
cleaned = cleaned.replace(ROLE_TURN_MARKER_RE, "");
|
||||
cleaned = stripMarkerOutsideCode(cleaned, INTERNAL_SEPARATOR_RE);
|
||||
cleaned = stripMarkerOutsideCode(cleaned, ASSISTANT_ROLE_MARKER_RE);
|
||||
cleaned = stripMarkerOutsideCode(cleaned, ROLE_TURN_MARKER_RE);
|
||||
|
||||
// Collapse excessive blank lines left after stripping.
|
||||
cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
|
||||
|
||||
Reference in New Issue
Block a user