Skip to content

send-attachment via bridge fails to stage file on macOS 26 (AppleScript path works) #103

@omarshahine

Description

@omarshahine

Symptom

imsg send-attachment (the IMCore-bridge attachment path) registers an IMFileTransfer, dispatches a placeholder IMMessage, and reports success — but the receiver sees an empty placeholder message with no file attached, and on the sender chat.db there's no row in attachment or message_attachment_join linking the transfer to the message. Same machine, same iMessage account, the AppleScript path works perfectly.

$ imsg send --file ~/photo.jpg --to "+15551234567"          # works ✓
$ imsg send-attachment --chat 'iMessage;-;+15551234567' \
                       --file ~/photo.jpg                    # broken ✗

Messages.app UI sending also works fine — receiver gets the file. So the issue is local to our IMFileTransferCenter handling on macOS 26.4.1.

What we know

  • Reproducible across small PNGs (1×1) and real-world JPEGs (178 KB) on macOS 26.4.1.
  • The bridge returns {transferGuid, messageGuid} without exception.
  • chat.db has the message row (attributedBody length ~270 B, OBJ placeholder + IM attributes), but no joined attachment row.
  • Receiver sees the empty placeholder; user-attached file via Messages.app UI in the same chat is delivered correctly side-by-side.

Root-cause investigation

The relevant code is prepareOutgoingTransfer in Sources/IMsgHelper/IMsgInjected.m, modeled on BlueBubblesHelper's prepareFileTransferForAttachment:

  1. [IMFileTransferCenter guidForNewOutgoingTransferWithLocalURL:] — returns a guid ✓
  2. [IMFileTransferCenter transferForGUID:] — returns a non-nil IMFileTransfer
  3. [IMDPersistentAttachmentController _persistentPathForTransfer:filename:highQuality:chatGUID:storeAtExternalPath:]returns nil on macOS 26.4.1
  4. (Skipped because path is nil) Copy file to persistent path
  5. [IMFileTransferCenter retargetTransfer:toPath:] (skipped)
  6. [IMFileTransfer setLocalURL:] (skipped)
  7. [IMFileTransferCenter registerTransferWithDaemon:] — called with the original /tmp/... path because we never retargeted

The daemon then has a transfer guid pointing at a path Messages.app can read but apparently can't ship through to imagent properly. Receiver gets the empty placeholder.

What's been ruled out

  • Path-prefix attribute issues: __kIMFileTransferGUIDAttributeName, __kIMFilenameAttributeName, __kIMMessagePartAttributeName, __kIMBaseWritingDirectionAttributeName are all set on the OBJ placeholder, matching BlueBubblesHelper's attachmentStr exactly.
  • Send flags: previously 0x5; now 0x100005 (BB-verified, the 0x100000 finalization bit). Doesn't change attachment behavior.
  • Init signature: switched to BB's 12-arg initWithSender:…:expressiveSendStyleID: (not the legacy initIMMessageWithSender:). Doesn't change attachment behavior.
  • IMMessageItem-first construction: bypassed for attachments — they go through BB's IMMessage init directly. No effect on the staging gap.
  • ARC zombie: getReturnValue into __unsafe_unretained for the persistentPath was tightened to a strong reference. The path was returning nil already, before the retention change — so this wasn't the cause.

What's likely the fix

Runtime probe of IMDPersistentAttachmentController on macOS 26.4.1 reveals the modern staging selectors:

-[IMDPersistentAttachmentController saveAttachmentsForTransfer:chatGUID:storeAtExternalLocation:completion:]
-[IMDPersistentAttachmentController _saveAttachmentForTransfer:highQuality:copyWithinAttachmentStore:chatGUID:storeAtExternalPath:]
-[IMDPersistentAttachmentController _persistentPathForTransfer:filename:highQuality:chatGUID:storeAtExternalPath:]

The _persistentPath… selector is still in the class but returns nil for our IMFileTransfer. The likely modern path:

  1. Allocate the transfer guid (as we do today).
  2. Resolve IMFileTransfer (as we do today).
  3. Use saveAttachmentsForTransfer:chatGUID:storeAtExternalLocation:completion: (block-based) instead of _persistentPathForTransfer: to let the controller stage the file itself.
  4. Wait for the completion to fire (run-loop pump similar to deriveThreadIdentifier).
  5. registerTransferWithDaemon:.

Or possibly _saveAttachmentForTransfer:highQuality:copyWithinAttachmentStore:chatGUID:storeAtExternalPath: — synchronous variant. Worth class-dumping IMDPersistentAttachmentController.h from a fresh dyld_shared_cache extract on macOS 26.4.1 to get the exact signatures.

Reference

BlueBubblesHelper's prepareFileTransferForAttachment is what prepareOutgoingTransfer is modeled on. BB's most recent test target may pre-date macOS 26.

Workaround

Use the AppleScript path until the staging fix lands:

imsg send --file ~/photo.jpg --to "+15551234567"

imsg send --file does not require SIP-disabled bridge injection and is unaffected by this regression. It's the path Lobster's gateway should prefer for attachments.

Related

cc @omarshahine

🤖 Filed via Claude Code after a multi-hour debugging session

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions