diff --git a/services/imessage-bridge/src/session.ts b/services/imessage-bridge/src/session.ts index 5ec00c74..c0219aaf 100644 --- a/services/imessage-bridge/src/session.ts +++ b/services/imessage-bridge/src/session.ts @@ -7373,7 +7373,12 @@ export class IMessageSession { // PROACTIVE INGESTER (unknown senders): appointment confirmations → // surface to the owner + offer to add to the calendar; marketing/spam → // suppress (no auto-reply). Behind LANTERN_APPT_INGEST (default on). - const ingest = await this.maybeIngestUnknownInbound(row.handle, text).catch(() => "pass" as const); + // DM-only, matching every sibling proactive-scan block above and the + // WhatsApp equivalent (isGroupJid guard) — a group participant's message + // is never an "unknown sender" appointment confirmation to swallow. + const ingest = !isGroup + ? await this.maybeIngestUnknownInbound(row.handle, text).catch(() => "pass" as const) + : ("pass" as const); if (ingest === "handled") { // No silent drops: an unknown-inbound message owned by the life-event / // appointment ingester means the CONTACT got no auto-reply. Leave a trace diff --git a/services/imessage-bridge/src/unknown-inbound-group-scope.test.ts b/services/imessage-bridge/src/unknown-inbound-group-scope.test.ts new file mode 100644 index 00000000..a3e01c09 --- /dev/null +++ b/services/imessage-bridge/src/unknown-inbound-group-scope.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; + +// Regression: PROACTIVE INGESTER (maybeIngestUnknownInbound) is designed for +// unknown 1:1 senders ("appointment confirmations from an unrecognized +// number"), but its iMessage call site had no !isGroup guard — unlike every +// sibling proactive-scan block in this file (style-learning, inner-circle +// agentic, proactive-memory) and unlike the WhatsApp bridge's own +// maybeIngestUnknownInbound (`!this.isGroupJid(from)`). A group participant's +// message pattern-matching delivery/appointment language got silently +// swallowed — "contact reply suppressed — unknown-inbound owned by +// life-event/appointment ingestion" — inside live GROUP threads, confirmed +// live 2026-08-18 (chatRowid 2656, isGroup:true) and reconfirmed repeatedly. +// This only fixes the group-scope leak; whether a genuine 1:1 unknown-sender +// appointment text should ever get a contact-facing reply is a separate, +// still-open product question (introspect state.json). + +describe("maybeIngestUnknownInbound call site is DM-only", () => { + const src = readFileSync(new URL("./session.ts", import.meta.url), "utf8"); + + it("the call site checks !isGroup before invoking the ingester", () => { + const anchor = "const ingest = "; + const site = src.slice(src.indexOf(anchor), src.indexOf(anchor) + 300); + expect(site).toMatch(/!isGroup/); + expect(site).toMatch(/maybeIngestUnknownInbound/); + }); +});