macsync/docs/modules/icalls.md
Natalie ad8e126dd1 docs(mac-sync): outbox/read architecture, handoffs, module docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:35:13 -04:00

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.swiftCallHistoryReader:
    • Tries current (Application Support/CallHistoryDB) then legacy path.
    • fetchCalls(since: Date?) — filters on ZDATE > ref, oldest-first.
    • WAL snapshot for freshness: makeSnapshot copies the live .storedata
      • -wal + -shm into a temp dir; readCalls opens the copy read-write so SQLite replays un-checkpointed frames. Recent calls (critical for triage) are no longer missed. Snapshot is cleaned up with defer.
    • Timestamp conversion: Date(timeIntervalSinceReferenceDate:).
    • Basic CNContact enrichment for display names when ZNAME absent (best-effort).
    • Uses PhoneUtils.normalize. Exposed via isAccessible for Base auth gating.
  • APIClient.swift:
    • syncCalls(_:)POST /client/icalls/sync
    • getStats()GET /client/icalls/stats
  • SyncManager.swift — 120s read interval (per operator request), no send queue hooks. Batches of 200. Simple syncedThisSession + total from server. Authorization hooks gate on reader.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). Records calls_synced in 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_calls tool (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 reports callHistoryDb (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/fetchCallStats
  • web/src/tabs/Calls/index.tsx (filter by direction/callType, table view)
  • Integrated in App.tsx route, AppShell nav, Dashboard KPI tile (from throughput) + "Call History DB" row in Native Subsystems (from diagnostics).
  • Prospect cockpit /thread also surfaces recentCalls for the handle.

Known characteristics

  • First sync backfills available history (can be thousands of rows; rows are tiny).
  • ZUNIQUE_ID (or synthetic zpk:<Z_PK>) is the stable external id.
  • Multi-party FaceTime participants live in join tables (Z_2REMOTEPARTICIPANTHANDLES + ZHANDLE); v1 surfaces primary ZADDRESS only.
  • 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.