You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds a Voicemail service that reads the voicemails the Phone app on macOS 26 and later syncs to the Mac — both carrier voicemails and FaceTime audio and video messages.
Three read-only tools:
voicemails_fetch — list messages (sender, date, duration, read state, type, transcript availability), with filters for sender, date range, and type
voicemail_transcript_fetch — flattened transcript text plus per-segment text and confidence
voicemail_audio_fetch — the message audio as audio/amr (legacy carrier voicemail) or audio/mp4 (FaceTime audio message and current carrier voicemail)
How
The facetimemessagestored daemon mirrors voicemails into a Core Data SQLite store inside the FaceTime group container (.../Data Store/FaceTimeMessageStore-local.sqlitedb), with message audio in an Assets tree beside the database. The schema is private to the daemon, so VoicemailStore reads only the columns it needs, through the same pattern the Phone service uses for call history: read-only SQLite with an immutable-URI fallback, and a folder grant on Data Store when the default path is not readable (the write-ahead log and the Assets tree sit beside the database, so the grant has to cover the folder).
Transcripts are inline NSKeyedArchiver blobs in two shapes — a transcript object (transcriptionString, confidence, segments) for carrier voicemails and a bare array of segment dictionaries for FaceTime messages; both are decoded.
Since the iCloud voicemail era, the daemon writes carrier voicemails as audio-only QuickTime .MOV files — the same file type FaceTime video messages carry — so .MOV rows are classified by inspecting the asset's tracks: a sound track with no video track is a carrier voicemail (returned as audio/mp4); a video track is a FaceTime video message, listed for completeness but not returned as audio. Assets that are missing or don't parse keep the conservative video-message default, and hasAudio reflects audio the tool can return. Type filters resolve .MOV rows by asset, with the result limit applied after classification.
The service registers beside PhoneService, gated on macOS 26 availability (VoicemailService.isSupported), so Macs without the Phone app never see it; the store itself is validated during activation, when the folder grant runs.
Testing
VoicemailStoreTests build a synthesized SQLite store from scratch — no fixture is copied from a real one — and cover row mapping, filters, deleted-message exclusion, both transcript shapes, audio resolution, 64-bit identifiers, and .MOV track-based classification (audio-only, video, unparseable, extended-size atoms, and malformed-size rejection without overflow)
xcodebuild -scheme imcp-serverTests -destination 'platform=macOS' test passes (44 tests)
Read-only checks against a live store on a Mac running the Phone app: message listing, both transcript shapes, playable audio (including iCloud-era .MOV carrier voicemails), and type filters all resolve correctly
* Add VoicemailService with voicemails_fetch, voicemail_transcript_fetch,
and voicemail_audio_fetch tools
* Read the store the Phone app syncs on macOS 26 and later: ZSTOREDMESSAGE
rows, inline NSKeyedArchiver transcripts, and audio in the Assets tree
* Decode both transcript shapes: transcript objects with flattened text and
segments, and bare segment arrays
* Gate service registration on the store's existence, so Macs without the
Phone app are unaffected
* Request access to the Data Store folder, since the write-ahead log and
the Assets tree sit beside the database
* List FaceTime video messages but return audio only for carrier
voicemails and FaceTime audio messages
* Add VoicemailStoreTests against a synthesized store; no fixture is copied
from a real one
- Register the service on macOS 26 and later instead of probing the
store path, which the app sandbox hides before any grant is given;
activation validates the store and requests the Data Store folder
- Implement Service activation: isActivated reports real access state
and activate() runs the folder-grant flow when the service is toggled
on, matching MessageService
- Escape SQL LIKE wildcards in the sender filter, so a literal backslash,
percent, or underscore in an address matches only itself
- Add a regression test for wildcard senders
Hi @bradhallett. Thank you for all your work on this! I appreciate the detailed write-up in #252, the tests against a synthesized store, and how you worked through Copilot's feedback (and thanks for signing your commits, too). I'm excited to try it out today.
Since the iCloud voicemail era, the daemon stores carrier voicemails as
audio-only QuickTime .MOV files — the same ZFILETYPE FaceTime video
messages use — so every recent voicemail listed as a faceTimeVideo with
hasAudio false, and voicemail_audio_fetch rejected them.
The store now inspects each .MOV asset's tracks: a sound track with no
video track is a carrier voicemail, served as audio/mp4; a video track is
a FaceTime video message. Rows whose asset is missing or unparseable keep
the conservative faceTimeVideo default. Kind filters for the two .MOV
kinds select candidates by ZFILETYPE and resolve them by asset, with the
limit applied after classification.
Two findings from testing against a live store on macOS 27.2, worth knowing before you try it:
Carrier voicemails are audio-only .MOV files now. Since the iCloud voicemail era (this Mac flipped over in July, on the 27 beta), facetimemessagestored writes every new carrier voicemail as a QuickTime .MOV with a single AAC track — the same ZFILETYPE FaceTime video messages carry. On the previous revision that typed every post-July voicemail as faceTimeVideo with hasAudio: false, and voicemail_audio_fetch rejected them. I pinned the taxonomy down against CallHistoryDB: the post-July .MOV rows correlate with cellular calls, .amr is the legacy carrier format, and .m4a rows are FaceTime audio messages.
af3e511 fixes the classification by inspecting each .MOV asset's tracks: a sound track with no video track is a carrier voicemail (served as audio/mp4), a video track is a FaceTime video message, and assets that are missing or don't parse keep the conservative default. The type filters resolve .MOV rows by asset, with the limit applied after classification. The suite is at 40 tests now; four new ones cover the classification and the limit ordering. 0d3ab92 also points the no-transcript error at voicemail_audio_fetch, since that audio is still available.
On untranscribed messages: 19 on this Mac, and most are hang-ups — 12 are 1–9 seconds long with tiny assets, nothing to transcribe. The other 7 have real speech (15–204s), including four max-length 203s carrier voicemails where the system engine marked transcription as failed. So a SpeechTranscriber fallback would only ever help that second group — feels like a follow-up at most, not part of this PR.
- Create the Data Store bookmark with .withSecurityScope alongside
.securityScopeAllowOnlyReadAccess, so it resolves as read-only
security-scoped rather than as an ordinary bookmark.
- Probe .MOV assets through a memory-mapped read, so classification
faults only the header pages it inspects instead of loading whole
video assets.
- Parse extended-size QuickTime atoms: a size of 1 marks a 16-byte
header whose 64-bit size follows the type, and traversal enters or
skips such atoms correctly.
- Bind integer parameters with sqlite3_bind_int64 so Z_PK values above
Int32.max keep resolving to their transcripts and audio.
Avoid directing clients to audio tools when audio is unavailable
App/Services/Voicemail.swift:390
This path does not establish that audio is available: it also handles video messages and voicemails whose asset is missing (hasAudio == false). The error therefore directs clients to another tool that is guaranteed to fail for some valid messages; either condition the advice on hasAudio or omit it.
A malformed extended-size atom can declare a size up to Int.max;
computing offset + size then traps instead of breaking out of the
atom walk. Compare against the bytes remaining in the container so
damaged assets keep the documented conservative classification.
Avoid claiming audio is available when no transcript exists
App/Services/Voicemail.swift:390
This error always claims audio is available, but transcript-less FaceTime video messages and rows with missing assets explicitly have hasAudio == false. That sends clients to a tool guaranteed to fail; report only that no transcript exists (or condition the audio guidance on the resolved message's hasAudio).
Stop probing assets after reaching the requested match limit
App/Services/VoicemailStore.swift:33
For carrier/video filters this loop probes every candidate .MOV asset before filtering and applying limit. Because the SQL result is already newest-first, classification can stop as soon as limit matching rows have been found; otherwise a request such as limit: 1 still opens every movie in a large store.
Thanks, @bradhallett. I really appreciate you building this out and working through every round of review.
While trying it with my own voicemail, I found a few things, which I fixed in #270:
voicemails_fetch now reads the inbox by default. The store keeps blocked and deleted messages in the same table (ZMAILBOXTYPE 1 and 2), so they were mixed into the results. A new mailbox parameter asks for them explicitly.
The message type comes from ZPROVIDER and ZMESSAGETYPE instead of the file extension. Some carrier voicemails are stored as m4a, and FaceTime video messages can be lowercase mov.
When ZDURATION is 0, the duration comes from the file's movie header.
Phone numbers in the sender filter match by digits, so (503) 555-0123 finds +15035550123.
The service is green now, to match its icon.
I also took the store tests out of the CLI test target. I'd like to move iMCP's database access into its own package, and test it thoroughly there.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #252
What
Adds a Voicemail service that reads the voicemails the Phone app on macOS 26 and later syncs to the Mac — both carrier voicemails and FaceTime audio and video messages.
Three read-only tools:
voicemails_fetch— list messages (sender, date, duration, read state, type, transcript availability), with filters for sender, date range, and typevoicemail_transcript_fetch— flattened transcript text plus per-segment text and confidencevoicemail_audio_fetch— the message audio asaudio/amr(legacy carrier voicemail) oraudio/mp4(FaceTime audio message and current carrier voicemail)How
The
facetimemessagestoreddaemon mirrors voicemails into a Core Data SQLite store inside the FaceTime group container (.../Data Store/FaceTimeMessageStore-local.sqlitedb), with message audio in anAssetstree beside the database. The schema is private to the daemon, soVoicemailStorereads only the columns it needs, through the same pattern the Phone service uses for call history: read-only SQLite with an immutable-URI fallback, and a folder grant onData Storewhen the default path is not readable (the write-ahead log and theAssetstree sit beside the database, so the grant has to cover the folder).Transcripts are inline
NSKeyedArchiverblobs in two shapes — a transcript object (transcriptionString,confidence,segments) for carrier voicemails and a bare array of segment dictionaries for FaceTime messages; both are decoded.Since the iCloud voicemail era, the daemon writes carrier voicemails as audio-only QuickTime
.MOVfiles — the same file type FaceTime video messages carry — so.MOVrows are classified by inspecting the asset's tracks: a sound track with no video track is a carrier voicemail (returned asaudio/mp4); a video track is a FaceTime video message, listed for completeness but not returned as audio. Assets that are missing or don't parse keep the conservative video-message default, andhasAudioreflects audio the tool can return. Type filters resolve.MOVrows by asset, with the result limit applied after classification.The service registers beside
PhoneService, gated on macOS 26 availability (VoicemailService.isSupported), so Macs without the Phone app never see it; the store itself is validated during activation, when the folder grant runs.Testing
VoicemailStoreTestsbuild a synthesized SQLite store from scratch — no fixture is copied from a real one — and cover row mapping, filters, deleted-message exclusion, both transcript shapes, audio resolution, 64-bit identifiers, and.MOVtrack-based classification (audio-only, video, unparseable, extended-size atoms, and malformed-size rejection without overflow)swift format lint --strict --recursive .passesxcodebuild -scheme iMCP -configuration Debug -destination 'platform=macOS' buildsucceedsxcodebuild -scheme imcp-serverTests -destination 'platform=macOS' testpasses (44 tests).MOVcarrier voicemails), and type filters all resolve correctly