iMessage / SMS¶
The live iMessage/SMS connector. Unlike every other connector, it runs on the host Mac, not in the container scheduler — read this whole page before turning it on.
What it does¶
An incremental poll over ~/Library/Messages/chat.db, the local SQLite
database the macOS Messages app maintains for iMessage and (via
Continuity/SMS relay) text messages. The connector opens the database
read-only and walks forward from a {rowid, guid} cursor, normalizing new
rows into the same cap_items shape as every other connector. No
credentials, no network calls.
Prereqs¶
Full Disk Access required
chat.db lives behind macOS's Full Disk Access (FDA) protection. Grant
FDA to whichever process runs the poll — the terminal app, or the
python3 binary itself: System Settings → Privacy & Security → Full
Disk Access → add it. Without FDA, opening chat.db fails with
authorization denied.
- macOS only, run on the same Mac whose Messages app owns
chat.db. - No IMAP-style account or app password — this connector reads a local file.
Why host-only¶
The connector declares host_only = True. capture_agent.poll partitions
connectors by that flag: the containerized scheduler (poll.py without
--host) skips any host_only row silently — no last_error noise — and
only the owner's --host invocation polls it. There's deliberately no
module.yaml schedule for it; it's driven by an owner-installed launchd
job instead (see below).
The cap_connectors row¶
{
"kind": "imessage",
"enabled": true,
"secret_ref": "",
"config": {
"db_path": "~/Library/Messages/chat.db"
},
"owner_addresses": ["<your iMessage handles>"]
}
secret_ref is empty — chat.db needs no credentials. owner_addresses
should list your own iMessage handles (phone numbers / email aliases); if
left empty, the connector falls back to the comma-separated
JARVIS_OWNER_EMAIL env var, same as every other connector.
Turn it on (host)¶
Create the cap_connectors row above via the
PocketBase admin UI, then run the poll once by
hand from the repo root to confirm it works:
PYTHONPATH="agent-runtime/src:modules/capture/agent" POCKETBASE_URL=http://127.0.0.1:8091 DEV_USER_EMAIL=user@jarvis.local DEV_USER_PASSWORD=userpassword123 python3 -m capture_agent.poll --host
Expect capture poll: {'connectors_polled': 1, 'ingested': N, 'errors': 0}.
LaunchAgent (recurring)¶
For it to run on its own every 10 minutes, install the LaunchAgent template
at modules/capture/agent/host/com.jarvis.capture.imessage-poll.plist:
- Edit the plist, replacing every
REPLACE_ME_*placeholder (repo root, dev credentials, owner email). - Install and load it:
cp modules/capture/agent/host/com.jarvis.capture.imessage-poll.plist ~/Library/LaunchAgents/com.jarvis.capture.imessage-poll.plist
launchctl load ~/Library/LaunchAgents/com.jarvis.capture.imessage-poll.plist
The job runs python3 -m capture_agent.poll --host every 10 minutes against
PocketBase at http://127.0.0.1:8091. Logs go to
/tmp/jarvis-capture-imessage-poll.log (stdout) and
/tmp/jarvis-capture-imessage-poll.err (stderr). To stop it:
launchctl unload ~/Library/LaunchAgents/com.jarvis.capture.imessage-poll.plist
Full details, including how the LaunchAgent is wired up, live in the host
runbook: modules/capture/agent/host/HOST_POLL.md.
Backfill¶
To import history that predates turning the live poll on:
python -m capture_agent.backfill imessage --db ~/Library/Messages/chat.db --since 2026-01-01
Verify¶
In the PocketBase admin: cap_items grows by roughly the number of new
messages, the row's last_poll_at updates each run, and last_error is
empty. The row's cursor becomes {"rowid": <last ROWID>, "guid": <that
row's guid>}.
Errors¶
- Full Disk Access denied — opening
chat.dbraisesauthorization denied; grant FDA as described above and re-run. chat.dbrebuilt — a Messages restore or migration can reset SQLite ROWIDs. Before trusting its cursor, the connector checks that the row still at the cursor'srowidstill has the recordedguid; if it doesn't, the cursor resets to0and the whole database is re-scanned. This is safe:cap_itemsdedupes byexternal_id(the messageguid), so already-ingested messages are skipped and only genuinely new ones get written.