* 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>
45 lines
1.7 KiB
TypeScript
45 lines
1.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { createSentMessageCache } from "./echo-cache.js";
|
|
|
|
describe("iMessage sent-message echo cache", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("matches recent text within the same scope", () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-02-25T00:00:00Z"));
|
|
const cache = createSentMessageCache();
|
|
|
|
cache.remember("acct:imessage:+1555", { text: " Reasoning:\r\n_step_ " });
|
|
|
|
expect(cache.has("acct:imessage:+1555", { text: "Reasoning:\n_step_" })).toBe(true);
|
|
expect(cache.has("acct:imessage:+1666", { text: "Reasoning:\n_step_" })).toBe(false);
|
|
});
|
|
|
|
it("matches by outbound message id and ignores placeholder ids", () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-02-25T00:00:00Z"));
|
|
const cache = createSentMessageCache();
|
|
|
|
cache.remember("acct:imessage:+1555", { messageId: "abc-123" });
|
|
cache.remember("acct:imessage:+1555", { messageId: "ok" });
|
|
|
|
expect(cache.has("acct:imessage:+1555", { messageId: "abc-123" })).toBe(true);
|
|
expect(cache.has("acct:imessage:+1555", { messageId: "ok" })).toBe(false);
|
|
});
|
|
|
|
it("keeps message-id lookups longer than text fallback", () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-02-25T00:00:00Z"));
|
|
const cache = createSentMessageCache();
|
|
|
|
cache.remember("acct:imessage:+1555", { text: "hello", messageId: "m-1" });
|
|
// Text fallback stays short to avoid suppressing legitimate repeated user text.
|
|
vi.advanceTimersByTime(6_000);
|
|
|
|
expect(cache.has("acct:imessage:+1555", { text: "hello" })).toBe(false);
|
|
expect(cache.has("acct:imessage:+1555", { messageId: "m-1" })).toBe(true);
|
|
});
|
|
});
|