Files
openclaw/src/imessage/monitor/sanitize-outbound.test.ts
OfflynAI adb9234d03 fix(imessage): prevent echo loop from leaking internal metadata and amplifying NO_REPLY into queue overflow (#33295)
* fix(imessage): prevent echo loop from leaking internal metadata and amplifying NO_REPLY into queue overflow

- Add outbound sanitization at channel boundary (sanitize-outbound.ts):
  strips thinking/reasoning tags, relevant-memories tags, model-specific
  separators (+#+#), and assistant role markers before iMessage delivery

- Add inbound reflection guard (reflection-guard.ts): detects and drops
  messages containing assistant-internal markers that indicate a reflected
  outbound message, preventing recursive echo amplification

- Harden echo cache: increase text TTL from 5s to 30s to catch delayed
  reflections that previously expired before the echo could be detected

- Add loop rate limiter (loop-rate-limiter.ts): per-conversation rapid-fire
  detection that suppresses conversations exceeding threshold within a
  time window, acting as a safety net against amplification

Closes #33281

* fix(imessage): address review — stricter reflection regex, loop-aware rate limiter

- Reflection guard: require closing > bracket on thinking/final/memory
  tag patterns to prevent false-positives on user phrases like
  '<final answer>' or '<thought experiment>' (#33295 review)

- Rate limiter: only record echo/reflection/from-me drops instead of
  all dispatches, so the limiter acts as a loop-specific escalation
  mechanism rather than a general throttle on normal conversation
  velocity (#33295 review)

* Changelog: add iMessage echo-loop hardening entry

* iMessage: restore short echo-text TTL

* iMessage: ignore reflection markers in code

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-03-06 19:19:57 -05:00

65 lines
2.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { sanitizeOutboundText } from "./sanitize-outbound.js";
describe("sanitizeOutboundText", () => {
it("returns empty string unchanged", () => {
expect(sanitizeOutboundText("")).toBe("");
});
it("preserves normal user-facing text", () => {
const text = "Hello! How can I help you today?";
expect(sanitizeOutboundText(text)).toBe(text);
});
it("strips <thinking> tags and content", () => {
const text = "<thinking>internal reasoning</thinking>The answer is 42.";
expect(sanitizeOutboundText(text)).toBe("The answer is 42.");
});
it("strips <thought> tags and content", () => {
const text = "<thought>secret</thought>Visible reply";
expect(sanitizeOutboundText(text)).toBe("Visible reply");
});
it("strips <final> tags", () => {
const text = "<final>Hello world</final>";
expect(sanitizeOutboundText(text)).toBe("Hello world");
});
it("strips <relevant_memories> tags and content", () => {
const text = "<relevant_memories>memory data</relevant_memories>Visible";
expect(sanitizeOutboundText(text)).toBe("Visible");
});
it("strips +#+#+#+# separator patterns", () => {
const text = "NO_REPLY +#+#+#+#+#+ more internal stuff";
expect(sanitizeOutboundText(text)).not.toContain("+#+#");
});
it("strips assistant to=final markers", () => {
const text = "Some text assistant to=final more text";
const result = sanitizeOutboundText(text);
expect(result).not.toMatch(/assistant\s+to\s*=\s*final/i);
});
it("strips trailing role turn markers", () => {
const text = "Hello\nassistant:\nuser:";
const result = sanitizeOutboundText(text);
expect(result).not.toMatch(/^assistant:$/m);
});
it("collapses excessive blank lines after stripping", () => {
const text = "Hello\n\n\n\n\nWorld";
expect(sanitizeOutboundText(text)).toBe("Hello\n\nWorld");
});
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);
expect(result).not.toContain("<thinking>");
expect(result).not.toContain("+#+#");
expect(result).not.toMatch(/assistant to=final/i);
expect(result).toContain("Actual reply");
});
});