# Next Session: Phone Memory Cleanup + Quarantine-Based Purge

## Session Prompt

Pick up `docs/NEXT-SESSION-PHONE-MEMORY-CLEANUP.md`. Implement the plan at `~/.claude/plans/functional-scribbling-sphinx.md`. All research and adversarial review is done (26 findings, all addressed). Execute Phase A first (phone tools), then Phase B (quarantine purge), then Phase C (Moscow Coffee cleanup). No more planning needed — just build it.

## What happened

Annie hallucinated "Moscow Coffee" as a recent order during phone calls (2026-04-04 sessions `20260404-171309-2fdfce` and `20260404-172432-7c6048`). The hallucination propagated across 9 memory layers. The dashboard "Forget" button only cleans PostgreSQL — everything else persists. Phone calls lack delete_note/update_note tools so Rajesh can't correct Annie mid-call.

## Adversarial review completed

**26 findings, ALL implemented (0 rejected, 0 deferred).** Key design pivot: original cascading-delete design replaced with **quarantine pattern** after architecture review found 5 critical flaws (JSONL re-ingestion, Qdrant UUID5 mismatch, partial-success inconsistency, GIN index bypass, cross-process file race).

## Plan file

`~/.claude/plans/functional-scribbling-sphinx.md` — full plan with code snippets, file locations, line numbers.

## Implementation order

### Phase A: Phone Tools (~30 min)

**A1.** Add `delete_note` + `update_note` tool defs to `_PHONE_TOOL_DEFS` in `services/annie-voice/phone_tools.py` (after `read_notes` line 116, before `offload_task` line 117). Include `"minLength": 10` on `content_fragment` fields.

**A2.** Add 2 elif handlers in `execute_phone_tool()` (after `read_notes` line 272, before `offload_task` line 274). Call `memory_notes.delete_note()` and `memory_notes.update_note()`.

**A2b.** Add `_DELETE_INTENT_RE` guard for delete_note (same pattern as `_SAVE_INTENT_RE` at line 153). Match: forget, delete, remove, that's wrong, not true, incorrect, get rid of, erase.

**A2c.** Fix `_offload_count` thread safety — add `_offload_lock = asyncio.Lock()` protecting the counter in `_submit_offload_task` and `reset_offload_counter`.

**A2d.** Fix multi-match ambiguity in `memory_notes.py` `delete_note()` (line 166-192) — count matches first, return warning if >1 match instead of silently deleting first.

**A3.** Add ~10 tests to `tests/test_phone_tools_offload.py`.

**A4.** Verify: `cd services/annie-voice && python3 -m pytest tests/ -q` (expect 2646+).

### Phase B: Quarantine Purge API (~90 min)

**B1.** Schema migration: add `quarantined BOOLEAN DEFAULT FALSE` + partial indexes to `segments` and `entities` tables in `schema.sql`.

**B2.** Add `AND NOT quarantined` to all public search queries in `db.py` (search_segments, get_segments_by_session, search_entities, get_entities_clustered).

**B3.** Add `quarantine_by_pattern()` to `db.py` — two-step: tsquery search (GIN-indexed) then UPDATE. Also cleans entity_validations and ner_training_data arrays.

**B4.** Add 4 event types to `VALID_EVENT_TYPES` in `chronicler.py`: `entity_deleted`, `segment_deleted`, `purge_complete`, `quarantine`. **Deploy BEFORE the purge endpoint.**

**B5.** Build `DELETE /v1/purge` endpoint in `main.py` — validates text_pattern (5-200 chars, no regex metacharacters), calls quarantine_by_pattern, emits Chronicler event. Supports dry_run.

**B6.** Add event emission to existing `DELETE /v1/entities/{id}` endpoint (keep behavior unchanged, just add audit trail).

**B7.** Build `POST /v1/purge/finalize` endpoint — hard-deletes quarantined data across all stores (Qdrant via existing `delete_by_session()`, Neo4j metadata flag, JSONL line commenting, ingest offset reset, session scrub via annie-voice API).

**B8.** Build `POST /v1/phone/session/scrub` on annie-voice side (`server.py`) — scrubs both `summary` field AND matching messages in the `messages[]` array. Uses session_broker's atomic write.

**B9.** Add ~14 tests across context-engine test suite.

**B10.** Verify: `cd services/context-engine && python3 -m pytest tests/ -q` (expect 1336+).

### Phase C: Execute Moscow Coffee Cleanup

```bash
# Soft-delete (instant)
curl -X DELETE 'http://localhost:8100/v1/purge' \
  -H 'Content-Type: application/json' \
  -H "X-Internal-Token: $TOKEN" \
  -d '{"text_pattern": "Moscow Coffee"}'

# Verify
curl 'http://localhost:8100/v1/context?query=moscow+coffee&limit=5' -H "X-Internal-Token: $TOKEN"

# Hard cleanup
curl -X POST 'http://localhost:8100/v1/purge/finalize' -H "X-Internal-Token: $TOKEN"
```

### Deploy

1. Git commit + push
2. On Titan: `git pull && find . -name __pycache__ -exec rm -rf {} +`
3. Run schema migration (ALTER TABLE for quarantined columns)
4. From laptop: `./start.sh`
5. Execute Moscow Coffee cleanup curls above
6. Check dashboard timeline for purge_complete event

## Key design decisions (don't re-debate these)

- **Quarantine pattern over cascading delete** — single DB column, fully reversible, no cross-store coordination for instant fix
- **Two-step tsquery→update** instead of ILIKE — uses GIN index, avoids seqscan
- **Session scrub via annie-voice API** (`POST /v1/phone/session/scrub`), NOT direct file write from context-engine — avoids cross-process file race
- **`_DELETE_INTENT_RE`** on phone delete_note — deletion is irreversible, guard matches correction-intent phrases
- **Multi-match ambiguity guard** in memory_notes.delete_note — warns when >1 note matches instead of silent first-match delete
- **Keep `DELETE /v1/entities/{id}` behavior unchanged** — add audit event only, no silent cascade expansion. Use `/v1/purge` for full cleanup
- **Neo4j: metadata quarantine flag**, not DETACH DELETE — avoids corrupting graph topology

## Files to modify (11 total)

| File | What |
|------|------|
| `services/annie-voice/phone_tools.py` | +2 tool defs, +2 handlers, +_DELETE_INTENT_RE, +_offload_lock |
| `services/annie-voice/memory_notes.py` | Multi-match ambiguity guard in delete_note() |
| `services/annie-voice/tests/test_phone_tools_offload.py` | +10 tests |
| `services/annie-voice/server.py` | +POST /v1/phone/session/scrub |
| `services/context-engine/schema.sql` | +quarantined column + partial indexes |
| `services/context-engine/db.py` | +quarantine_by_pattern(), +AND NOT quarantined in searches |
| `services/context-engine/main.py` | +DELETE /v1/purge, +POST /v1/purge/finalize, +audit events |
| `services/context-engine/chronicler.py` | +4 event types |
| `services/context-engine/graphiti_client.py` | +quarantine_graph_nodes() for hard cleanup |
| `services/context-engine/tests/conftest.py` | +MockDB methods |
| `services/context-engine/tests/test_main.py` | +14 tests |

## Tests (current baselines)

- annie-voice: 2636 passing → expect 2646+
- context-engine: 1322 passing → expect 1336+

## Rules (from memory)

- NEVER rsync to Titan — git commit/push/pull only
- NEVER manually start/stop services — use start.sh/stop.sh from laptop
- Clear __pycache__ after git pull on Titan
- No deferrals — implement every adversarial finding
