# Next Session: Fix WhatsApp Web Poll — Seen-Hashes Window

## What

Replace the broken `last_seen_id` (single content hash) poll tracker with a **seen-hashes window** (frozenset of 50 hashes) + **scroll-to-bottom** before each poll. The current tracker breaks when outgoing messages shift WhatsApp Web's virtual scroll viewport, causing the single hash to disappear from the DOM. The seen-hashes approach provides 50 anchors instead of 1, and stale checkpoint detection prevents mass replay on restart.

## Plan

`~/.claude/plans/glittery-nibbling-honey.md`

Read the plan first — it has the full implementation, all 20 adversarial review findings with resolutions, state machine diagram, pre-mortem analysis, and code snippets for every step.

## Key Design Decisions (from adversarial review)

1. **Scroll uses `[role="application"]` selector** — NOT a heuristic div scan. WhatsApp Web's message list has `role="application"`. Fallback to `#main.scrollTop`. The div-scan heuristic matched wrong elements (emoji picker, GIF panel).

2. **Scroll wait: 50ms, not 300ms** — `scrollTo()` is synchronous. 300ms starved the send flow by holding `page_lock` too long. Total lock time: ~100-150ms.

3. **v1 migration → first-run catch-up** — Old `{"id": "hash"}` format used non-normalized timestamps, so the hash won't match anything. Return empty frozenset → catch-up skips all visible messages. One 5s cycle delay is better than mass replay.

4. **Stale checkpoint detection** — If ZERO current DOM hashes appear in the seen set (e.g., restart after hours), treat as first-run catch-up. Prevents replaying 25+ messages.

5. **Timestamp normalization to HH:MM** — Makes hashes stable across WhatsApp Web's two timestamp formats. Same-minute collision risk is acceptable (false dedup < false replay).

6. **Health endpoint: `seen_hash_count` (int)** — Not the frozenset itself. frozenset is not JSON-serializable.

7. **Delete old `save_last_seen`/`load_last_seen`** — Dead code removal. Update imports.

## Files to Modify

1. `services/whatsapp-agent/wa_web.py` — Add `_scroll_to_bottom()`, `_normalize_timestamp()`. Replace persistence (seen hashes). Rewrite `poll_new_messages()`.
2. `services/whatsapp-agent/agent.py` — `_last_seen_id` → `_seen_hashes`, import update, poll loop, health endpoint.
3. `services/whatsapp-agent/tests/test_wa_web.py` — `TestSeenHashes`, updated poll tests, new tests (scroll, normalize, v1 migration, stale checkpoint, cap).
4. `services/whatsapp-agent/tests/test_agent_poll.py` — Mock return types, patch targets, health assertions.

## Start Command

```bash
# Read the reviewed plan
cat ~/.claude/plans/glittery-nibbling-honey.md

# Then implement the plan. All adversarial findings are already addressed in it.
```

## Verification

1. `cd services/whatsapp-agent && python -m pytest tests/ -v`
2. `git add services/whatsapp-agent/ && git commit -m "fix: replace last_seen_id with seen-hashes window for robust poll tracking" && git push`
3. `ssh panda "cd ~/workplace/her/her-os && git pull && find services/whatsapp-agent -name '__pycache__' -exec rm -rf {} + 2>/dev/null; true"`
4. `./stop.sh whatsapp && ./start.sh whatsapp`
5. Wait for "Navigated to chat 'Rajesh'"
6. Send from Pixel: "What's the weather like?" → Annie replies
7. Send 2nd message → verify no replay of 1st
8. `curl -s http://panda:8780/v1/health | python3 -m json.tool` → `seen_hash_count > 0`
