feat: nightly agentic pre-generation of memory reels

Implement end-to-end nightly pre-generation of memory reels with agentic
scripting that grounds narration in calendar, location, messages, and RAG.

Sections A-E from the plan:

A. Extract produce_reel pipeline core from run_reel_job with
   ScripterMode::Fast/Agentic and progress callbacks.

B. Agentic scripter: factor run_readonly_tool_loop from the insight
   generator, build read-only tool gate, prompt builder with GPS, and
   generate_script_agentic with fallback to fast path.

C. Precomputed reels ledger (SQLite table + DAO), GET /reels/precomputed
   handler with validity gate, GET /reels/by-key/{key}/video streaming,
   and normalize_library_key helper.

D. Nightly scheduler: spawn_pregen_scheduler with configurable hour,
   run_pregen_batch (day/week/month spans), pregen_one with dedup and
   disk-check, secs_until_next_run_hour time math.

E. user_ai_prefs passive mirror table + DAO for param capture in
   create_reel_handler and replay in the scheduler.

Also fixes resolve_library_param signature to take &[Library] and adds
resolve_library_param_state wrapper for AppState callers.

New files: migrations/2026-06-13-000000_add_precomputed_reels/,
  migrations/2026-06-13-000010_add_user_ai_prefs/,
  src/database/precomputed_reel_dao.rs,
  src/database/user_ai_prefs_dao.rs
This commit is contained in:
Cameron Cordes committed 2026-06-13 14:29:34 -04:00
1 parent b30c8c16d0
commit f707353807
26 files changed
+1825 -153

No files matched your search

+10 -9
View File
@@ -1755,7 +1755,7 @@ async fn stats_handler<D: FaceDao>(
let span = global_tracer().start_with_context("faces.stats", &context);
let span_context = opentelemetry::Context::current_with_span(span);
let library_id = libraries::resolve_library_param(&app_state, query.library.as_deref())
let library_id = libraries::resolve_library_param_state(&app_state, query.library.as_deref())
.ok()
.flatten()
.map(|l| l.id);
@@ -1782,11 +1782,12 @@ async fn list_faces_handler<D: FaceDao>(
let normalized_path = normalize_path(&query.path);
// resolve_library_param returns Option<&Library>; clone so the result
// is owned (matching the primary_library fallback's type).
let library: Library = libraries::resolve_library_param(&app_state, query.library.as_deref())
.ok()
.flatten()
.cloned()
.unwrap_or_else(|| app_state.primary_library().clone());
let library: Library =
libraries::resolve_library_param_state(&app_state, query.library.as_deref())
.ok()
.flatten()
.cloned()
.unwrap_or_else(|| app_state.primary_library().clone());
let mut dao = face_dao.lock().expect("face dao lock");
let hash = match dao.resolve_content_hash(&span_context, library.id, &normalized_path) {
@@ -1870,7 +1871,7 @@ async fn create_face_handler<D: FaceDao>(
}
let normalized_path = normalize_path(&body.path);
let library: Library = match libraries::resolve_library_param(
let library: Library = match libraries::resolve_library_param_state(
&app_state,
body.library.as_ref().map(|i| i.to_string()).as_deref(),
) {
@@ -2192,7 +2193,7 @@ async fn list_persons_handler<D: FaceDao>(
let span = global_tracer().start_with_context("persons.list", &context);
let span_context = opentelemetry::Context::current_with_span(span);
let library_id = libraries::resolve_library_param(&app_state, query.library.as_deref())
let library_id = libraries::resolve_library_param_state(&app_state, query.library.as_deref())
.ok()
.flatten()
.map(|l| l.id);
@@ -2345,7 +2346,7 @@ async fn person_faces_handler<D: FaceDao>(
let context = extract_context_from_request(&request);
let span = global_tracer().start_with_context("persons.faces", &context);
let span_context = opentelemetry::Context::current_with_span(span);
let library_id = libraries::resolve_library_param(&app_state, query.library.as_deref())
let library_id = libraries::resolve_library_param_state(&app_state, query.library.as_deref())
.ok()
.flatten()
.map(|l| l.id);