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
2026-06-13 14:29:34 -04:00
parent b30c8c16d0
commit f707353807
26 changed files with 1825 additions and 153 deletions
+9 -9
View File
@@ -275,14 +275,14 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
// Resolve the optional library filter. Unknown values return 400. A
// `None` result means "union across all libraries" and downstream
// walks iterate every configured library root.
let library = match crate::libraries::resolve_library_param(&app_state, req.library.as_deref())
{
Ok(lib) => lib,
Err(msg) => {
log::warn!("Rejecting /photos request: {}", msg);
return HttpResponse::BadRequest().body(msg);
}
};
let library =
match crate::libraries::resolve_library_param_state(&app_state, req.library.as_deref()) {
Ok(lib) => lib,
Err(msg) => {
log::warn!("Rejecting /photos request: {}", msg);
return HttpResponse::BadRequest().body(msg);
}
};
let span_context = opentelemetry::Context::current_with_span(span);
@@ -1238,7 +1238,7 @@ pub async fn list_exif_summary(
// Resolve the library filter up front so a bad id/name 400s before we
// ever take the DAO mutex. None == union across all libraries.
let library_filter =
match crate::libraries::resolve_library_param(&app_state, req.library.as_deref()) {
match crate::libraries::resolve_library_param_state(&app_state, req.library.as_deref()) {
Ok(lib) => lib.map(|l| l.id),
Err(msg) => {
span.set_status(Status::error(msg.clone()));