4 KiB
iCalls (ICallsSync)
Purpose
Ingest macOS call history (Phone/FaceTime) into the server for timelines, recent activity, and correlation with messages/contacts/prospects.
Direction
Read-only. Call logs are append-only records emitted by the system; there is no Sender or send queue.
OS surface
~/Library/Application Support/CallHistoryDB/CallHistory.storedata (and legacy
~/Library/CallHistoryDB/CallHistory.storedata).
Direct readonly SQLite via GRDB (Core Data schema: ZCALLRECORD, ZUNIQUE_ID,
ZDATE as Apple reference time, ZADDRESS, ZORIGINATED, ZANSWERED,
ZDURATION, ZCALLTYPE, ZSERVICE_PROVIDER, ZNAME).
com.apple.security.files.all entitlement (already present for chat.db) is
sufficient. No extra TCC prompt beyond Full Disk Access.
Files
Reader.swift—CallHistoryReader:- Tries current (
Application Support/CallHistoryDB) then legacy path. fetchCalls(since: Date?)— filters onZDATE > ref, oldest-first.- WAL snapshot for freshness:
makeSnapshotcopies the live.storedata-wal+-shminto a temp dir;readCallsopens the copy read-write so SQLite replays un-checkpointed frames. Recent calls (critical for triage) are no longer missed. Snapshot is cleaned up withdefer.
- Timestamp conversion:
Date(timeIntervalSinceReferenceDate:). - Basic CNContact enrichment for display names when
ZNAMEabsent (best-effort). - Uses
PhoneUtils.normalize. Exposed viaisAccessiblefor Base auth gating.
- Tries current (
APIClient.swift:syncCalls(_:)→POST /client/icalls/syncgetStats()→GET /client/icalls/stats
SyncManager.swift— 120s read interval (per operator request), no send queue hooks. Batches of 200. SimplesyncedThisSession+ total from server. Authorization hooks gate onreader.isAccessible(Full Disk).
Timing
- Read interval: 120s (
@packages/icalls/Sources/ICallsSync/SyncManager.swift).
Server
- Entity:
macsync.calls(dedicated table, not mixed into messages). Unique on(device_id, external_id). - Ingest:
features/icalls/ingestCalls(dedup + upsert). Recordscalls_syncedin sync-history. - Queries:
/my/calls(filter by deviceId, direction, callType, since (ISO), limit, offset) +/my/calls/stats. Returns address, normalizedAddress, contactName, direction, callType (telephony/facetime_*), answered, durationSeconds, startedAt, serviceProvider. Sorted newest-first for triage use. - Client ingest:
/client/icalls/sync+/stats(wrapped in sync-history 'calls'). - MCP:
recent_callstool (mcp/) exposes the surface for agents/triage.
Local Mac access (no remote server hop)
LocalWebServer(/api/calls?limit=N) — structured recent calls, same shape as server. Useful for "Control your Mac" agents or local tools. Also reportscallHistoryDb(available + error) in/api/diagnostics(shown in Dashboard "Native Subsystems" card).- Same Full Disk grant as iMessage (no extra prompt).
Web
web/src/api/calls.ts+fetchCalls/fetchCallStatsweb/src/tabs/Calls/index.tsx(filter by direction/callType, table view)- Integrated in
App.tsxroute,AppShellnav, Dashboard KPI tile (from throughput) + "Call History DB" row in Native Subsystems (from diagnostics). - Prospect cockpit
/threadalso surfacesrecentCallsfor the handle.
Known characteristics
- First sync backfills available history (can be thousands of rows; rows are tiny).
ZUNIQUE_ID(or syntheticzpk:<Z_PK>) is the stable external id.- Multi-party FaceTime participants live in join tables (
Z_2REMOTEPARTICIPANTHANDLES+ZHANDLE); v1 surfaces primaryZADDRESSonly. - Duration 0 + unanswered often indicates missed/ringing-out attempts.
- See known limitations for WAL snapshot details, iPhone cellular gap, etc.
Adding / extending
Follow the read-only path in docs/adding-a-module.md (no send queue / Sender / admin
send routes). Add columns or participant join expansion in a follow-up migration if needed.