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
152pub async fn store_touch_updated_at(store: &Option<Arc<TaskStore>>, repo: &str, task_id: &str) {
156 if let Some(ref s) = store {
157 match s.resolve_task_id(repo, task_id).await {
158 Ok(Some(store_id)) => {
159 if let Err(e) = s.touch_updated_at(store_id).await {
160 tracing::warn!(task_id, error = %e, "store touch_updated_at failed");
161 } else {
162 tracing::debug!(task_id, store_id, "store touch_updated_at succeeded");
166 }
167 }
168 Ok(None) => {} Err(e) => {
170 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
171 }
172 }
173 }
174}
175
176#[allow(clippy::too_many_arguments)]
178pub async fn store_log_activity(
179 store: &Option<Arc<TaskStore>>,
180 repo: &str,
181 task_id: &str,
182 event_type: &str,
183 from_status: Option<&str>,
184 to_status: Option<&str>,
185 agent: Option<&str>,
186 model: Option<&str>,
187 details: Option<&serde_json::Value>,
188) {
189 if let Some(ref s) = store {
190 match s.resolve_task_id(repo, task_id).await {
191 Ok(Some(store_id)) => {
192 if let Err(e) = s
193 .append_activity(
194 store_id,
195 event_type,
196 from_status,
197 to_status,
198 agent,
199 model,
200 details,
201 )
202 .await
203 {
204 tracing::warn!(task_id, event_type, error = %e, "store append_activity failed");
205 }
206 }
207 Ok(None) => {} Err(e) => {
209 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
210 }
211 }
212 }
213}
214
215pub async fn store_increment_by_id(
223 store: &Option<Arc<TaskStore>>,
224 store_id: i64,
225 field: &str,
226) -> anyhow::Result<u64> {
227 let s = store
228 .as_ref()
229 .ok_or_else(|| anyhow!("task store unavailable"))?;
230 match s.increment(store_id, field).await {
231 Ok(new_val) => Ok(new_val as u64),
232 Err(e) => Err(anyhow!("store.increment failed: {}", e)),
233 }
234}
235
236pub async fn store_increment(
244 store: &Option<Arc<TaskStore>>,
245 repo: &str,
246 task_id: &str,
247 field: &str,
248) -> anyhow::Result<u64> {
249 let s = store
250 .as_ref()
251 .ok_or_else(|| anyhow!("task store unavailable"))?;
252 match s.resolve_task_id(repo, task_id).await {
253 Ok(Some(store_id)) => store_increment_by_id(store, store_id, field).await,
254 Ok(None) => Err(anyhow!("task not present in store: {}/{}", repo, task_id)),
255 Err(e) => {
256 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
257 Err(anyhow!("resolve_task_id failed: {}", e))
258 }
259 }
260}
261
262pub async fn get_task_field(
264 store: &Option<Arc<TaskStore>>,
265 repo: &str,
266 task_id: &str,
267 field: &str,
268) -> Option<String> {
269 let task = opt_store_get_task(store, repo, task_id).await?;
270 match field {
271 "no_code_last_agent" => Some(task.no_code_last_agent),
272 _ => {
273 tracing::warn!(task_id, field, "get_task_field: unknown field");
274 None
275 }
276 }
277}
278
279pub async fn get_task_field_direct(
281 store: &Arc<TaskStore>,
282 repo: &str,
283 task_id: &str,
284 field: &str,
285) -> Option<String> {
286 let store_opt: &Option<Arc<TaskStore>> = &Some(store.clone());
287 get_task_field(store_opt, repo, task_id, field).await
288}
289
290pub async fn store_reset_counters(store: &Option<Arc<TaskStore>>, repo: &str, task_id: &str) {
292 if let Some(ref store) = store {
293 match store.resolve_task_id(repo, task_id).await {
294 Ok(Some(store_id)) => {
295 if let Err(e) = store.reset_counters(store_id).await {
296 tracing::warn!(task_id, err = %e, "store reset_counters failed");
297 }
298 }
299 Ok(None) => {} Err(e) => {
301 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
302 }
303 }
304 }
305}
306
307pub async fn store_reset_failure_counters(
312 store: &Option<Arc<TaskStore>>,
313 repo: &str,
314 task_id: &str,
315) {
316 if let Some(ref store) = store {
317 match store.resolve_task_id(repo, task_id).await {
318 Ok(Some(store_id)) => {
319 if let Err(e) = store.reset_failure_counters(store_id).await {
320 tracing::warn!(task_id, err = %e, "failed to reset failure counters after review dispatch");
321 }
322 }
323 Ok(None) => {} Err(e) => {
325 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
326 }
327 }
328 }
329}
330
331pub async fn get_token_usage_result(
333 store: &Option<Arc<TaskStore>>,
334 repo: &str,
335 task_id: &str,
336) -> StoreResult<TokenUsage> {
337 let s = store
338 .as_ref()
339 .ok_or_else(|| anyhow!("task store unavailable"))?;
340 let store_id = s
341 .resolve_task_id(repo, task_id)
342 .await?
343 .ok_or_else(|| anyhow!("task {}/{} not found in store", repo, task_id))?;
344 let task = s.get(store_id).await?;
345 Ok(Some(TokenUsage {
346 input_tokens: token_i64_to_u64_non_negative(task.input_tokens),
347 output_tokens: token_i64_to_u64_non_negative(task.output_tokens),
348 }))
349}
350
351pub async fn get_token_usage(
356 store: &Option<Arc<TaskStore>>,
357 repo: &str,
358 task_id: &str,
359) -> TokenUsage {
360 match get_token_usage_result(store, repo, task_id).await {
361 Ok(Some(usage)) => usage,
362 Ok(None) => TokenUsage::default(),
363 Err(e) => {
364 tracing::warn!(task_id, error = %e, "get_token_usage failed — returning zero usage");
365 TokenUsage::default()
366 }
367 }
368}
369
370pub async fn get_cost_estimate_result(
372 store: &Option<Arc<TaskStore>>,
373 repo: &str,
374 task_id: &str,
375) -> StoreResult<CostEstimate> {
376 let s = store
377 .as_ref()
378 .ok_or_else(|| anyhow!("task store unavailable"))?;
379 let store_id = s
380 .resolve_task_id(repo, task_id)
381 .await?
382 .ok_or_else(|| anyhow!("task {}/{} not found in store", repo, task_id))?;
383 let task = s.get(store_id).await?;
384 Ok(Some(CostEstimate {
385 input_cost_usd: task.input_cost_usd,
386 output_cost_usd: task.output_cost_usd,
387 total_cost_usd: task.total_cost_usd,
388 }))
389}
390
391pub async fn get_cost_estimate(
396 store: &Option<Arc<TaskStore>>,
397 repo: &str,
398 task_id: &str,
399) -> CostEstimate {
400 match get_cost_estimate_result(store, repo, task_id).await {
401 Ok(Some(cost)) => cost,
402 Ok(None) => CostEstimate::default(),
403 Err(e) => {
404 tracing::warn!(task_id, error = %e, "get_cost_estimate failed — returning zero cost");
405 CostEstimate::default()
406 }
407 }
408}
409
410pub async fn get_recent_memory_result(
412 store: &Option<Arc<TaskStore>>,
413 repo: &str,
414 task_id: &str,
415 max: usize,
416) -> StoreResult<Vec<MemoryEntry>> {
417 let s = store
418 .as_ref()
419 .ok_or_else(|| anyhow!("task store unavailable"))?;
420 let store_id = s
421 .resolve_task_id(repo, task_id)
422 .await?
423 .ok_or_else(|| anyhow!("task {}/{} not found in store", repo, task_id))?;
424 let entries = s.recent_memory(store_id, max).await?;
425 Ok(Some(entries))
426}
427
428pub async fn get_recent_memory(
433 store: &Option<Arc<TaskStore>>,
434 repo: &str,
435 task_id: &str,
436 max: usize,
437) -> Vec<MemoryEntry> {
438 match get_recent_memory_result(store, repo, task_id, max).await {
439 Ok(Some(entries)) => entries,
440 Ok(None) => Vec::new(),
441 Err(e) => {
442 tracing::warn!(task_id, error = %e, "get_recent_memory failed — returning empty memory");
443 Vec::new()
444 }
445 }
446}