a short incident note.

netsky sent 678 iMessages into my own self-chat on 2026-05-16.

Apple’s Messages database recorded 1,356 rows because each ack appeared twice: once as the outgoing send and once as the reflected incoming copy.

the ack burst in Messages

what happened #

flowchart TD
    A["daemon starts"] --> B["cannot read ~/Library/Messages/chat.db"]
    B --> C["record imessage.poll_error events"]
    C --> D["Full Disk Access changes"]
    D --> E["daemon can read chat.db"]
    E --> F{"durable iMessage cursor?"}
    F -- "no" --> G["bad recovery path: start from rowid 0"]
    G --> H["scan old self-chat rows"]
    H --> I["import 679 historical inbound rows"]
    I --> J["create 679 tasks"]
    J --> K["send 678 auto-acks"]
    K --> L["self-chat reflects each ack"]
    L --> M["1,356 Messages rows"]

The daemon had no durable iMessage cursor. It also had prior imessage.poll_error events from macOS privacy denial: the foreground process could read chat.db, but the launchd daemon could not.

The bad startup rule was:

if there were poll errors and no durable cursor, start from rowid 0.

After Full Disk Access started working, that rule replayed historical Messages rows instead of starting at the current watermark.

counts #

  • 679 old inbound/self-reflected iMessages imported
  • 678 successful imessage.ack_sent events
  • 0 imessage.reply_sent events
  • 678 outgoing Messages rows: is_from_me=1, is_sent=1, is_delivered=1, error=0
  • 678 reflected incoming Messages rows: is_from_me=0
  • 678 distinct ack texts shaped like received: task-...

The historical messages came from April 16-18, 2026. The ack burst happened on May 16, 2026.

timeline #

  • 18:39:42 EDT: first ack recorded by netsky
  • 18:41:20 EDT: last ack recorded by netsky
  • 18:41:57 EDT: last outgoing ack row recorded by Messages
  • shortly after: daemon unloaded, LaunchAgent plist disabled, Messages quit

One imported task did not get an ack before the process was killed.

fix #

  • no cursor means bootstrap to the current Messages watermark
  • bootstrap sends nothing
  • the same rule applies if chat.db becomes readable after startup
  • backlog recovery must be explicit and bounded
  • the daemon remains unloaded until re-enabled intentionally

Automatic daemon startup must not turn historical private messages into new outbound messages.