Skip to content

fix(imessage): the SMS pre-route branch skipped the outbound audit log - #228

Merged
dshakes merged 3 commits into
masterfrom
fix/audit-sms-preroute
Aug 25, 2026
Merged

dshakes merged 3 commits into
masterfrom
fix/audit-sms-preroute

Conversation

@dshakes

@dshakes dshakes commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Found by the first real end-to-end test after the mute was lifted.

A contact texted "Hi", the bot replied "hey what's up" — the path works. But the audit log added in #224 to make reply quality measurable had no record of it.

Cause

The reply went out via the SMS pre-route branch (the contact is on RCS, so iMessage is skipped in favour of Text Message Forwarding). That branch returns at its own return { ok: true }, before the audit line at the end of send().

Every RCS/SMS-only contact's replies were invisible to the audit.

Why this matters more than a missing log line

The whole point of the audit was that a week of traffic left only ~33 verbatim samples. The contacts most likely to be missing from the corpus are exactly the ones on non-iMessage handsets — a silent, systematic sampling bias in the data meant to answer "does the bot sound like me?"

Measuring voice against a corpus that structurally excludes a class of contacts would have produced confident, wrong conclusions.

Fix

Both early returns now audit, same field shape as the main site plus via: "sms-preroute" so the paths stay distinguishable.

The test asserts every send path reaches an audit line — an invariant, not a value. That's the bug class: a new return in send() silently drops a contact out of the corpus.

57/57 imessage-bridge, 1498/1498 bridge-core, typecheck clean.

Found by the first real end-to-end test after the mute was lifted. A contact
texted "Hi", the bot replied "hey what's up" — the path works — but the audit
log added to make reply quality measurable had NO record of it.

Cause: the reply went out via the SMS pre-route branch (the contact is on RCS,
so iMessage is skipped in favour of Text Message Forwarding). That branch
returns at its own `return { ok: true }`, before the audit line at the end of
send(). Every RCS/SMS-only contact's replies were invisible to it.

This matters more than a missing log line: the whole point of the audit was
that a week of traffic left only ~33 verbatim samples, and the contacts most
likely to be missing from the corpus are exactly the ones on non-iMessage
handsets — a silent, systematic sampling bias in the data meant to answer
"does the bot sound like me".

Audited both early returns now, same field shape as the main site (plus
via:"sms-preroute" so the paths are distinguishable) and a test that asserts
every send path reaches an audit line rather than checking a value.

57/57 imessage-bridge, 1498/1498 bridge-core, typecheck clean.
@github-actions github-actions Bot added the domain:core Core library / business logic label Aug 25, 2026
@compass-sdlc-bot compass-sdlc-bot Bot added the agent:reviewed-clean Reviewer found no Blocking issues this round label Aug 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Codex cross-audit (agent:audit)

No Blocking findings.

I did not find correctness, security, or missed-edge regressions in the PR diff. The runtime change only adds the missing "outbound sent" audit log on the successful SMS pre-route early return, using the same bounded textPreview pattern as the normal send path.

Verification note: I attempted the targeted Vitest file, but dependencies are not installed in this workspace:

sh: 1: vitest: not found

Same class as the iMessage SMS pre-route gap, found by auditing every send
path instead of waiting for the next test to trip over one: sendSelf talks to
the Baileys socket DIRECTLY rather than going through sendMessage, so all
owner-facing WhatsApp traffic — briefs, digests, draft approvals, drop notices
— never reached the audit line.

The iMessage bridge logged owner sends (they go through send()); WhatsApp did
not. So "how much of what we send is machinery vs real replies" was computed
from a corpus missing one channel's owner traffic entirely, which is precisely
the ratio the audit exists to answer.

Audited with via:"sendSelf" so the paths stay distinguishable. sendMessage's
two early returns are deliberately NOT audited — they are suppressions (the
abstain sentinel, and the duplicate-send backstop), not sends.

1498/1498 bridge-core, 57/57 imessage-bridge, both bridges typecheck clean.
@github-actions

Copy link
Copy Markdown
Contributor

🔷 Gemini cross-audit (agent:audit-gemini)

Audit Report: unified diff in pr-diff.patch

🔴 Blocking Issues

  • None found. The proposed changes are extremely clean, highly targeted, and correct.

🟢 Correctness & Edge Cases

  1. Resolved Gap: The patch correctly identifies and plugs a critical logging gap. When replies are sent to RCS/SMS-only contacts via the native SMS service (Text Message Forwarding), they return early (return { ok: true }) before reaching the bottom of the send() method. By adding the audit block in this branch, all successful SMS pre-route sends are now captured.
  2. Execution Safety: All properties and parameters in the added logging call are safe to invoke:
    • text is typed as a string on the method signature, ensuring text.slice(0, 160) and text.length are safe and null-tolerant.
    • isBotSelfMessage(text) is correctly imported/in-scope and operates safely.
  3. No Duplicate Logging: Since the pre-route branch terminates immediately after with return { ok: true }, there is no risk of dual-logging the same send event.

🔒 Security & Privacy

  • PII Protection: The log statement conforms with standard privacy practices in the repository by capping the verbatim preview at 160 characters (text.slice(0, 160)), ensuring full PII is not leaked into the persistence layer.
  • Traceability: Including { via: "sms-preroute" } as a property provides valuable tracing context while keeping the core message "outbound sent" consistent and easily searchable across all pathways.

🧪 Test Suite Evaluation

  • Assertion Validity: The new tests in silent-drop-audit.test.ts use source introspection (readFileSync and regex/indexOf slicing) to statically assert the logic boundaries in session.ts. This ensures no future developer can introduce a regression or bypass auditing.
  • Test Bounds: Slicing the file backwards by 4000 characters is safe given the 11,000+ line size of session.ts, and correctly scopes the entire send() function to prevent false positives from neighboring methods.

Third instance of the same class, found by enumerating every raw
socket.sendMessage call rather than fixing them one test at a time.
confirmToSelf is the owner's MAIN reply channel on WhatsApp and it talks to
the socket directly — so after fixing sendSelf the audit still saw almost no
owner-side WhatsApp traffic.

Audited the full set now. What each remaining raw send is, and why it stays
unaudited:
  - sendVoiceToSelf / flushWarnings / checkAttention — owner-facing, and all
    of them ultimately duplicate text already audited elsewhere
  - sendDocument — a file, not a text reply
  - sendReaction — an emoji tapback, not a message

The guarantee that matters: NO contact-facing text reply can reach a contact
without an audit line, on either bridge. Owner traffic is audited wherever it
originates a message rather than echoing one.

1498/1498 bridge-core, 57/57 imessage-bridge, both bridges typecheck clean.
@dshakes
dshakes merged commit 243dd9f into master Aug 25, 2026
8 checks passed
@dshakes
dshakes deleted the fix/audit-sms-preroute branch August 25, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:reviewed-clean Reviewer found no Blocking issues this round domain:core Core library / business logic

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant