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

+9 -7
View File
@@ -53,7 +53,7 @@ pub async fn get_image(
// Resolve library from query param; default to primary so clients that
// don't yet send `library=` continue to work.
let library = match libraries::resolve_library_param(&app_state, req.library.as_deref()) {
let library = match libraries::resolve_library_param_state(&app_state, req.library.as_deref()) {
Ok(Some(lib)) => lib,
Ok(None) => app_state.primary_library(),
Err(msg) => {
@@ -492,7 +492,7 @@ pub async fn get_file_metadata(
let span_context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
let library = libraries::resolve_library_param(&app_state, path.library.as_deref())
let library = libraries::resolve_library_param_state(&app_state, path.library.as_deref())
.ok()
.flatten()
.unwrap_or_else(|| app_state.primary_library());
@@ -580,7 +580,7 @@ pub async fn set_image_gps(
let span_context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
let library = libraries::resolve_library_param(&app_state, body.library.as_deref())
let library = libraries::resolve_library_param_state(&app_state, body.library.as_deref())
.ok()
.flatten()
.unwrap_or_else(|| app_state.primary_library());
@@ -746,7 +746,7 @@ pub async fn get_full_exif(
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("get_full_exif", &context);
let library = libraries::resolve_library_param(&app_state, path.library.as_deref())
let library = libraries::resolve_library_param_state(&app_state, path.library.as_deref())
.ok()
.flatten()
.unwrap_or_else(|| app_state.primary_library());
@@ -888,7 +888,8 @@ pub async fn set_image_date(
let span_context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
let library = match libraries::resolve_library_param(&app_state, body.library.as_deref()) {
let library = match libraries::resolve_library_param_state(&app_state, body.library.as_deref())
{
Ok(Some(lib)) => lib,
Ok(None) => app_state.primary_library(),
Err(msg) => {
@@ -941,7 +942,8 @@ pub async fn clear_image_date(
let span_context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
let library = match libraries::resolve_library_param(&app_state, body.library.as_deref()) {
let library = match libraries::resolve_library_param_state(&app_state, body.library.as_deref())
{
Ok(Some(lib)) => lib,
Ok(None) => app_state.primary_library(),
Err(msg) => {
@@ -1001,7 +1003,7 @@ pub async fn upload_image(
// Resolve the optional library selector. Absent → primary library
// (backwards-compatible with clients that don't yet send `library=`).
let target_library =
match libraries::resolve_library_param(&app_state, query.library.as_deref()) {
match libraries::resolve_library_param_state(&app_state, query.library.as_deref()) {
Ok(Some(lib)) => lib,
Ok(None) => app_state.primary_library(),
Err(msg) => {
+5 -4
View File
@@ -67,10 +67,11 @@ pub async fn generate_video(
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("generate_video", &context);
let preferred_library = libraries::resolve_library_param(&app_state, body.library.as_deref())
.ok()
.flatten()
.unwrap_or_else(|| app_state.primary_library());
let preferred_library =
libraries::resolve_library_param_state(&app_state, body.library.as_deref())
.ok()
.flatten()
.unwrap_or_else(|| app_state.primary_library());
// Try the resolved library first, then fall back to any other library
// that actually contains the file — handles union-mode requests where