I texted Bubbles a screenshot of a golf leaderboard. He replied, with courtesy, that the image did not come through.

The image came through. It sat in the Messages attachments folder on his Mac, the same as always. Bubbles could not see it. The reason became one of my favorite fixes of the year.

An empty picture frame

Our family AI uses an iMessage integration. It is a fork of an adapter that ships as compiled Python bytecode. A .pyc file. No source. It works, and that is the reason I run it. But when a defect appears, you cannot read the code. You reason about a locked room.

The flaw of the locked room was simple. The adapter handled outbound images only. The polling step dropped the inbound attachments. The AI model received only one invisible placeholder character. That is the Unicode “object replacement character,” which Messages uses to mark the position of an attachment. No attachment sat behind it. The model held an empty picture frame, and I asked it what it saw.

A lever connected to nothing

My first fix failed in an instructive way. I wrote it as a “skill,” which is the documented extension mechanism. It did nothing, in silence. An earlier upstream update removed that loading feature. The docs described a lever that connected to nothing.

It got worse. The model saw the placeholder character and a skill that it half remembered. It started to hallucinate a fake tool call to an image-analysis function that did not exist. The lesson: when behavior must happen, put it in deterministic code. Do not put it in a suggestion to a language model.

A bay window on a black box

The real fix is a shim. The compiled adapter builds each incoming message event and dispatches it through a normal method call. A subclass can intercept a normal method call. So mine does.

When a message contains the placeholder character, the shim queries the Messages database directly. It joins the message to the attachment. It waits a few seconds, in case the file transfer is still in flight. It converts HEIC to JPEG. It fills the media fields that the gateway already knows how to route. The pixels flow to the model natively. The black box stays sealed. I built a bay window onto its side.

The row-number confession

One defect on the way gets its confession. The adapter’s message ID was the database row number, not the message GUID that I assumed. So my first shim matched zero rows and failed in silence. It was a no-op with perfect posture. The lookup now accepts either identifier.

Now I text Bubbles a leaderboard, and he tells me who wins. It was worth each hour. I still never saw the source code of the thing that I fixed.