Tracing: DAO layer + handler spans on tracing, drop all Context params (#141)

trace_db_call no longer takes an OpenTelemetry Context: it opens a
db.<query_type>.<operation> span (same name via otel.name, same
db.query_type / db.operation attrs) on the caller's ambient tracing span
and runs the DAO body inside it. The cx / ctx / context parameter is gone
from every DAO trait method, impl and call site, and from the helpers and
background drains that only threaded it.

The remaining request-handler spans (files, tags, faces, image, video,
favorites, duplicates, memories) move to #[instrument]; the
Context::current_with_span / with_remote_span_context plumbing is gone, as
is extract_context_from_request (the tracing-actix-web root span covers
it). otel::set_span_attributes replaces BoxedSpan::set_attributes.

Background-job raw spans (thumbnails, video actors) are left for #142.

tests/otel_db_spans.rs pins name, attributes, status and nesting (across
an instrumented spawn); otel_traceparent now uses an #[instrument]
handler. CLAUDE.md documents the tracing conventions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-09-29 17:02:40 -04:00
1 parent 1c508cdabd
commit b6deca2a52
55 files changed
+1942 -3852

No files matched your search

+65 -131
View File
@@ -239,7 +239,7 @@ impl InsightGenerator {
.user_ai_prefs_dao
.lock()
.ok()
.and_then(|mut dao| dao.get_prefs(&opentelemetry::Context::new()).ok().flatten())
.and_then(|mut dao| dao.get_prefs().ok().flatten())
.and_then(|p| p.display_tz);
crate::camera_tz::display_zone_for(stored.as_deref())
};
@@ -345,36 +345,35 @@ impl InsightGenerator {
has_vision: bool,
persona: Option<(i32, &str)>,
) -> ToolGateOpts {
let cx = opentelemetry::Context::new();
let calendar_present = {
let mut dao = self
.calendar_dao
.lock()
.expect("Unable to lock CalendarEventDao");
dao.get_event_count(&cx).map(|n| n > 0).unwrap_or(false)
dao.get_event_count().map(|n| n > 0).unwrap_or(false)
};
let location_history_present = {
let mut dao = self
.location_dao
.lock()
.expect("Unable to lock LocationHistoryDao");
dao.get_location_count(&cx).map(|n| n > 0).unwrap_or(false)
dao.get_location_count().map(|n| n > 0).unwrap_or(false)
};
let daily_summaries_present = {
let mut dao = self
.daily_summary_dao
.lock()
.expect("Unable to lock DailySummaryDao");
dao.has_any_summaries(&cx).unwrap_or(false)
dao.has_any_summaries().unwrap_or(false)
};
let faces_present = {
let mut dao = self.face_dao.lock().expect("Unable to lock FaceDao");
dao.has_any_faces(&cx).unwrap_or(false)
dao.has_any_faces().unwrap_or(false)
};
let allow_agent_corrections = persona
.and_then(|(uid, pid)| {
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
pdao.get_persona(&cx, uid, pid).ok().flatten()
pdao.get_persona(uid, pid).ok().flatten()
})
.map(|p| p.allow_agent_corrections)
.unwrap_or(false);
@@ -396,9 +395,8 @@ impl InsightGenerator {
/// carries `persona_id` but no explicit `system_prompt` should speak
/// in the persona's stored voice, not the neutral default.
pub(crate) fn persona_system_prompt(&self, user_id: i32, persona_id: &str) -> Option<String> {
let cx = opentelemetry::Context::new();
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
pdao.get_persona(&cx, user_id, persona_id)
pdao.get_persona(user_id, persona_id)
.ok()
.flatten()
.map(|p| p.system_prompt)
@@ -446,9 +444,8 @@ impl InsightGenerator {
/// that need a few specific fields (date_taken, GPS) without
/// duplicating DAO plumbing.
pub(crate) fn fetch_exif(&self, file_path: &str) -> Option<crate::database::models::ImageExif> {
let cx = opentelemetry::Context::current();
let mut dao = self.exif_dao.lock().expect("Unable to lock ExifDao");
dao.get_exif(&cx, file_path).ok().flatten()
dao.get_exif(file_path).ok().flatten()
}
/// Load image file, resize it, and encode as base64 for vision models
@@ -619,7 +616,6 @@ impl InsightGenerator {
span.set_attribute("query", query.clone());
// Create context with this span for child operations
let search_cx = span.context();
log::info!("========================================");
log::info!("RAG QUERY: {}", query);
@@ -640,7 +636,7 @@ impl InsightGenerator {
let date_str = date.format("%Y-%m-%d").to_string();
let similar_summaries = summary_dao
.find_similar_summaries_with_time_weight(&search_cx, &query_embedding, &date_str, limit)
.find_similar_summaries_with_time_weight(&query_embedding, &date_str, limit)
.map_err(|e| anyhow::anyhow!("Failed to find similar summaries: {:?}", e))?;
log::info!(
@@ -684,7 +680,6 @@ impl InsightGenerator {
if let Some(d) = date {
span.set_attribute("date", d.to_string());
}
let search_cx = span.context();
log::info!("RAG QUERY: {} (anchor date: {:?})", query, date);
@@ -700,12 +695,11 @@ impl InsightGenerator {
let similar_summaries = match date {
Some(d) => summary_dao.find_similar_summaries_with_time_weight(
&search_cx,
&query_embedding,
&d.format("%Y-%m-%d").to_string(),
limit,
),
None => summary_dao.find_similar_summaries(&search_cx, &query_embedding, limit),
None => summary_dao.find_similar_summaries(&query_embedding, limit),
}
.map_err(|e| anyhow::anyhow!("Failed to find similar summaries: {:?}", e))?;
@@ -770,7 +764,6 @@ impl InsightGenerator {
location: Option<&str>,
) -> Result<Option<String>> {
let span = tracing::Span::current();
let calendar_cx = span.context();
let query_embedding = if let Some(loc) = location {
match crate::ai::embed_query(&self.ollama, self.llamacpp.as_deref(), loc).await {
@@ -790,7 +783,6 @@ impl InsightGenerator {
.lock()
.expect("Unable to lock CalendarEventDao");
dao.find_relevant_events_hybrid(
&calendar_cx,
timestamp,
7, // ±7 days window
query_embedding.as_deref(),
@@ -844,7 +836,6 @@ impl InsightGenerator {
exif_gps: Option<(f64, f64)>,
) -> Result<Option<String>> {
let span = tracing::Span::current();
let location_cx = span.context();
let nearest = {
let mut dao = self
@@ -852,9 +843,7 @@ impl InsightGenerator {
.lock()
.expect("Unable to lock LocationHistoryDao");
dao.find_nearest_location(
&location_cx,
timestamp,
10800, // ±3 hours (more realistic for photo timing)
timestamp, 10800, // ±3 hours (more realistic for photo timing)
)
.ok()
.flatten()
@@ -922,7 +911,6 @@ impl InsightGenerator {
enrichment: Option<&str>,
) -> Result<Option<String>> {
let span = tracing::Span::current();
let search_cx = span.context();
// Use enrichment (topics + photo description + tags) if available;
// fall back to generic temporal query.
@@ -961,7 +949,6 @@ impl InsightGenerator {
.lock()
.expect("Unable to lock SearchHistoryDao");
dao.find_relevant_searches_hybrid(
&search_cx,
timestamp,
30, // ±30 days (wider window than calendar)
Some(&query_embedding),
@@ -1099,13 +1086,12 @@ impl InsightGenerator {
}
// Create context with this span for child operations
let insight_cx = span.context();
// 1. Get EXIF data for the photo
let exif = {
let mut exif_dao = self.exif_dao.lock().expect("Unable to lock ExifDao");
exif_dao
.get_exif(&insight_cx, &file_path)
.get_exif(&file_path)
.map_err(|e| anyhow::anyhow!("Failed to get EXIF: {:?}", e))?
};
@@ -1124,7 +1110,7 @@ impl InsightGenerator {
// Fetch file tags (used to enrich RAG and final context)
let tag_names: Vec<String> = {
let mut dao = self.tag_dao.lock().expect("Unable to lock TagDao");
dao.get_tags_for_path(&insight_cx, &file_path)
dao.get_tags_for_path(&file_path)
.unwrap_or_else(|e| {
log::warn!("Failed to fetch tags for insight {}: {}", file_path, e);
Vec::new()
@@ -1562,9 +1548,8 @@ impl InsightGenerator {
let dao = self.insight_dao.clone();
let result = retry_with_backoff("store_insight", 3, || {
let cx = opentelemetry::Context::new();
let mut d = dao.lock().expect("Unable to lock InsightDao");
d.store_insight(&cx, insight.clone())
d.store_insight(insight.clone())
})
.map_err(|e| anyhow::anyhow!("Failed to store insight: {:?}", e));
@@ -1770,34 +1755,33 @@ Return ONLY the summary, nothing else."#,
file_path: &str,
user_id: i32,
persona_id: &str,
cx: &opentelemetry::Context,
) -> String {
let model = backend.model();
let backend_label = backend.kind.as_str();
let result = match tool_name {
"search_rag" => self.tool_search_rag(arguments, backend.local()).await,
"search_messages" => self.tool_search_messages(arguments, cx).await,
"get_sms_messages" => self.tool_get_sms_messages(arguments, cx).await,
"get_calendar_events" => self.tool_get_calendar_events(arguments, cx).await,
"get_location_history" => self.tool_get_location_history(arguments, cx).await,
"get_file_tags" => self.tool_get_file_tags(arguments, cx).await,
"get_faces_in_photo" => self.tool_get_faces_in_photo(arguments, cx).await,
"search_messages" => self.tool_search_messages(arguments).await,
"get_sms_messages" => self.tool_get_sms_messages(arguments).await,
"get_calendar_events" => self.tool_get_calendar_events(arguments).await,
"get_location_history" => self.tool_get_location_history(arguments).await,
"get_file_tags" => self.tool_get_file_tags(arguments).await,
"get_faces_in_photo" => self.tool_get_faces_in_photo(arguments).await,
"describe_photo" => {
self.tool_describe_photo(backend.local(), image_base64)
.await
}
"reverse_geocode" => self.tool_reverse_geocode(arguments).await,
"get_personal_place_at" => self.tool_get_personal_place_at(arguments).await,
"recall_entities" => self.tool_recall_entities(arguments, cx).await,
"recall_entities" => self.tool_recall_entities(arguments).await,
"recall_facts_for_photo" => {
self.tool_recall_facts_for_photo(arguments, user_id, persona_id, cx)
self.tool_recall_facts_for_photo(arguments, user_id, persona_id)
.await
}
"recall_facts_for_entity" => {
self.tool_recall_facts_for_entity(arguments, user_id, persona_id, cx)
self.tool_recall_facts_for_entity(arguments, user_id, persona_id)
.await
}
"store_entity" => self.tool_store_entity(arguments, cx).await,
"store_entity" => self.tool_store_entity(arguments).await,
"store_fact" => {
self.tool_store_fact(
arguments,
@@ -1806,16 +1790,15 @@ Return ONLY the summary, nothing else."#,
persona_id,
model,
backend_label,
cx,
)
.await
}
"update_fact" => {
self.tool_update_fact(arguments, user_id, persona_id, model, backend_label, cx)
self.tool_update_fact(arguments, user_id, persona_id, model, backend_label)
.await
}
"supersede_fact" => {
self.tool_supersede_fact(arguments, user_id, persona_id, model, backend_label, cx)
self.tool_supersede_fact(arguments, user_id, persona_id, model, backend_label)
.await
}
"get_current_datetime" => Self::tool_get_current_datetime(),
@@ -2039,11 +2022,7 @@ Return ONLY the summary, nothing else."#,
/// Tool: search_messages — keyword / semantic / hybrid search over all
/// SMS message bodies via the Django FTS5 + embeddings pipeline. Now
/// supports optional `contact_id`, `start_ts`, `end_ts` filters.
async fn tool_search_messages(
&self,
args: &serde_json::Value,
cx: &opentelemetry::Context,
) -> String {
async fn tool_search_messages(&self, args: &serde_json::Value) -> String {
let query = match args.get("query").and_then(|v| v.as_str()) {
Some(q) if !q.trim().is_empty() => q.trim(),
_ => {
@@ -2068,7 +2047,7 @@ Return ONLY the summary, nothing else."#,
log::info!(
"search_messages with `date` and no `query` — routing to get_sms_messages"
);
let routed = self.tool_get_sms_messages(args, cx).await;
let routed = self.tool_get_sms_messages(args).await;
return format!(
"(Note: routed to get_sms_messages — search_messages requires a \
`query`; date-only browsing belongs on get_sms_messages. Prefer \
@@ -2207,11 +2186,7 @@ Return ONLY the summary, nothing else."#,
}
/// Tool: get_sms_messages — fetch SMS messages near a date for a contact
async fn tool_get_sms_messages(
&self,
args: &serde_json::Value,
_cx: &opentelemetry::Context,
) -> String {
async fn tool_get_sms_messages(&self, args: &serde_json::Value) -> String {
let date_str = match args.get("date").and_then(|v| v.as_str()) {
Some(d) => d,
None => return "Error: missing required parameter 'date'".to_string(),
@@ -2289,11 +2264,7 @@ Return ONLY the summary, nothing else."#,
}
/// Tool: get_calendar_events — fetch calendar events near a date
async fn tool_get_calendar_events(
&self,
args: &serde_json::Value,
cx: &opentelemetry::Context,
) -> String {
async fn tool_get_calendar_events(&self, args: &serde_json::Value) -> String {
let date_str = match args.get("date").and_then(|v| v.as_str()) {
Some(d) => d,
None => return "Error: missing required parameter 'date'".to_string(),
@@ -2327,7 +2298,7 @@ Return ONLY the summary, nothing else."#,
.calendar_dao
.lock()
.expect("Unable to lock CalendarEventDao");
dao.find_relevant_events_hybrid(cx, timestamp, days_radius, None, limit)
dao.find_relevant_events_hybrid(timestamp, days_radius, None, limit)
.ok()
};
@@ -2369,11 +2340,7 @@ Return ONLY the summary, nothing else."#,
}
/// Tool: get_location_history — fetch location records near a date
async fn tool_get_location_history(
&self,
args: &serde_json::Value,
cx: &opentelemetry::Context,
) -> String {
async fn tool_get_location_history(&self, args: &serde_json::Value) -> String {
let date_str = match args.get("date").and_then(|v| v.as_str()) {
Some(d) => d,
None => return "Error: missing required parameter 'date'".to_string(),
@@ -2404,7 +2371,7 @@ Return ONLY the summary, nothing else."#,
.location_dao
.lock()
.expect("Unable to lock LocationHistoryDao");
dao.find_locations_in_range(cx, start_ts, end_ts).ok()
dao.find_locations_in_range(start_ts, end_ts).ok()
};
match locations {
@@ -2462,11 +2429,7 @@ Return ONLY the summary, nothing else."#,
}
/// Tool: get_file_tags — fetch tags for a file path
async fn tool_get_file_tags(
&self,
args: &serde_json::Value,
cx: &opentelemetry::Context,
) -> String {
async fn tool_get_file_tags(&self, args: &serde_json::Value) -> String {
let file_path = match args.get("file_path").and_then(|v| v.as_str()) {
Some(p) => p.to_string(),
None => return "Error: missing required parameter 'file_path'".to_string(),
@@ -2476,7 +2439,7 @@ Return ONLY the summary, nothing else."#,
let tags = {
let mut dao = self.tag_dao.lock().expect("Unable to lock TagDao");
dao.get_tags_for_path(cx, &file_path).ok()
dao.get_tags_for_path(&file_path).ok()
};
match tags {
@@ -2493,11 +2456,7 @@ Return ONLY the summary, nothing else."#,
/// the given file path. Resolves rel_path → content_hash via FaceDao,
/// then queries face_detections joined with persons (status='detected'
/// only). Returns a compact bullet list keyed for human-LLM readability.
async fn tool_get_faces_in_photo(
&self,
args: &serde_json::Value,
cx: &opentelemetry::Context,
) -> String {
async fn tool_get_faces_in_photo(&self, args: &serde_json::Value) -> String {
let file_path = match args.get("file_path").and_then(|v| v.as_str()) {
Some(p) if !p.trim().is_empty() => p.trim().to_string(),
_ => return "Error: missing required parameter 'file_path'".to_string(),
@@ -2509,11 +2468,9 @@ Return ONLY the summary, nothing else."#,
// is synchronous and there's no await in the loop body.
let content_hash: Option<String> = {
let mut dao = self.face_dao.lock().expect("Unable to lock FaceDao");
self.libraries.iter().find_map(|lib| {
dao.resolve_content_hash(cx, lib.id, &file_path)
.ok()
.flatten()
})
self.libraries
.iter()
.find_map(|lib| dao.resolve_content_hash(lib.id, &file_path).ok().flatten())
};
let Some(content_hash) = content_hash else {
return "No content_hash found for that file path (the photo may not be indexed yet, \
@@ -2523,7 +2480,7 @@ Return ONLY the summary, nothing else."#,
let faces = {
let mut dao = self.face_dao.lock().expect("Unable to lock FaceDao");
match dao.list_for_content_hash(cx, &content_hash) {
match dao.list_for_content_hash(&content_hash) {
Ok(rows) => rows,
Err(e) => return format!("Error querying faces: {}", e),
}
@@ -2642,11 +2599,7 @@ Return ONLY the summary, nothing else."#,
}
/// Tool: recall_entities — search the knowledge memory for known entities
async fn tool_recall_entities(
&self,
args: &serde_json::Value,
cx: &opentelemetry::Context,
) -> String {
async fn tool_recall_entities(&self, args: &serde_json::Value) -> String {
use crate::database::EntityFilter;
let name_search = args
@@ -2688,7 +2641,7 @@ Return ONLY the summary, nothing else."#,
.knowledge_dao
.lock()
.expect("Unable to lock KnowledgeDao");
match kdao.list_entities(cx, filter) {
match kdao.list_entities(filter) {
Ok((entities, _total)) if entities.iter().all(|e| e.status == "rejected") => {
"No known entities found matching the query.".to_string()
}
@@ -2719,7 +2672,6 @@ Return ONLY the summary, nothing else."#,
args: &serde_json::Value,
user_id: i32,
persona_id: &str,
cx: &opentelemetry::Context,
) -> String {
use crate::database::PersonaFilter;
let persona_filter = PersonaFilter::Single {
@@ -2739,7 +2691,7 @@ Return ONLY the summary, nothing else."#,
// default of active+reviewed.
let reviewed_only = {
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
pdao.get_persona(cx, user_id, persona_id)
pdao.get_persona(user_id, persona_id)
.ok()
.flatten()
.map(|p| p.reviewed_only_facts)
@@ -2752,7 +2704,7 @@ Return ONLY the summary, nothing else."#,
.expect("Unable to lock KnowledgeDao");
// Fetch photo links to find which entities appear in this photo
let links = match kdao.get_links_for_photo(cx, &file_path) {
let links = match kdao.get_links_for_photo(&file_path) {
Ok(l) => l,
Err(e) => return format!("Error fetching photo links: {:?}", e),
};
@@ -2766,7 +2718,7 @@ Return ONLY the summary, nothing else."#,
// For each linked entity, fetch its facts
for entity_id in entity_ids {
if let Ok(entity) = kdao.get_entity_by_id(cx, entity_id)
if let Ok(entity) = kdao.get_entity_by_id(entity_id)
&& let Some(e) = entity
{
let role = links
@@ -2778,7 +2730,7 @@ Return ONLY the summary, nothing else."#,
"Entity: {} ({}, role: {})",
e.name, e.entity_type, role
));
if let Ok(facts) = kdao.get_facts_for_entity(cx, entity_id, &persona_filter) {
if let Ok(facts) = kdao.get_facts_for_entity(entity_id, &persona_filter) {
// Default scope: active + reviewed (everything not
// rejected / superseded). Strict mode trims to
// reviewed only — the persona has opted into seeing
@@ -2794,7 +2746,7 @@ Return ONLY the summary, nothing else."#,
let obj = if let Some(ref v) = f.object_value {
v.clone()
} else if let Some(oid) = f.object_entity_id {
kdao.get_entity_by_id(cx, oid)
kdao.get_entity_by_id(oid)
.ok()
.flatten()
.map(|e| format!("{} (entity ID: {})", e.name, e.id))
@@ -2825,7 +2777,6 @@ Return ONLY the summary, nothing else."#,
args: &serde_json::Value,
user_id: i32,
persona_id: &str,
cx: &opentelemetry::Context,
) -> String {
use crate::database::PersonaFilter;
let persona_filter = PersonaFilter::Single {
@@ -2853,7 +2804,7 @@ Return ONLY the summary, nothing else."#,
// row → permissive active+reviewed default).
let reviewed_only = {
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
pdao.get_persona(cx, user_id, persona_id)
pdao.get_persona(user_id, persona_id)
.ok()
.flatten()
.map(|p| p.reviewed_only_facts)
@@ -2865,14 +2816,14 @@ Return ONLY the summary, nothing else."#,
.lock()
.expect("Unable to lock KnowledgeDao");
let entity = match kdao.get_entity_by_id(cx, entity_id) {
let entity = match kdao.get_entity_by_id(entity_id) {
Ok(Some(e)) => e,
Ok(None) => return format!("No entity found with ID {}.", entity_id),
Err(e) => return format!("Error fetching entity: {:?}", e),
};
let mut output_lines = vec![format!("Entity: {} ({})", entity.name, entity.entity_type)];
match kdao.get_facts_for_entity(cx, entity_id, &persona_filter) {
match kdao.get_facts_for_entity(entity_id, &persona_filter) {
Ok(facts) => {
// Default scope: active + reviewed. Strict mode trims to
// reviewed only — same allow rule as recall_facts_for_photo.
@@ -2887,7 +2838,7 @@ Return ONLY the summary, nothing else."#,
let obj = if let Some(ref v) = f.object_value {
v.clone()
} else if let Some(oid) = f.object_entity_id {
kdao.get_entity_by_id(cx, oid)
kdao.get_entity_by_id(oid)
.ok()
.flatten()
.map(|e| format!("{} (entity ID: {})", e.name, e.id))
@@ -2914,11 +2865,7 @@ Return ONLY the summary, nothing else."#,
/// Tool: store_entity — upsert an entity into the knowledge memory.
/// Embeddings go through the configured local backend (`LLM_BACKEND`),
/// independent of the per-request chat backend in the caller.
async fn tool_store_entity(
&self,
args: &serde_json::Value,
cx: &opentelemetry::Context,
) -> String {
async fn tool_store_entity(&self, args: &serde_json::Value) -> String {
use crate::database::models::InsertEntity;
let name = match args.get("name").and_then(|v| v.as_str()) {
@@ -2964,7 +2911,7 @@ Return ONLY the summary, nothing else."#,
.knowledge_dao
.lock()
.expect("Unable to lock KnowledgeDao");
kdao.list_entities(cx, filter)
kdao.list_entities(filter)
.unwrap_or_default()
.0
.into_iter()
@@ -3025,7 +2972,7 @@ Return ONLY the summary, nothing else."#,
.knowledge_dao
.lock()
.expect("Unable to lock KnowledgeDao");
kdao.upsert_entity(cx, insert.clone())
kdao.upsert_entity(insert.clone())
}) {
Ok(entity) => {
let mut response = format!(
@@ -3056,7 +3003,6 @@ Return ONLY the summary, nothing else."#,
persona_id: &str,
model: &str,
backend: &str,
cx: &opentelemetry::Context,
) -> String {
use crate::database::models::{InsertEntityFact, InsertEntityPhotoLink};
@@ -3140,7 +3086,7 @@ Return ONLY the summary, nothing else."#,
.knowledge_dao
.lock()
.expect("Unable to lock KnowledgeDao");
kdao.upsert_fact(cx, fact.clone())
kdao.upsert_fact(fact.clone())
}) {
Ok((f, is_new)) => (f, is_new),
Err(e) => return format!("Error storing fact: {:?}", e),
@@ -3164,7 +3110,7 @@ Return ONLY the summary, nothing else."#,
.knowledge_dao
.lock()
.expect("Unable to lock KnowledgeDao");
kdao.upsert_photo_link(cx, link.clone())
kdao.upsert_photo_link(link.clone())
},
) {
log::warn!(
@@ -3197,14 +3143,13 @@ Return ONLY the summary, nothing else."#,
persona_id: &str,
model: &str,
backend: &str,
cx: &opentelemetry::Context,
) -> String {
use crate::database::FactPatch;
// Defense-in-depth gate check.
let allowed = {
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
pdao.get_persona(cx, user_id, persona_id)
pdao.get_persona(user_id, persona_id)
.ok()
.flatten()
.map(|p| p.allow_agent_corrections)
@@ -3257,7 +3202,7 @@ Return ONLY the summary, nothing else."#,
.knowledge_dao
.lock()
.expect("Unable to lock KnowledgeDao");
kdao.update_fact(cx, fact_id, patch.clone(), Some((model, backend)))
kdao.update_fact(fact_id, patch.clone(), Some((model, backend)))
}) {
Ok(Some(f)) => format!(
"Updated fact ID:{} (status={}, confidence={:.2})",
@@ -3277,11 +3222,10 @@ Return ONLY the summary, nothing else."#,
persona_id: &str,
model: &str,
backend: &str,
cx: &opentelemetry::Context,
) -> String {
let allowed = {
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
pdao.get_persona(cx, user_id, persona_id)
pdao.get_persona(user_id, persona_id)
.ok()
.flatten()
.map(|p| p.allow_agent_corrections)
@@ -3318,7 +3262,7 @@ Return ONLY the summary, nothing else."#,
.knowledge_dao
.lock()
.expect("Unable to lock KnowledgeDao");
kdao.supersede_fact(cx, old_fact_id, new_fact_id, Some((model, backend)))
kdao.supersede_fact(old_fact_id, new_fact_id, Some((model, backend)))
},
) {
Ok(Some(f)) => format!(
@@ -4140,13 +4084,11 @@ Return ONLY the summary, nothing else."#,
span.set_attribute("model", backend.model().to_string());
span.set_attribute("images_inline", backend.images_inline);
let insight_cx = span.context();
// 3. Fetch EXIF
let exif = {
let mut exif_dao = self.exif_dao.lock().expect("Unable to lock ExifDao");
exif_dao
.get_exif(&insight_cx, &file_path)
.get_exif(&file_path)
.map_err(|e| anyhow::anyhow!("Failed to get EXIF: {:?}", e))?
};
@@ -4160,7 +4102,7 @@ Return ONLY the summary, nothing else."#,
// 5. Fetch tags
let tag_names: Vec<String> = {
let mut dao = self.tag_dao.lock().expect("Unable to lock TagDao");
dao.get_tags_for_path(&insight_cx, &file_path)
dao.get_tags_for_path(&file_path)
.unwrap_or_else(|e| {
log::warn!("Failed to fetch tags for agentic {}: {}", file_path, e);
Vec::new()
@@ -4196,7 +4138,7 @@ Return ONLY the summary, nothing else."#,
created_at: Utc::now().timestamp(),
updated_at: Utc::now().timestamp(),
};
match kdao.upsert_entity(&insight_cx, owner) {
match kdao.upsert_entity(owner) {
Ok(e) => {
log::info!("Owner entity '{}' ID: {}", owner_name, e.id);
Some(e.id)
@@ -4334,7 +4276,6 @@ Return ONLY the summary, nothing else."#,
// LLM / tool awaits below run inside `loop_span` so their spans
// nest under it.
let loop_span = tracing::info_span!("ai.agentic.loop");
let loop_cx = loop_span.context();
let mut final_content = String::new();
let mut iterations_used = 0usize;
@@ -4392,7 +4333,6 @@ Return ONLY the summary, nothing else."#,
&file_path,
user_id,
&persona_id,
&loop_cx,
)
.instrument(loop_span.clone())
.await;
@@ -4445,7 +4385,6 @@ Return ONLY the summary, nothing else."#,
&file_path,
user_id,
&persona_id,
&loop_cx,
)
.instrument(loop_span.clone())
.await;
@@ -4526,9 +4465,8 @@ Return ONLY the summary, nothing else."#,
let dao = self.insight_dao.clone();
let stored = retry_with_backoff("store_insight", 3, || {
let cx = opentelemetry::Context::new();
let mut d = dao.lock().expect("Unable to lock InsightDao");
d.store_insight(&cx, insight.clone())
d.store_insight(insight.clone())
})
.map_err(|e| anyhow::anyhow!("Failed to store agentic insight: {:?}", e));
@@ -4551,8 +4489,7 @@ Return ONLY the summary, nothing else."#,
.knowledge_dao
.lock()
.expect("Unable to lock KnowledgeDao");
if let Err(e) = kdao.update_facts_insight_id(&insight_cx, &file_path, stored_insight.id)
{
if let Err(e) = kdao.update_facts_insight_id(&file_path, stored_insight.id) {
log::warn!(
"Failed to backfill source_insight_id for {}: {:?}",
file_path,
@@ -4591,7 +4528,6 @@ Return ONLY the summary, nothing else."#,
tools: Vec<Tool>,
max_iter: usize,
) -> Result<String> {
let loop_cx = opentelemetry::Context::current();
let mut final_content = String::new();
for iteration in 0..max_iter {
@@ -4641,7 +4577,6 @@ Return ONLY the summary, nothing else."#,
"",
0,
"",
&loop_cx,
)
.await;
messages.push(ChatMessage::tool_result(result));
@@ -4683,7 +4618,6 @@ Return ONLY the summary, nothing else."#,
"",
0,
"",
&loop_cx,
)
.await;
messages.push(ChatMessage::tool_result(result));