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

+2 -7
View File
@@ -232,12 +232,8 @@ pub async fn generate_daily_summaries(
// Check if summary already exists
{
let mut dao = summary_dao.lock().expect("Unable to lock DailySummaryDao");
let otel_context = opentelemetry::Context::new();
if dao
.summary_exists(&otel_context, &date_str, contact)
.unwrap_or(false)
{
if dao.summary_exists(&date_str, contact).unwrap_or(false) {
skipped += 1;
if idx % 10 == 0 {
log::info!(
@@ -366,11 +362,10 @@ async fn generate_and_store_daily_summary(
};
// DAO calls still take an explicit OTel context (#141).
let child_cx = span.context();
let mut dao = summary_dao.lock().expect("Unable to lock DailySummaryDao");
let result = dao
.store_summary(&child_cx, insert)
.store_summary(insert)
.map_err(|e| anyhow::anyhow!("Failed to store summary: {:?}", e));
match &result {
+34 -69
View File
@@ -93,14 +93,12 @@ pub async fn generation_status_handler(
query: web::Query<GenerationStatusQuery>,
app_state: web::Data<AppState>,
) -> impl Responder {
let ctx = opentelemetry::Context::new();
if let Some(jid) = query.job_id {
let mut dao = app_state
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.get_job_by_id(&ctx, jid) {
match dao.get_job_by_id(jid) {
Ok(Some(job)) => {
return HttpResponse::Ok().json(GenerationStatusResponse {
job_id: job.id,
@@ -136,7 +134,7 @@ pub async fn generation_status_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.get_active_job(&ctx, library.id, &normalized) {
match dao.get_active_job(library.id, &normalized) {
Ok(Some(job)) => {
return HttpResponse::Ok().json(GenerationStatusResponse {
job_id: job.id,
@@ -187,14 +185,12 @@ pub async fn cancel_generation_handler(
request: web::Json<CancelGenerationRequest>,
app_state: web::Data<AppState>,
) -> impl Responder {
let ctx = opentelemetry::Context::new();
if let Some(jid) = request.job_id {
let mut dao = app_state
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.cancel_job(&ctx, jid) {
match dao.cancel_job(jid) {
Ok(true) => {
let mut handles = app_state
.insight_job_handles
@@ -238,12 +234,12 @@ pub async fn cancel_generation_handler(
.lock()
.expect("Unable to lock InsightJobDao");
let ids = dao
.get_active_job(&ctx, library.id, &normalized)
.get_active_job(library.id, &normalized)
.ok()
.flatten()
.map(|j| vec![j.id])
.unwrap_or_default();
let _ = dao.cancel_active_jobs(&ctx, library.id, &normalized);
let _ = dao.cancel_active_jobs(library.id, &normalized);
ids
};
@@ -400,7 +396,7 @@ fn apply_owner_defaults(
let Some(mut dao) = app_state.user_ai_prefs_dao.lock().ok() else {
return;
};
let Ok(Some(prefs)) = dao.get_prefs(&opentelemetry::Context::new()) else {
let Ok(Some(prefs)) = dao.get_prefs() else {
return;
};
if model.is_none() {
@@ -455,14 +451,13 @@ pub async fn generate_insight_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
let ctx = opentelemetry::Context::new();
let ids = dao
.get_active_job(&ctx, library.id, &normalized_path)
.get_active_job(library.id, &normalized_path)
.ok()
.flatten()
.map(|j| vec![j.id])
.unwrap_or_default();
let _ = dao.cancel_active_jobs(&ctx, library.id, &normalized_path);
let _ = dao.cancel_active_jobs(library.id, &normalized_path);
ids
};
for jid in &old_job_ids {
@@ -481,12 +476,7 @@ pub async fn generate_insight_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.create_job(
&opentelemetry::Context::new(),
library.id,
&normalized_path,
gen_type,
) {
match dao.create_job(library.id, &normalized_path, gen_type) {
Ok(id) => id,
Err(e) => {
log::error!("Failed to create generation job: {:?}", e);
@@ -538,7 +528,6 @@ pub async fn generate_insight_handler(
})
.await;
let ctx = opentelemetry::Context::new();
let mut dao = job_dao.lock().expect("Unable to lock InsightJobDao");
match result {
@@ -547,18 +536,12 @@ pub async fn generate_insight_handler(
.insight_dao()
.lock()
.expect("Unable to lock InsightDao");
let insight_id = insight_dao
.get_insight(&ctx, &path)
.ok()
.flatten()
.map(|i| i.id);
let insight_id = insight_dao.get_insight(&path).ok().flatten().map(|i| i.id);
if let Some(id) = insight_id {
if let Err(e) = dao.complete_job(&ctx, job_id, id) {
if let Err(e) = dao.complete_job(job_id, id) {
log::error!("Failed to mark job {} as completed: {:?}", job_id, e);
}
} else if let Err(e) =
dao.fail_job(&ctx, job_id, "generation returned no insight")
{
} else if let Err(e) = dao.fail_job(job_id, "generation returned no insight") {
log::error!("Failed to mark job {} as failed: {:?}", job_id, e);
}
}
@@ -566,7 +549,7 @@ pub async fn generate_insight_handler(
log::error!("Insight generation failed for {}: {:?}", path, e);
// `{:#}` = one-line context chain; the job's error_message is
// returned to the client verbatim, so no Debug/backtrace here.
if let Err(err) = dao.fail_job(&ctx, job_id, &format!("{:#}", e)) {
if let Err(err) = dao.fail_job(job_id, &format!("{:#}", e)) {
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
@@ -577,14 +560,14 @@ pub async fn generate_insight_handler(
timeout_secs
);
if let Err(err) =
dao.fail_job(&ctx, job_id, &format!("timeout after {}s", timeout_secs))
dao.fail_job(job_id, &format!("timeout after {}s", timeout_secs))
{
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
Err(_) => {
log::error!("Insight generation task panicked for {}", path);
if let Err(err) = dao.fail_job(&ctx, job_id, "generation task panicked") {
if let Err(err) = dao.fail_job(job_id, "generation task panicked") {
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
@@ -627,8 +610,6 @@ pub async fn get_insight_handler(
let normalized_path = normalize_path(&query.path);
log::debug!("Fetching insight for {}", normalized_path);
let otel_context = opentelemetry::Context::new();
// Expand to rel_paths sharing content so an insight generated under
// library 1 still shows when the same photo is viewed from library 2.
let library = libraries::resolve_library_param_state(&app_state, query.library.as_deref())
@@ -637,13 +618,13 @@ pub async fn get_insight_handler(
.unwrap_or_else(|| app_state.primary_library());
let sibling_paths = {
let mut exif = exif_dao.lock().expect("Unable to lock ExifDao");
exif.get_rel_paths_sharing_content(&otel_context, library.id, &normalized_path)
exif.get_rel_paths_sharing_content(library.id, &normalized_path)
.unwrap_or_else(|_| vec![normalized_path.clone()])
};
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
match dao.get_insight_for_paths(&otel_context, &sibling_paths) {
match dao.get_insight_for_paths(&sibling_paths) {
Ok(Some(insight)) => HttpResponse::Ok().json(PhotoInsightResponse::from(insight)),
Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
"error": "Insight not found"
@@ -667,10 +648,9 @@ pub async fn delete_insight_handler(
let normalized_path = normalize_path(&query.path);
log::info!("Deleting insight for {}", normalized_path);
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
match dao.delete_insight(&otel_context, &normalized_path) {
match dao.delete_insight(&normalized_path) {
Ok(()) => HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": "Insight deleted successfully"
@@ -692,10 +672,9 @@ pub async fn get_all_insights_handler(
) -> impl Responder {
log::debug!("Fetching all insights");
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
match dao.get_all_insights(&otel_context) {
match dao.get_all_insights() {
Ok(insights) => {
let responses: Vec<PhotoInsightResponse> = insights
.into_iter()
@@ -725,10 +704,9 @@ pub async fn get_insight_history_handler(
let normalized_path = normalize_path(&query.path);
log::debug!("Fetching insight history for {}", normalized_path);
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
match dao.get_insight_history(&otel_context, &normalized_path) {
match dao.get_insight_history(&normalized_path) {
Ok(insights) => {
let responses: Vec<PhotoInsightResponse> = insights
.into_iter()
@@ -790,14 +768,13 @@ pub async fn generate_agentic_insight_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
let ctx = opentelemetry::Context::new();
let ids = dao
.get_active_job(&ctx, library.id, &normalized_path)
.get_active_job(library.id, &normalized_path)
.ok()
.flatten()
.map(|j| vec![j.id])
.unwrap_or_default();
let _ = dao.cancel_active_jobs(&ctx, library.id, &normalized_path);
let _ = dao.cancel_active_jobs(library.id, &normalized_path);
ids
};
for jid in &old_job_ids {
@@ -816,12 +793,7 @@ pub async fn generate_agentic_insight_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.create_job(
&opentelemetry::Context::new(),
library.id,
&normalized_path,
gen_type,
) {
match dao.create_job(library.id, &normalized_path, gen_type) {
Ok(id) => id,
Err(e) => {
log::error!("Failed to create agentic generation job: {:?}", e);
@@ -844,7 +816,6 @@ pub async fn generate_agentic_insight_handler(
};
let fewshot_examples: Vec<Vec<ChatMessage>> = {
let otel_context = opentelemetry::Context::new();
let mut dao = app_state
.insight_chat
.insight_dao()
@@ -853,7 +824,7 @@ pub async fn generate_agentic_insight_handler(
fewshot_ids
.iter()
.filter_map(|id| {
let insight = dao.get_insight_by_id(&otel_context, *id).ok().flatten()?;
let insight = dao.get_insight_by_id(*id).ok().flatten()?;
let json = insight.training_messages?;
match serde_json::from_str::<Vec<ChatMessage>>(&json) {
Ok(msgs) => Some(msgs),
@@ -940,19 +911,16 @@ pub async fn generate_agentic_insight_handler(
})
.await;
let ctx = opentelemetry::Context::new();
let mut dao = job_dao.lock().expect("Unable to lock InsightJobDao");
match result {
Ok(Ok(Ok((Some(insight_id), _, _)))) => {
if let Err(e) = dao.complete_job(&ctx, job_id, insight_id) {
if let Err(e) = dao.complete_job(job_id, insight_id) {
log::error!("Failed to mark job {} as completed: {:?}", job_id, e);
}
}
Ok(Ok(Ok((None, _, _)))) => {
if let Err(e) =
dao.fail_job(&ctx, job_id, "agentic generation returned no insight")
{
if let Err(e) = dao.fail_job(job_id, "agentic generation returned no insight") {
log::error!("Failed to mark job {} as failed: {:?}", job_id, e);
}
}
@@ -960,7 +928,7 @@ pub async fn generate_agentic_insight_handler(
log::error!("Agentic insight generation failed for {}: {:?}", path, e);
// `{:#}` = one-line context chain; the job's error_message is
// returned to the client verbatim, so no Debug/backtrace here.
if let Err(err) = dao.fail_job(&ctx, job_id, &format!("{:#}", e)) {
if let Err(err) = dao.fail_job(job_id, &format!("{:#}", e)) {
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
@@ -971,14 +939,14 @@ pub async fn generate_agentic_insight_handler(
timeout_secs
);
if let Err(err) =
dao.fail_job(&ctx, job_id, &format!("timeout after {}s", timeout_secs))
dao.fail_job(job_id, &format!("timeout after {}s", timeout_secs))
{
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
Err(_) => {
log::error!("Agentic insight generation task panicked for {}", path);
if let Err(err) = dao.fail_job(&ctx, job_id, "generation task panicked") {
if let Err(err) = dao.fail_job(job_id, "generation task panicked") {
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
@@ -1135,14 +1103,13 @@ pub async fn rate_insight_handler(
request.approved
);
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
// Rate a specific version by id when provided (history view), otherwise
// rate the current insight for the path.
let result = match request.insight_id {
Some(id) => dao.rate_insight_by_id(&otel_context, id, request.approved),
None => dao.rate_insight(&otel_context, &normalized_path, request.approved),
Some(id) => dao.rate_insight_by_id(id, request.approved),
None => dao.rate_insight(&normalized_path, request.approved),
};
match result {
@@ -1169,13 +1136,12 @@ pub async fn export_training_data_handler(
let approved_only = query.approved_only.unwrap_or(true);
log::info!("Exporting training data (approved_only={})", approved_only);
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
let insights = if approved_only {
dao.get_approved_insights(&otel_context)
dao.get_approved_insights()
} else {
dao.get_all_insights(&otel_context)
dao.get_all_insights()
};
match insights {
@@ -2369,8 +2335,7 @@ mod owner_defaults_tests {
)));
if let Some(p) = prefs {
let mut dao = SqliteUserAiPrefsDao::from_connection(conn);
dao.upsert_prefs(&opentelemetry::Context::new(), &p)
.expect("seed prefs");
dao.upsert_prefs(&p).expect("seed prefs");
}
state
}
+18 -41
View File
@@ -154,15 +154,14 @@ impl InsightChatService {
branch_id: Option<u64>,
) -> Result<HistoryView> {
let normalized = normalize_path(file_path);
let cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
let insight = match dao
.get_current_insight_for_library(&cx, library_id, &normalized)
.get_current_insight_for_library(library_id, &normalized)
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
{
Some(i) => i,
None => dao
.get_insight(&cx, &normalized)
.get_insight(&normalized)
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
.ok_or_else(|| anyhow!("no insight found for path"))?,
};
@@ -238,9 +237,8 @@ impl InsightChatService {
// 2. Load the current insight + history.
let insight = {
let cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
dao.get_current_insight_for_library(&cx, req.library_id, &normalized)
dao.get_current_insight_for_library(req.library_id, &normalized)
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
.ok_or_else(|| anyhow!("no insight found for path"))?
};
@@ -363,7 +361,6 @@ impl InsightChatService {
// Each LLM / tool await runs inside `loop_span` so their spans
// nest under it.
let loop_span = tracing::info_span!("ai.chat.loop");
let loop_cx = loop_span.context();
let mut tool_calls_made = 0usize;
let mut iterations_used = 0usize;
let mut last_prompt_eval_count: Option<i32> = None;
@@ -414,7 +411,6 @@ impl InsightChatService {
&normalized,
req.user_id,
&active_persona,
&loop_cx,
)
.instrument(loop_span.clone())
.await;
@@ -533,18 +529,16 @@ impl InsightChatService {
};
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, new_row.clone())
d.store_insight(new_row.clone())
})
.map_err(|e| anyhow!("failed to store amended insight: {:?}", e))?;
amended_insight_id = Some(stored.id);
} else {
let dao = self.insight_dao.clone();
let rows = retry_with_backoff("update_training_messages", 3, || {
let cx = opentelemetry::Context::new();
let mut d = dao.lock().expect("Unable to lock InsightDao");
d.update_training_messages(&cx, req.library_id, &normalized, &json)
d.update_training_messages(req.library_id, &normalized, &json)
})
.map_err(|e| anyhow!("failed to persist chat history: {:?}", e))?;
if rows == 0 {
@@ -602,9 +596,8 @@ impl InsightChatService {
let _guard = entry_lock.lock().await;
let insight = {
let cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
dao.get_current_insight_for_library(&cx, library_id, &normalized)
dao.get_current_insight_for_library(library_id, &normalized)
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
.ok_or_else(|| anyhow!("no insight found for path"))?
};
@@ -639,9 +632,8 @@ impl InsightChatService {
let dao = self.insight_dao.clone();
let rows = retry_with_backoff("update_training_messages", 3, || {
let cx = opentelemetry::Context::new();
let mut d = dao.lock().expect("Unable to lock InsightDao");
d.update_training_messages(&cx, library_id, &normalized, &json)
d.update_training_messages(library_id, &normalized, &json)
})
.map_err(|e| anyhow!("failed to persist rewound history: {:?}", e))?;
if rows == 0 {
@@ -677,9 +669,8 @@ impl InsightChatService {
let _guard = entry_lock.lock().await;
let insight = {
let cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
dao.get_current_insight_for_library(&cx, library_id, &normalized)
dao.get_current_insight_for_library(library_id, &normalized)
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
.ok_or_else(|| anyhow!("no insight found for path"))?
};
@@ -710,9 +701,8 @@ impl InsightChatService {
let dao = self.insight_dao.clone();
let rows = retry_with_backoff("update_training_messages", 3, || {
let cx = opentelemetry::Context::new();
let mut d = dao.lock().expect("Unable to lock InsightDao");
d.update_training_messages(&cx, library_id, &normalized, &json)
d.update_training_messages(library_id, &normalized, &json)
})
.map_err(|e| anyhow!("failed to persist branch switch: {:?}", e))?;
if rows == 0 {
@@ -742,10 +732,9 @@ impl InsightChatService {
viewing_leaf: Option<u64>,
) -> Result<(Vec<BranchLeafInfo>, u64)> {
let normalized = normalize_path(file_path);
let cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
let insight = dao
.get_current_insight_for_library(&cx, library_id, &normalized)
.get_current_insight_for_library(library_id, &normalized)
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
.ok_or_else(|| anyhow!("no insight found for path"))?;
@@ -930,9 +919,8 @@ impl InsightChatService {
// Look up existing insight scoped to this turn's library_id.
let existing_insight = {
let cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
dao.get_current_insight_for_library(&cx, req.library_id, &normalized)
dao.get_current_insight_for_library(req.library_id, &normalized)
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
};
@@ -1129,18 +1117,16 @@ impl InsightChatService {
};
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, new_row.clone())
d.store_insight(new_row.clone())
})
.map_err(|e| anyhow!("failed to store amended insight: {:?}", e))?;
amended_insight_id = Some(stored.id);
} else {
let dao = self.insight_dao.clone();
let rows = retry_with_backoff("update_training_messages", 3, || {
let cx = opentelemetry::Context::new();
let mut d = dao.lock().expect("Unable to lock InsightDao");
d.update_training_messages(&cx, req.library_id, &normalized, &json)
d.update_training_messages(req.library_id, &normalized, &json)
})
.map_err(|e| anyhow!("failed to persist chat history: {:?}", e))?;
if rows == 0 {
@@ -1327,9 +1313,8 @@ impl InsightChatService {
};
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, new_row.clone())
d.store_insight(new_row.clone())
})
.map_err(|e| anyhow!("failed to store bootstrap insight: {:?}", e))?;
@@ -1417,9 +1402,8 @@ impl InsightChatService {
// library bootstraps cleanly, even when another library has
// an insight for the same rel_path.
let existing_insight = {
let db_cx = opentelemetry::Context::new();
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
dao.get_current_insight_for_library(&db_cx, req.library_id, &normalized)
dao.get_current_insight_for_library(req.library_id, &normalized)
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
};
@@ -1595,18 +1579,16 @@ impl InsightChatService {
};
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, new_row.clone())
d.store_insight(new_row.clone())
})
.map_err(|e| anyhow!("failed to store amended insight: {:?}", e))?;
amended_insight_id = Some(stored.id);
} else {
let dao = self.insight_dao.clone();
let rows = retry_with_backoff("update_training_messages", 3, || {
let cx = opentelemetry::Context::new();
let mut d = dao.lock().expect("Unable to lock InsightDao");
d.update_training_messages(&cx, req.library_id, &normalized, &json)
d.update_training_messages(req.library_id, &normalized, &json)
})
.map_err(|e| anyhow!("failed to persist chat history: {:?}", e))?;
if rows == 0 {
@@ -1810,9 +1792,8 @@ impl InsightChatService {
};
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, new_row.clone())
d.store_insight(new_row.clone())
})
.map_err(|e| anyhow!("failed to store bootstrap insight: {:?}", e))?;
@@ -1920,7 +1901,6 @@ impl InsightChatService {
arguments: tool_call.function.arguments.clone(),
})
.await;
let cx = opentelemetry::Context::new();
let result = self
.generator
.execute_tool(
@@ -1931,7 +1911,6 @@ impl InsightChatService {
normalized,
user_id,
active_persona,
&cx,
)
.await;
let (result_preview, result_truncated) = truncate_tool_result(&result);
@@ -2421,7 +2400,6 @@ pub async fn run_streaming_agentic_loop_with_entry(
arguments: tool_call.function.arguments.clone(),
})
.await;
let cx = opentelemetry::Context::new();
let result = generator
.execute_tool(
&tool_call.function.name,
@@ -2431,7 +2409,6 @@ pub async fn run_streaming_agentic_loop_with_entry(
normalized,
user_id,
active_persona,
&cx,
)
.await;
let (result_preview, result_truncated) = truncate_tool_result(&result);
+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));
+12 -22
View File
@@ -529,26 +529,24 @@ impl PersonaChatSession {
if persona_id.is_empty() {
return Err(PersonaChatError::UnknownPersona(String::new()));
}
let cx = opentelemetry::Context::current();
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
// Validate up front rather than letting the first turn 404: a
// conversation row pointing at a persona that doesn't exist would be
// a permanently dead entry in the list.
let persona = dao
.get_persona(&cx, user_id, persona_id)
.get_persona(user_id, persona_id)
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))?;
if persona.is_none() {
return Err(PersonaChatError::UnknownPersona(persona_id.to_string()));
}
dao.create_persona_chat(&cx, user_id, persona_id, Utc::now().timestamp_millis())
dao.create_persona_chat(user_id, persona_id, Utc::now().timestamp_millis())
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))
}
/// Delete a conversation and its transcript outright.
pub fn delete_conversation(&self, user_id: i32, conversation_id: &str) -> Result<()> {
let cx = opentelemetry::Context::current();
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.delete_persona_chat(&cx, user_id, conversation_id)
dao.delete_persona_chat(user_id, conversation_id)
.map_err(|e| anyhow!("failed to delete persona chat: {e:?}"))?;
Ok(())
}
@@ -565,10 +563,9 @@ impl PersonaChatSession {
conversation_id: &str,
branch_id: Option<u64>,
) -> Result<PersonaChatHistoryView> {
let cx = opentelemetry::Context::current();
let row = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.get_persona_chat(&cx, user_id, conversation_id)
dao.get_persona_chat(user_id, conversation_id)
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
};
let Some(row) = row else {
@@ -628,18 +625,17 @@ impl PersonaChatSession {
return Err(PersonaChatError::UnknownConversation(conversation_id));
}
let cx = opentelemetry::Context::current();
// The persona comes from the stored conversation, not the request:
// a transcript's voice is fixed when it is created, so a stale
// client can't quietly swap it half-way through a thread.
let (persona, persona_id) = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
let row = dao
.get_persona_chat(&cx, user_id, &conversation_id)
.get_persona_chat(user_id, &conversation_id)
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))?
.ok_or_else(|| PersonaChatError::UnknownConversation(conversation_id.clone()))?;
let persona = dao
.get_persona(&cx, user_id, &row.persona_id)
.get_persona(user_id, &row.persona_id)
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))?
.ok_or_else(|| PersonaChatError::UnknownPersona(row.persona_id.clone()))?;
(persona, row.persona_id)
@@ -742,10 +738,9 @@ impl PersonaChatSession {
let _guard = lock.lock().await;
// Replay the active branch of the conversation tree.
let cx = opentelemetry::Context::current();
let row = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.get_persona_chat(&cx, user_id, &conversation_id)
dao.get_persona_chat(user_id, &conversation_id)
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
};
let row = row.ok_or_else(|| anyhow!("conversation not found"))?;
@@ -935,7 +930,6 @@ impl PersonaChatSession {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
let rows = dao
.update_persona_chat(
&cx,
user_id,
&conversation_id,
&json,
@@ -958,7 +952,7 @@ impl PersonaChatSession {
.unwrap_or_else(|| fallback_title(&messages));
if !title.is_empty() {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
if let Err(e) = dao.set_persona_chat_title(&cx, user_id, &conversation_id, &title) {
if let Err(e) = dao.set_persona_chat_title(user_id, &conversation_id, &title) {
log::warn!("failed to store persona chat title: {e:?}");
}
}
@@ -989,14 +983,13 @@ impl PersonaChatSession {
/// Conversations that hold nothing but the seed are omitted too — the
/// user has never actually said anything in them.
pub fn list_conversations(&self, user_id: i32) -> Result<Vec<PersonaConversationSummary>> {
let cx = opentelemetry::Context::current();
let (rows, personas) = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
let rows = dao
.list_persona_chats(&cx, user_id)
.list_persona_chats(user_id)
.map_err(|e| anyhow!("failed to list persona chats: {e:?}"))?;
let personas = dao
.list_personas(&cx, user_id)
.list_personas(user_id)
.map_err(|e| anyhow!("failed to list personas: {e:?}"))?;
(rows, personas)
};
@@ -1099,10 +1092,9 @@ impl PersonaChatSession {
};
let _guard = lock.lock().await;
let cx = opentelemetry::Context::current();
let row = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.get_persona_chat(&cx, user_id, conversation_id)
dao.get_persona_chat(user_id, conversation_id)
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
};
let row = row.ok_or_else(|| anyhow!("conversation not found"))?;
@@ -1121,7 +1113,6 @@ impl PersonaChatSession {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
let rows = dao
.update_persona_chat(
&cx,
user_id,
conversation_id,
&json,
@@ -1195,10 +1186,9 @@ impl PersonaChatSession {
node_id: Option<u64>,
viewing_leaf: Option<u64>,
) -> Result<(Vec<crate::ai::llm_client::BranchLeafInfo>, u64)> {
let cx = opentelemetry::Context::current();
let row = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.get_persona_chat(&cx, user_id, conversation_id)
dao.get_persona_chat(user_id, conversation_id)
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
};
let row = row.ok_or_else(|| anyhow!("conversation not found"))?;