feat: configurable daily recap depth (Brief/Normal/Deep)

Merged
AR
11d ago
+649-34

What changed and why

Closes #6137. Daily recaps were hard-coded to the short “be snappy” prompt. Users can now choose Brief, Normal, or Deep Reflection in macOS Settings → Alerts & Privacy → Daily Summary; the preference is stored on their account and used when the next recap is generated.

  • Brief preserves the existing limits and is the default for older or malformed profiles.
  • Normal allows up to eight useful items per section with context and related themes.
  • Deep removes fixed item counts and uses bounded context from up to three earlier recaps and three active goals. It asks for grounded connections and never invents emotional states. The optional context reads fail soft with fallback telemetry.
  • The GET/PATCH settings contract remains compatible with older callers. A depth PATCH echoes the saved value; the desktop picker reverts with an error if an older backend ignores it, instead of falsely appearing saved. The recap output JSON schema is unchanged and existing recaps are not rewritten. The app-client OpenAPI snapshot and generated Dart/TypeScript API clients are updated mechanically; a kind: none mobile fragment records that this is not a mobile feature.

Backend must deploy before the desktop picker is released. This PR does not deploy either service.

Product invariants affected

none (the new control uses the existing settings row and menu components; the desktop UX contract and brand ratchets pass).

How it was verified

  • backend/.venv/bin/python -m pytest backend/tests/unit/test_daily_summary_*.py backend/tests/unit/test_daily_notification_*.py backend/tests/unit/test_notifications_daily_summary_null_defaults.py backend/tests/unit/test_backfill_daily_summary_schedule_fields.py backend/tests/unit/test_users_error_sanitization.py -q → 202 passed on the rebased branch. Includes a real FastAPI TestClient GET → PATCH → GET round trip and captured production-generator prompts for each depth.
  • ./scripts/dev-feedback.py --once swift 'APIClientRoutingTests' → 72 passed on the rebased branch. Covers PATCH wire body, backend acknowledgement, legacy response rejection, and GET decoding fallback.
  • OMI_PR_BODY_FILE=/tmp/omi-6137-pr-body.md make preflight → 46 checks passed after rebasing onto current origin/main (including the scoped line-count exception below).
  • Generated OpenAPI, Dart users DTO, and all four TypeScript API clients pass their freshness checks. web/app tests pass 601/601 with NODE_OPTIONS=--no-experimental-webstorage to avoid a Node 26/jsdom global localStorage collision in this environment.
  • Built and launched isolated com.omi.omi-6137-recap-depth on macOS. The named bundle passed dependency audit, loaded signed-in Settings, and the accessibility tree exposed the picker. I opened its menu and visually confirmed all three options without changing the live account preference. Visual proof.
  • No production LLM call or live account-setting mutation was made. Generated recap quality still needs human review on a development backend after deployment.

Tests

  • test_daily_summary_depth_settings.py tests HTTP persistence and invalid values.
  • test_daily_summary_depth_prompt.py tests the real generator prompt, bounded prior context, non-Deep read avoidance, and optional-read degradation.
  • Firestore schedule-field tests prove changing depth preserves the existing daily-summary schedule.
  • APIClientRoutingTests covers desktop wire compatibility and failure presentation preconditions.

Failure class (fixes)

Failure-Class: none

Scoped cleanups

The existing daily-summary PATCH returned {status: "ok"} while its Swift client attempted to decode the GET response shape; this change corrects that contract. An unrelated users error-sanitization test gained the missing quota mock so the broader hermetic suite does not attempt a metadata-network call.

Line-Count-Exception: backend/routers/users.py | 2525 -> 2541 | reason: extend the existing daily-summary settings endpoint in place; splitting this large router as part of a scoped account-preference feature would add unrelated routing churn.