# 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 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](./known-limitations.md#icalls-limitations-mac-callhistory-only)
  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.
