# Next Session: Contact Book — Hybrid JSON + ADB, LLM-Driven Name Extraction

## What was decided (Session 422)

3 rounds of adversarial review (54 total findings) refined the architecture for Annie's contact lookup:

1. **LLM tool parameter** — add `callee_name` to `MakePhoneCallInput` schema. The LLM extracts "Roshni" from "call my wife before dinner" directly. NO regex extraction. Eliminates all overcapture bugs.

2. **Hybrid JSON + ADB** — `~/.her-os/contacts.json` on Panda is the fast primary source. Pixel ADB query is the fallback for contacts added on the phone. JSON is editable, auditable, and zero-latency.

3. **Privacy rule** — Numbers only spoken on voice, never in Telegram/logs/git. `_mask_number()` on all log paths. No `/contacts/list` endpoint.

## Plan file

**`~/.claude/plans/imperative-cooking-otter.md`** — COMPLETE, ready to implement. 7 phases, all adversarial findings addressed.

## Architecture (final)

```
User: "call my wife before dinner"
  → LLM invokes make_phone_call(callee_name="Roshni")  ← LLM resolves alias
  → Titan text_llm.py: GET /v1/phone/contacts/lookup?name=Roshni
  → Panda phone_api.py:
      1. Check ~/.her-os/contacts.json (mtime-based hot-reload)
      2. _find_contact("Roshni") → alias/exact/prefix/fuzzy match
      3. If not found: fallback to ADB query on Pixel (cached, 30s error cooldown)
  → Return {"name": "Roshni", "number": "+91..."}
  → Titan POSTs to /v1/phone/call with resolved number
  → Pixel dials
```

## Implementation phases (from plan)

1. **Tool schema** — Add `callee_name` to `MakePhoneCallInput` in `tool_schemas.py`
2. **phone_api.py** — JSON loader + ADB fallback + `_find_contact()` + `/v1/phone/contacts/lookup` endpoint + privacy fixes
3. **phone_adb.py** — `_mask_number()` + `query_contacts()` + `insert_contact()` + ADB row parser
4. **text_llm.py** — Delete regex extraction, new HTTP `_lookup_contact()`, rewrite `_dispatch_make_phone_call` handler
5. **Tests** — ~40 new tests across 3 test files, delete 8 old regex tests
6. **contacts.json** — Create on Panda + optional `seed_contacts.py` for Pixel
7. **Deploy** — Panda first (backward-compatible), then Titan

## Key files to modify

| File | What changes |
|------|-------------|
| `services/annie-voice/tool_schemas.py` | Add `callee_name: str` field |
| `services/annie-voice/phone_adb.py` | Add `_mask_number()`, `query_contacts()`, `insert_contact()` |
| `scripts/phone_api.py` | JSON loader, ADB fallback, `_find_contact()`, lookup endpoint, privacy fixes (lines 74, 78, 89-91, 109, 110) |
| `services/annie-voice/text_llm.py` | Delete lines 583-590 (regex), 593-609 (old lookup), 647 (duplicate extraction). New `_lookup_contact()`, rewrite handler. |
| `services/annie-voice/tests/test_make_phone_call.py` | Delete `TestContactLookup` + `TestHandlerContactLookup`, add HTTP lookup + handler tests |
| `scripts/seed_contacts.py` | NEW — reads `~/.her-os/contacts.json`, seeds Pixel via ADB |
| `services/annie-voice/tests/test_phone_adb_contacts.py` | NEW — mask, query, insert, parse tests |
| `scripts/tests/test_phone_api_contacts.py` | NEW — find_contact, JSON load, ADB fallback, endpoint, privacy tests |

## contacts.json format (create on Panda at ~/.her-os/contacts.json, chmod 600)

```json
[
  {"name": "Rajesh",  "aliases": [],                                    "number": "+917899218911"},
  {"name": "Roshni",  "aliases": ["wife", "my wife", "rosh"],           "number": "+917899643116"},
  {"name": "Reethi",  "aliases": ["daughter", "my daughter", "reethu"], "number": "+917899488319"},
  {"name": "Roshan",  "aliases": ["son", "my son"],                     "number": "+917899645116"}
]
```

## Critical design decisions from adversarial review

- **No regex** — LLM fills `callee_name` via tool schema. "call my wife before dinner" works because the LLM understands context.
- **Lazy asyncio.Lock** — initialized inside `_get_adb_contacts()`, not at module level (avoids event-loop-before-Lock crash).
- **30s ADB error cooldown** — prevents repeated ADB timeouts from blocking the event loop.
- **ADB row parser** — anchors on `key=` boundaries, not commas (handles "Kumar, Raj" correctly).
- **difflib cutoff=0.7** — raised from 0.6 to reduce false positives.
- **Server-side escaping** — angle brackets escaped in `phone_api.py` handle_call (defense-in-depth).
- **httpx reconnect** — `_reset_panda_client()` on ConnectError, prevents permanent stale connection.
- **Phase 4a + 5c deletions in same commit** — prevents test import failures.

## Start command

```bash
# Read the full plan:
cat ~/.claude/plans/imperative-cooking-otter.md

# Then implement phase by phase, TDD style (tests first).
```
