# Family Assistant v1.5 — Scope Document **Status:** Draft **Date:** 2026-04-21 **Owner:** Socrates (Backend) + Daedalus (Frontend) **Approver:** Matt (pending) **Target Release:** Post v1.0 stabilization --- ## Executive Summary Family Assistant v1.0 is feature-frozen and operational. v1.5 addresses **trust and reliability gaps** identified in the April 2026 audit — human error recovery, persistent state management, and graceful degradation. This document follows contract-first protocol: API specs are binding on backend; UI/UX requirements are binding on frontend. --- ## Goals | Priority | Goal | Success Metric | |----------|------|----------------| | P0 | Eliminate silent data loss scenarios | User can undo any calendar mutation within 5 minutes | | P0 | Make conflicts actionable | Conflicts don't disappear after first alert; nag until resolved | | P1 | Ensure Family Brain availability when Gaming PC sleeps | Degrade to keyword search; no hard failures | | P1 | Give Aundrea phone-less calendar access | Web dashboard with read-only family calendar view | | P2 | Improve newsletter signal-to-noise | Formatted digest, not raw item dumps | | P2 | Proactive travel notifications | "Leave in 15min" alerts based on cached travel times | --- ## Features ### 1. Undo Stack (P0) **Problem:** Intent engine mutations (move, cancel, rename) are destructive and irreversible. Aundrea has no "oops" path. **Solution:** Persist last 5 operations with reversible snapshots. #### API Contract (Socrates) ```yaml # POST /api/v1/undo # Authorization: Bearer token (same auth as webhook) # Request: { "action": "list" | "undo" | "redo", "operation_id": "uuid" # required for undo/redo } # Response (list): { "operations": [ { "id": "uuid", "type": "move" | "cancel" | "rename" | "add", "summary": "Sullivan Soccer", "executed_at": "2026-04-21T14:30:00-05:00", "reversible_until": "2026-04-21T14:35:00-05:00", # 5 min TTL "can_undo": true, "snapshot": { "event_id": "caldav-uuid", "previous_state": { ... }, # full iCal VEVENT snapshot "new_state": { ... } } } ] } # Response (undo): { "status": "undone" | "expired" | "not_found", "restored_event": { ... }, "message": "Sullivan Soccer restored to original time" } ``` **Storage:** `~/.family_assistant/undo_stack.jsonl` — append-only, capped at 5 entries, 5-min TTL per entry. **Security:** Operations reversible only by same Telegram user who initiated (check `sender_id`). #### Daedalus Requirements - **Telegram Integration:** Add "↩️ Undo" button to Hermes confirmation messages for 5-minute window - **Visual State:** Show "Recently modified" list in dashboard with undo affordance - **UX Pattern:** Toast notification style: "Moved Sullivan Soccer to Wednesday [Undo]" --- ### 2. Persistent Conflict Registry (P0) **Problem:** Conflict alerts fire once and disappear. No persistent "outstanding conflicts" list. **Solution:** SQLite-backed conflict registry with nag escalation. #### API Contract (Socrates) ```yaml # GET /api/v1/conflicts { "outstanding": [ { "id": "uuid", "detected_at": "2026-04-21T08:00:00-05:00", "event1": { "summary": "Sullivan Soccer", "start": "...", "end": "..." }, "event2": { "summary": "Harper Dance", "start": "...", "end": "..." }, "overlap_minutes": 30, "status": "unacknowledged" | "acknowledged" | "resolved", "nag_count": 2, # incremented on each heartbeat if unacknowledged "resolution": null | { "action": "split", "choice": 1, "executed_at": "..." } } ] } # POST /api/v1/conflicts/{id}/acknowledge { "status": "acknowledged" } # Stops nagging, keeps in registry # POST /api/v1/conflicts/{id}/resolve { "resolution_action": "split" | "reassign" | "reschedule", "choice_index": 0, # which option was selected "executed": true | false # whether calendar was actually modified } ``` **Storage:** SQLite at `~/.family_assistant/conflicts.db` — single table, indexed on `status` and `detected_at`. **Nag Logic:** - Heartbeat:check runs every 30m - If `unacknowledged` and `nag_count < 3`: send alert to family group - If `nag_count >= 3`: escalate to DM with "⚠️ UNRESOLVED CONFLICT" urgency #### Daedalus Requirements - **Conflict Badge:** Dashboard shows conflict count in header - **Conflict Detail View:** Side-by-side event comparison with resolution options - **Mobile Optimization:** Swipe-to-acknowledge, tap-to-resolve pattern - **Visual Priority:** Red accent for unacknowledged, yellow for acknowledged, green for resolved --- ### 3. Brain Offline Fallback (P1) **Problem:** If Gaming PC sleeps or Tailscale is down, `nomic-embed-text` fails. Family Brain throws exception. **Solution:** Try embeddings → if fail, fall back to keyword search against raw email chunks. #### API Contract (Socrates) ```yaml # POST /api/v1/brain/query { "question": "What do the kids need for the field trip?", "force_keyword": false # optional: bypass embeddings entirely } # Response: { "answer": "...", "mode": "semantic" | "keyword_fallback", # transparency to user "sources": [...], "confidence": "high" | "medium" | "low", "embeddings_available": true | false } ``` **Fallback Logic:** 1. Try `nomic-embed-text` on Gaming PC (timeout: 5s) 2. If timeout/error: search ChromaDB metadata (subject, sender, date) with keyword matching 3. If still no results: search raw email body text (cached in `~/.family_assistant/email_cache/`) **Health Check:** Add `embeddings_reachable` to `/health` endpoint. #### Daedalus Requirements - **Mode Indicator:** Show subtle "offline mode" badge when using keyword fallback - **Confidence Display:** Lower confidence → suggest rephrasing question - **No User Action Required:** Should be transparent, but honesty about quality is important --- ### 4. Family Dashboard — Read-Only Calendar View (P1) **Problem:** Aundrea doesn't want to install CalDAV clients. Needs phone-less access to family calendar. **Solution:** Web dashboard served from hoffdesk-api, read-only CalDAV view. #### API Contract (Socrates) ```yaml # GET /api/v1/calendar/events # Query params: # start=2026-04-01T00:00:00-05:00 # end=2026-04-30T23:59:59-05:00 # include_travel=true # enrich with travel time from location cache { "events": [ { "id": "caldav-uuid", "summary": "Sullivan Soccer", "start": "2026-04-21T15:00:00-05:00", "end": "2026-04-21T16:00:00-05:00", "location": "Golrusk Pet Center", "travel_time_minutes": 12, "travel_from_home": true, "status": "confirmed", "who": ["Sullivan"], "color": "#3b82f6" # auto-assigned per-person } ], "conflicts": [ // include outstanding conflicts in same response { "id": "...", "event1_id": "...", "event2_id": "...", "overlap_minutes": 30 } ] } # GET /api/v1/calendar/today # Shortcut for today's events # GET /api/v1/calendar/week # Query param: week_offset=0 (current), -1, +1, etc. ``` **Caching:** 5-minute cache on Radicale reads to reduce load. **Auth:** Same `TELEGRAM_BOT_TOKEN` validation as webhook (token in header, IP allowlist optional). #### Daedalus Requirements - **Calendar Views:** Day, week, month (month optional for MVP) - **Week View Default:** Family planning happens at weekly granularity - **Person Colors:** Sullivan = blue, Harper = pink, Family = green, etc. (see design-tokens/) - **Conflict Highlighting:** Red stripe on conflicting events - **Travel Time:** Small text under location: "12 min from home" - **Mobile-First:** Touch-friendly, swipe between days/weeks - **No Login Required:** Shareable URL with token (time-limited, revokeable) --- ### 5. Newsletter Digest v2 (P2) **Problem:** Info items dump raw text to Telegram. Low signal-to-noise. **Solution:** LLM-summarized digest with categories. #### API Contract (Socrates) ```yaml # POST /api/v1/digest/generate (internal, called by pipeline) { "items": [...], // raw items from newsletter parser "style": "brief" | "detailed" // brief for daily, detailed for weekly } # Response: { "sections": [ { "category": "School", "icon": "🎒", "items": [ { "summary": "Field trip permission due Friday", "who": ["Sullivan"], "action_required": true, "deadline": "2026-04-25" } ] }, { "category": "Sports", "icon": "⚽", "items": [...] } ], "markdown": "🎒 **School**\n• Field trip...\n\n⚽ **Sports**..." // formatted for Telegram } ``` **Categories:** School, Sports, Medical, Community, Other (configurable in `family.yaml`) **Prompt:** Single LLM call with category definitions, returns structured JSON. #### Daedalus Requirements - **Digest View:** Dashboard card with collapsible sections - **Category Icons:** Consistent emoji/icon mapping (design-tokens/) - **Action Badges:** "Needs action" indicator on items with deadlines - **Mark Done:** Inline button to dismiss (triggers intent engine) --- ### 6. Proactive Travel Notifications (P2 — Deferred to v2.0) **Status:** Deferred — requires sub-30-minute precision, out of scope for v1.5 heartbeat-based system. **Note:** Travel time data remains in calendar API responses (`travel_time_minutes`) for UI display. --- ## Data Model Changes ### New Tables/Files | Resource | Location | Schema | Purpose | |----------|----------|--------|---------| | `undo_stack` | `~/.family_assistant/undo_stack.jsonl` | JSON Lines, max 5 entries, 5min TTL | Reversible operations | | `conflicts` | `~/.family_assistant/conflicts.db` | SQLite: `id, event1_id, event2_id, detected_at, status, nag_count, resolution` | Persistent conflict registry | | `notification_state` | `~/.family_assistant/notification_state.json` | JSON: `leave_by_notified: {event_id: timestamp}` | Deduplication for travel alerts | ### API Endpoints Summary | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/api/v1/undo` | Bearer | List/undo/redo operations | | GET | `/api/v1/conflicts` | Bearer | List outstanding conflicts | | POST | `/api/v1/conflicts/{id}/ack` | Bearer | Acknowledge conflict | | POST | `/api/v1/conflicts/{id}/resolve` | Bearer | Resolve with action | | POST | `/api/v1/brain/query` | Bearer | Query with fallback mode | | GET | `/api/v1/calendar/events` | Bearer | Read-only events | | GET | `/api/v1/calendar/today` | Bearer | Today's events | | GET | `/api/v1/calendar/week` | Bearer | Weekly view | | POST | `/api/v1/digest/generate` | Internal | Newsletter summarization | | GET | `/api/v1/notifications/leave-by` | Internal | Travel alert candidates | | POST | `/api/v1/notifications/mark-sent` | Internal | Mark travel alert sent | --- ## Daedalus Deliverables ### UI Components Required 1. **Undo Toast** — Floating notification with countdown timer 2. **Conflict Badge** — Header indicator with count 3. **Conflict Detail Panel** — Side-by-side event comparison 4. **Calendar Week View** — Grid layout with person colors 5. **Calendar Day View** — Timeline with travel markers 6. **Digest Card** — Collapsible category sections 7. **Offline Badge** — Subtle mode indicator ### Design Token Requirements | Token | Value | Usage | |-------|-------|-------| | `--color-person-sullivan` | `#3b82f6` (blue-500) | Sullivan events | | `--color-person-harper` | `#ec4899` (pink-500) | Harper events | | `--color-person-family` | `#22c55e` (green-500) | Family events | | `--color-conflict-unack` | `#ef4444` (red-500) | Unacknowledged conflicts | | `--color-conflict-ack` | `#eab308` (yellow-500) | Acknowledged conflicts | | `--color-travel-marker` | `#6b7280` (gray-500) | Leave-by indicators | | `--undo-toast-ttl` | `600s` | Undo button visibility duration (10 min) | ### Shared Assets Place in `shared/design-tokens/family-assistant/`: - `colors.json` — Person assignments, conflict states - `icons.json` — Category icons for digest - `spacing.json` — Calendar grid dimensions --- ## Socrates Deliverables ### Backend Modules | Module | Purpose | |--------|---------| | `undo_stack.py` | Persistent operation log with snapshots | | `conflict_registry.py` | SQLite CRUD for conflicts | | `brain_fallback.py` | Keyword search when embeddings fail | | `travel_alerts.py` | Leave-by calculation and notification | **Deferred to v2.0** | | `digest_generator.py` | LLM-powered newsletter summarization | ### Cron/Heartbeat Changes - `heartbeat:check`: Add conflict nag escalation (travel alert scanning deferred to v2.0) - `family:daily-brief`: Include digest v2 format ### Prompts Required - `digest_summarize.txt` — Single-shot LLM prompt for categorizing newsletter items --- ## Dependencies | Dependency | Status | Notes | |------------|--------|-------| | Radicale CalDAV | ✅ Ready | v1.5 uses existing connection | | ChromaDB | ✅ Ready | No schema changes | | SQLite | ✅ Ready | Part of Python stdlib | | Daedalus Dashboard | 🔄 Blocked | Awaiting design tokens | | Gaming PC Online | ⚠️ Partial | Fallback mode handles offline | --- ## Testing Strategy ### Unit Tests (Socrates) - Undo/redo cycle with TTL expiration - Conflict registry CRUD with concurrent access - Brain fallback accuracy vs. semantic search - Travel time calculation edge cases (DST, traffic) ### Integration Tests - End-to-end: Intent → Calendar → Undo → Restore - Conflict detection → Registry → Nag → Resolution ### UI Tests (Daedalus) - Undo toast visibility and interaction - Conflict badge state transitions - Calendar responsive breakpoints --- ## Security Considerations 1. **Undo Authorization:** Only original `sender_id` can undo their own operations. **TTL: 10 minutes** (resolved 2026-04-21). 2. **Conflict Data:** No PII in registry beyond event summaries (already in CalDAV). Nag escalation: **Matt DM → family group if EOD unaddressed** (resolved 2026-04-21). 3. **Dashboard Auth:** **Passwordless token link** per standard dashboard protocols. Time-limited, revokeable, IP allowlist optional (resolved 2026-04-21). 4. **Travel Alerts:** Deferred to v2.0. --- ## Decisions (Resolved 2026-04-21) | # | Decision | Choice | Rationale | |---|----------|--------|-----------| | 1 | Undo TTL | **10 minutes** | Safer for 7 AM family chaos | | 2 | Conflict Nag | **Matt DM after 3 alerts** | Escalate to group chat if unaddressed by end of day | | 3 | Dashboard Auth | **Passwordless token link** | Standard dashboard access protocols, sovereign, no account mgmt | | 4 | Travel Buffer | **Fixed 5 minutes** | Per-type config deferred to v2.0; keeping cron/heartbeat ≤ 30 min | ## Open Questions _None — all decisions resolved by Matt on 2026-04-21._ --- ## Appendix: API Spec Location Machine-readable OpenAPI spec: `shared/api-specs/family-assistant-v1.5.yaml` (to be written after scope approval) --- *Document version: 1.0-draft* *Last updated: 2026-04-21T21:34:00Z*