# Next Session: WhatsApp DM Polling for Annie

## What

Annie doesn't reply to WhatsApp DMs from Rajesh because the agent only monitors the "Holliday" group chat (DOM polling reads only the active chat). Fix: add a parallel DM poll loop using wa-js `WPP.chat.getMessages()` to read DMs without navigating away from the group. The plan survived 2 adversarial reviews — 4 CRITICAL + 4 HIGH + 4 MEDIUM findings, all addressed.

## Plan

`~/.claude/plans/stateless-nibbling-mango.md`

Read the plan first — it has the full implementation, all review findings with resolutions, state machines for concurrent workflows, and pre-mortem failure analysis.

## Key Design Decisions (from adversarial review)

1. **`WHATSAPP_DM_JID` env var** — Direct JID construction (e.g., `919876543210@c.us`) bypasses unreliable name-based resolution. Fallback to `resolve_jid()` if env var not set. (CRITICAL-2: contact name mismatch breaks send)
2. **3s timeout on `get_messages()`** — Not 10s or 30s. Reading from WPP's in-memory store should be near-instant. Limits page_lock hold time to avoid starving the group poll and send path. (CRITICAL-1: lock thundering herd)
3. **Single lock acquisition** — Combine JID resolve + chat warmup + getMessages in ONE `async with browser._page_lock` block. Eliminates TOCTOU window between operations. (MEDIUM-12)
4. **Module-level `_trigger_in_progress` flag** — Prevents both poll loops from calling `_check_trigger` concurrently. Added to existing `_check_trigger()` function with try/finally. (CRITICAL-3: cross-loop double-trigger)
5. **`_safe_format()` helper** — Replaces `RESPONSE_PROMPT.format(text=text)` with a `format_map(SafeDict)` that leaves unknown placeholders as-is. Fixes pre-existing format string injection bug. (MEDIUM-13)
6. **Ordered list, not frozenset** — DM seen IDs stored as `list[str]` with append order. Eviction takes `[-DM_SEEN_MAX:]` (most recent), not arbitrary frozenset iteration. (MEDIUM-10)
7. **DM loop increments `_messages_since_checkpoint` but never checkpoints** — Only the main `_poll_loop` does the threshold check and `ctx_mgr.checkpoint()`. Prevents double-checkpoint race. (MEDIUM-11)
8. **Chat warmup via `WPP.chat.find(jid)`** — Called once after JID resolution to force-load the DM chat in WhatsApp Web's internal store. Without this, `getMessages` may return an empty array for chats not visible in the sidebar. (HIGH-5)

## Files to Modify (ordered)

1. `services/whatsapp-agent/config.py` — Add `DM_POLL_INTERVAL_S`, `DM_POLL_COUNT`, `DM_TARGET_JID`, `DM_FETCH_TIMEOUT_S`
2. `services/whatsapp-agent/wa_js.py` — Add `get_messages()` (3s timeout), `find_chat()` (warmup)
3. `services/whatsapp-agent/wa_web.py` — Add `save_dm_seen_ids()`, `load_dm_seen_ids()` (ordered list)
4. `services/whatsapp-agent/responder.py` — Add `_safe_format()`, `DM_RESPONSE_PROMPT`, `is_dm` param
5. `services/whatsapp-agent/agent.py` — Add `_dm_poll_loop()`, `_trigger_in_progress` in `_check_trigger`, wire into `main()`, health endpoint
6. `services/whatsapp-agent/tests/test_wa_js.py` — Tests for `get_messages`, `find_chat`
7. `services/whatsapp-agent/tests/test_wa_web.py` — Tests for DM seen-ID persistence
8. `services/whatsapp-agent/tests/test_agent_poll.py` — Tests for DM poll loop, cross-loop guard
9. `services/whatsapp-agent/tests/test_responder.py` — Tests for `_safe_format`, DM prompt

## Pre-Implementation Checklist

```bash
# 1. Read the full plan:
cat ~/.claude/plans/stateless-nibbling-mango.md

# 2. Verify wa-js vendor bundle has getMessages:
grep -c 'getMessages' services/whatsapp-agent/vendor/wppconnect-wa.js  # expect ≥1

# 3. Check Rajesh's WhatsApp JID (phone number format):
# Set WHATSAPP_DM_JID=91XXXXXXXXXX@c.us in .env on Panda
```

## Start Command

```
cat ~/.claude/plans/stateless-nibbling-mango.md
```

Then implement the plan. All adversarial findings are already addressed in it. Start with Step 1 (config.py) and work through sequentially.

## Verification

1. Run tests: `cd services/whatsapp-agent && python -m pytest tests/ -v`
2. Add `WHATSAPP_DM_JID=91XXXXXXXXXX@c.us` to `.env` on Panda
3. Deploy: `./stop.sh whatsapp && ./start.sh whatsapp`
4. Check logs: `ssh panda "tail -50 /tmp/whatsapp-agent.log"` — look for `[DM_POLL] Chat warmed`
5. Send DM from Rajesh → verify response within 15s
6. Send group message → verify group still works (regression check)
7. Check health: `curl http://192.168.68.57:8780/v1/health | python3 -m json.tool` — verify `dm_poll.jid_resolved: true`

## All Findings Addressed

No rejected or deferred items. All 18 adversarial findings implemented in the plan, including:
- DM response cooldown (`DM_RESPONSE_COOLDOWN_S = 60`) to rate-limit rapid DMs
- Integration test `test_dm_full_flow` verifying end-to-end DM→trigger→response→send
- TODO code comments for future refactors: event-driven multi-chat, unified wa-js reading
