1use crate::store::{CostEstimate, MemoryEntry, Task, TaskStore, TokenUsage};
8use anyhow::anyhow;
9use std::sync::Arc;
10
11pub type StoreResult<T> = anyhow::Result<Option<T>>;
13
14fn token_i64_to_u64_non_negative(tokens: i64) -> u64 {
15 u64::try_from(tokens.max(0)).unwrap_or(0)
16}
17
18pub async fn resolve_store_id(
23 store: &Option<Arc<TaskStore>>,
24 repo: &str,
25 task_id: &str,
26) -> Option<i64> {
27 let s = store.as_ref()?;
28 match s.resolve_task_id(repo, task_id).await {
29 Ok(Some(id)) => Some(id),
30 Ok(None) => None,
31 Err(e) => {
32 tracing::warn!(task_id, error = %e, "failed to resolve task id for store operations");
33 None
34 }
35 }
36}
37
38pub async fn opt_store_get_task_by_id(
43 store: &Option<Arc<TaskStore>>,
44 store_id: i64,
45) -> Option<Task> {
46 let s = store.as_ref()?;
47 match s.get(store_id).await {
48 Ok(task) => Some(task),
49 Err(e) => {
50 tracing::warn!(store_id, error = %e, "failed to fetch task from store");
51 None
52 }
53 }
54}
55
56pub async fn opt_store_get_task(
61 store: &Option<Arc<TaskStore>>,
62 repo: &str,
63 task_id: &str,
64) -> Option<Task> {
65 let s = store.as_ref()?;
66 let store_id = match s.resolve_task_id(repo, task_id).await {
67 Ok(Some(id)) => id,
68 Ok(None) => return None,
69 Err(e) => {
70 tracing::warn!(task_id, error = %e, "failed to resolve task id in store");
71 return None;
72 }
73 };
74 opt_store_get_task_by_id(store, store_id).await
75}
76
77pub async fn store_set_by_id(
81 store: &Option<&Arc<TaskStore>>,
82 store_id: i64,
83 store_fields: &[(&str, serde_json::Value)],
84) -> anyhow::Result<()> {
85 if let Some(store) = store {
86 store.set_fields(store_id, store_fields).await?;
87 }
88 Ok(())
89}
90
91pub async fn store_set(
95 store: &Option<Arc<TaskStore>>,
96 repo: &str,
97 task_id: &str,
98 store_fields: &[(&str, serde_json::Value)],
99) {
100 if let Some(ref store) = store {
101 match store.resolve_task_id(repo, task_id).await {
102 Ok(Some(store_id)) => {
103 if let Err(e) = store.set_fields(store_id, store_fields).await {
104 tracing::warn!(task_id, error = %e, "store set_fields failed");
105 }
106 }
107 Ok(None) => {} Err(e) => {
109 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
110 }
111 }
112 }
113}
114
115pub async fn store_set_result_by_id(
121 store: &Option<Arc<TaskStore>>,
122 store_id: i64,
123 store_fields: &[(&str, serde_json::Value)],
124) -> anyhow::Result<()> {
125 if let Some(ref store) = store {
126 store.set_fields(store_id, store_fields).await?;
127 }
128 Ok(())
129}
130
131pub async fn store_set_result(
137 store: &Option<Arc<TaskStore>>,
138 repo: &str,
139 task_id: &str,
140 store_fields: &[(&str, serde_json::Value)],
141) -> anyhow::Result<()> {
142 if let Some(ref store) = store {
143 let store_id = store
144 .resolve_task_id(repo, task_id)
145 .await?
146 .ok_or_else(|| anyhow::anyhow!("task {} not found in store", task_id))?;
147 store.set_fields(store_id, store_fields).await?;
148 }
149 Ok(())
150}
151
152#[allow(dead_code)]
156pub async fn store_touch_updated_at_by_id(store: &Option<Arc<TaskStore>>, store_id: i64) {
157 if let Some(ref s) = store {
158 if let Err(e) = s.touch_updated_at(store_id).await {
159 tracing::warn!(store_id, error = %e, "store touch_updated_at failed");
160 }
161 }
162}
163
164pub async fn store_touch_updated_at(store: &Option<Arc<TaskStore>>, repo: &str, task_id: &str) {
168 if let Some(ref s) = store {
169 match s.resolve_task_id(repo, task_id).await {
170 Ok(Some(store_id)) => {
171 if let Err(e) = s.touch_updated_at(store_id).await {
172 tracing::warn!(task_id, error = %e, "store touch_updated_at failed");
173 } else {
174 tracing::debug!(task_id, store_id, "store touch_updated_at succeeded");
178 }
179 }
180 Ok(None) => {} Err(e) => {
182 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
183 }
184 }
185 }
186}
187
188#[allow(clippy::too_many_arguments)]
190pub async fn store_log_activity(
191 store: &Option<Arc<TaskStore>>,
192 repo: &str,
193 task_id: &str,
194 event_type: &str,
195 from_status: Option<&str>,
196 to_status: Option<&str>,
197 agent: Option<&str>,
198 model: Option<&str>,
199 details: Option<&serde_json::Value>,
200) {
201 if let Some(ref s) = store {
202 match s.resolve_task_id(repo, task_id).await {
203 Ok(Some(store_id)) => {
204 if let Err(e) = s
205 .append_activity(
206 store_id,
207 event_type,
208 from_status,
209 to_status,
210 agent,
211 model,
212 details,
213 )
214 .await
215 {
216 tracing::warn!(task_id, event_type, error = %e, "store append_activity failed");
217 }
218 }
219 Ok(None) => {} Err(e) => {
221 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
222 }
223 }
224 }
225}
226
227pub async fn review_session_expected(store: &Arc<TaskStore>, repo: &str, task_id: &str) -> bool {
228 match store.resolve_task_id(repo, task_id).await {
229 Ok(Some(store_id)) => store
230 .get(store_id)
231 .await
232 .map(|task| task.review_session_expected)
233 .unwrap_or(false),
234 Ok(None) | Err(_) => false,
235 }
236}
237
238pub async fn set_review_session_expected(
239 store: &Arc<TaskStore>,
240 repo: &str,
241 task_id: &str,
242 expected: bool,
243) {
244 store_set(
245 &Some(Arc::clone(store)),
246 repo,
247 task_id,
248 &[("review_session_expected", serde_json::json!(expected))],
249 )
250 .await;
251}
252
253pub async fn store_increment_by_id(
261 store: &Option<Arc<TaskStore>>,
262 store_id: i64,
263 field: &str,
264) -> anyhow::Result<u64> {
265 let s = store
266 .as_ref()
267 .ok_or_else(|| anyhow!("task store unavailable"))?;
268 match s.increment(store_id, field).await {
269 Ok(new_val) => Ok(new_val as u64),
270 Err(e) => Err(anyhow!("store.increment failed: {}", e)),
271 }
272}
273
274pub async fn store_increment(
282 store: &Option<Arc<TaskStore>>,
283 repo: &str,
284 task_id: &str,
285 field: &str,
286) -> anyhow::Result<u64> {
287 let s = store
288 .as_ref()
289 .ok_or_else(|| anyhow!("task store unavailable"))?;
290 match s.resolve_task_id(repo, task_id).await {
291 Ok(Some(store_id)) => store_increment_by_id(store, store_id, field).await,
292 Ok(None) => Err(anyhow!("task not present in store: {}/{}", repo, task_id)),
293 Err(e) => {
294 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
295 Err(anyhow!("resolve_task_id failed: {}", e))
296 }
297 }
298}
299
300pub async fn get_task_field(
302 store: &Option<Arc<TaskStore>>,
303 repo: &str,
304 task_id: &str,
305 field: &str,
306) -> Option<String> {
307 let task = opt_store_get_task(store, repo, task_id).await?;
308 match field {
309 "no_code_last_agent" => Some(task.no_code_last_agent),
310 _ => {
311 tracing::warn!(task_id, field, "get_task_field: unknown field");
312 None
313 }
314 }
315}
316
317pub async fn get_task_field_direct(
319 store: &Arc<TaskStore>,
320 repo: &str,
321 task_id: &str,
322 field: &str,
323) -> Option<String> {
324 let store_opt: &Option<Arc<TaskStore>> = &Some(store.clone());
325 get_task_field(store_opt, repo, task_id, field).await
326}
327
328pub async fn store_reset_counters(store: &Option<Arc<TaskStore>>, repo: &str, task_id: &str) {
330 if let Some(ref store) = store {
331 match store.resolve_task_id(repo, task_id).await {
332 Ok(Some(store_id)) => {
333 if let Err(e) = store.reset_counters(store_id).await {
334 tracing::warn!(task_id, err = %e, "store reset_counters failed");
335 }
336 }
337 Ok(None) => {} Err(e) => {
339 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
340 }
341 }
342 }
343}
344
345pub async fn store_reset_failure_counters(
350 store: &Option<Arc<TaskStore>>,
351 repo: &str,
352 task_id: &str,
353) {
354 if let Some(ref store) = store {
355 match store.resolve_task_id(repo, task_id).await {
356 Ok(Some(store_id)) => {
357 if let Err(e) = store.reset_failure_counters(store_id).await {
358 tracing::warn!(task_id, err = %e, "failed to reset failure counters after review dispatch");
359 }
360 }
361 Ok(None) => {} Err(e) => {
363 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
364 }
365 }
366 }
367}
368
369pub async fn get_token_usage_result(
371 store: &Option<Arc<TaskStore>>,
372 repo: &str,
373 task_id: &str,
374) -> StoreResult<TokenUsage> {
375 let s = store
376 .as_ref()
377 .ok_or_else(|| anyhow!("task store unavailable"))?;
378 let store_id = s
379 .resolve_task_id(repo, task_id)
380 .await?
381 .ok_or_else(|| anyhow!("task {}/{} not found in store", repo, task_id))?;
382 let task = s.get(store_id).await?;
383 Ok(Some(TokenUsage {
384 input_tokens: token_i64_to_u64_non_negative(task.input_tokens),
385 output_tokens: token_i64_to_u64_non_negative(task.output_tokens),
386 }))
387}
388
389pub async fn get_token_usage(
394 store: &Option<Arc<TaskStore>>,
395 repo: &str,
396 task_id: &str,
397) -> TokenUsage {
398 match get_token_usage_result(store, repo, task_id).await {
399 Ok(Some(usage)) => usage,
400 Ok(None) => TokenUsage::default(),
401 Err(e) => {
402 tracing::warn!(task_id, error = %e, "get_token_usage failed — returning zero usage");
403 TokenUsage::default()
404 }
405 }
406}
407
408pub async fn get_cost_estimate_result(
410 store: &Option<Arc<TaskStore>>,
411 repo: &str,
412 task_id: &str,
413) -> StoreResult<CostEstimate> {
414 let s = store
415 .as_ref()
416 .ok_or_else(|| anyhow!("task store unavailable"))?;
417 let store_id = s
418 .resolve_task_id(repo, task_id)
419 .await?
420 .ok_or_else(|| anyhow!("task {}/{} not found in store", repo, task_id))?;
421 let task = s.get(store_id).await?;
422 Ok(Some(CostEstimate {
423 input_cost_usd: task.input_cost_usd,
424 output_cost_usd: task.output_cost_usd,
425 total_cost_usd: task.total_cost_usd,
426 }))
427}
428
429pub async fn get_cost_estimate(
434 store: &Option<Arc<TaskStore>>,
435 repo: &str,
436 task_id: &str,
437) -> CostEstimate {
438 match get_cost_estimate_result(store, repo, task_id).await {
439 Ok(Some(cost)) => cost,
440 Ok(None) => CostEstimate::default(),
441 Err(e) => {
442 tracing::warn!(task_id, error = %e, "get_cost_estimate failed — returning zero cost");
443 CostEstimate::default()
444 }
445 }
446}
447
448pub async fn get_recent_memory_result(
450 store: &Option<Arc<TaskStore>>,
451 repo: &str,
452 task_id: &str,
453 max: usize,
454) -> StoreResult<Vec<MemoryEntry>> {
455 let s = store
456 .as_ref()
457 .ok_or_else(|| anyhow!("task store unavailable"))?;
458 let store_id = s
459 .resolve_task_id(repo, task_id)
460 .await?
461 .ok_or_else(|| anyhow!("task {}/{} not found in store", repo, task_id))?;
462 let entries = s.recent_memory(store_id, max).await?;
463 Ok(Some(entries))
464}
465
466pub async fn get_recent_memory(
471 store: &Option<Arc<TaskStore>>,
472 repo: &str,
473 task_id: &str,
474 max: usize,
475) -> Vec<MemoryEntry> {
476 match get_recent_memory_result(store, repo, task_id, max).await {
477 Ok(Some(entries)) => entries,
478 Ok(None) => Vec::new(),
479 Err(e) => {
480 tracing::warn!(task_id, error = %e, "get_recent_memory failed — returning empty memory");
481 Vec::new()
482 }
483 }
484}