feat: async insight generation with SQLite job tracking

- Add insight_generation_jobs table migration and DAO
- Implement job lifecycle: create_or_get_active, complete, fail, cancel
- Refactor POST /insights/generate and /agentic to async spawn with timeout
- Add GET /insights/generation/status endpoint with job_id and file_path lookup
- Use String for enum fields in Diesel models to avoid private Bound type
- Add from_str() helpers on InsightJobStatus and InsightGenerationType
- Fix update_training_messages to return Result<usize, DbError>
- 7/7 DAO unit tests passing
This commit is contained in:
Cameron Cordes
2026-05-27 10:01:17 -04:00
parent 5a75d1a28c
commit b87eb4e690
13 changed files with 1046 additions and 174 deletions
+35 -3
View File
@@ -107,6 +107,11 @@ impl InsightChatService {
}
}
/// Accessor for the insight DAO (used by async job completion).
pub fn insight_dao(&self) -> &Arc<Mutex<Box<dyn InsightDao>>> {
&self.insight_dao
}
/// Load the rendered transcript for chat-UI display. Filters internal
/// scaffolding (system message, tool turns, tool-dispatch-only assistant
/// messages) and drops base64 images from user turns to keep payloads
@@ -522,8 +527,17 @@ impl InsightChatService {
} else {
let cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
dao.update_training_messages(&cx, req.library_id, &normalized, &json)
let rows = dao
.update_training_messages(&cx, req.library_id, &normalized, &json)
.map_err(|e| anyhow!("failed to persist chat history: {:?}", e))?;
if rows == 0 {
log::warn!(
"update_training_messages updated 0 rows for {} (lib {}), \
concurrent regenerate likely flipped is_current",
normalized,
req.library_id
);
}
}
Ok(ChatTurnResult {
@@ -590,8 +604,17 @@ impl InsightChatService {
let cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
dao.update_training_messages(&cx, library_id, &normalized, &json)
let rows = dao
.update_training_messages(&cx, library_id, &normalized, &json)
.map_err(|e| anyhow!("failed to persist truncated history: {:?}", e))?;
if rows == 0 {
log::warn!(
"update_training_messages (rewind) updated 0 rows for {} (lib {}), \
concurrent regenerate likely flipped is_current",
normalized,
library_id
);
}
Ok(())
}
@@ -851,8 +874,17 @@ impl InsightChatService {
} else {
let cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
dao.update_training_messages(&cx, req.library_id, &normalized, &json)
let rows = dao
.update_training_messages(&cx, req.library_id, &normalized, &json)
.map_err(|e| anyhow!("failed to persist chat history: {:?}", e))?;
if rows == 0 {
log::warn!(
"update_training_messages (stream) updated 0 rows for {} (lib {}), \
concurrent regenerate likely flipped is_current",
normalized,
req.library_id
);
}
}
let _ = tx