diff --git a/services/imessage-bridge/src/session.ts b/services/imessage-bridge/src/session.ts index 83f47cab..269720ba 100644 --- a/services/imessage-bridge/src/session.ts +++ b/services/imessage-bridge/src/session.ts @@ -5726,6 +5726,16 @@ export class IMessageSession { const sms = await this.sender.sendSMS(to, text); if (sms.ok) { this.logger.info({ to, lastService: st?.service, lastError: st?.error }, "pre-routed via SMS service (Text Message Forwarding) — iMessage not working for this contact"); + // Audit the send HERE too: this branch returns before the outbound + // audit at the end of send(), so an RCS/SMS-only contact's replies + // were invisible to it. Caught by the first real end-to-end test — + // the reply landed, and the log that exists to measure reply quality + // had no record of it. Same shape as the main site so the two are + // greppable together. + this.logger.info( + { to, textPreview: text.slice(0, 160), len: text.length, botSelf: isBotSelfMessage(text), via: "sms-preroute" }, + "outbound sent", + ); this.recordBridgeSend(text); this.enqueueOutboundEcho(to, text); this.broadcast({ type: "agent_reply", data: { to, text, timestamp: Date.now() } }); diff --git a/services/imessage-bridge/src/silent-drop-audit.test.ts b/services/imessage-bridge/src/silent-drop-audit.test.ts index 1e650cfb..be6dccc6 100644 --- a/services/imessage-bridge/src/silent-drop-audit.test.ts +++ b/services/imessage-bridge/src/silent-drop-audit.test.ts @@ -75,3 +75,25 @@ describe("no reply-suppressing path returns without a trace", () => { expect(src).toMatch(/this\.reportSilentDrops\(\);/); }); }); + +describe("every send path reaches the outbound audit log", () => { + const src = readFileSync(new URL("./session.ts", import.meta.url), "utf8"); + + it("the SMS pre-route branch audits before it returns", () => { + // Found by the first real end-to-end test: an RCS-only contact got a real + // reply via Text Message Forwarding, but the branch returns early — before + // the audit at the end of send() — so the log built to measure reply + // quality had no record of the one reply that proved the path works. + const branch = src.slice(src.indexOf("pre-routed via SMS service")); + const upToReturn = branch.slice(0, branch.indexOf("return { ok: true }")); + expect(upToReturn).toMatch(/"outbound sent"/); + }); + + it("send() has an audit line for each early return", () => { + const fn = src.slice(src.indexOf("pre-routed via SMS service") - 4000); + const body = fn.slice(0, fn.indexOf("private recordBridgeSend")); + const audits = (body.match(/"outbound sent"/g) || []).length; + // One for the SMS pre-route branch, one for the normal path. + expect(audits).toBeGreaterThanOrEqual(2); + }); +}); diff --git a/services/whatsapp-bridge/src/session.ts b/services/whatsapp-bridge/src/session.ts index 741d062e..a426e3f9 100644 --- a/services/whatsapp-bridge/src/session.ts +++ b/services/whatsapp-bridge/src/session.ts @@ -3486,6 +3486,16 @@ export class WhatsAppSession { } const sent = await this.socket.sendMessage(own, { text }); if (sent?.key?.id) this.bridgeSentIds.set(sent.key.id, Date.now()); + // Audit here too: sendSelf talks to the socket DIRECTLY rather than going + // through sendMessage, so owner-facing traffic (briefs, digests, drafts, + // drop notices) was absent from the audit log. Same omission as the + // iMessage SMS pre-route branch — a send path that bypasses the one place + // the audit lives. botSelf is nearly always true here, which is exactly + // what makes the "real reply vs machinery" ratio meaningful. + this.logger.info( + { to: own, textPreview: text.slice(0, 160), len: text.length, botSelf: isBotSelfMessage(text), via: "sendSelf" }, + "outbound sent", + ); // ponytail: owner briefs are serial, so a single last-key field is enough // for handleCenterCommand to remember/edit the Brief it just sent. this.lastSelfSentKey = sent?.key ?? undefined; @@ -7198,6 +7208,13 @@ export class WhatsAppSession { if (!own || !this.socket) return; try { const sent = await this.socket.sendMessage(own, { text }); + // Audit: confirmToSelf is the owner's MAIN reply channel on WhatsApp and + // it talks to the socket directly, like sendSelf. Without this the audit + // log sees almost no owner-side WhatsApp traffic at all. + this.logger.info( + { to: own, textPreview: text.slice(0, 160), len: text.length, botSelf: isBotSelfMessage(text), via: "confirmToSelf" }, + "outbound sent", + ); if (sent?.key?.id) { this.bridgeSentIds.set(sent.key.id, Date.now()); // Best-effort retry-tracking: confirmToSelf is called from many