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 review_session_expected(store: &Arc<TaskStore>, repo: &str, task_id: &str) -> bool {
216 match store.resolve_task_id(repo, task_id).await {
217 Ok(Some(store_id)) => store
218 .get(store_id)
219 .await
220 .map(|task| task.review_session_expected)
221 .unwrap_or(false),
222 Ok(None) | Err(_) => false,
223 }
224}
225
226pub async fn set_review_session_expected(
227 store: &Arc<TaskStore>,
228 repo: &str,
229 task_id: &str,
230 expected: bool,
231) {
232 store_set(
233 &Some(Arc::clone(store)),
234 repo,
235 task_id,
236 &[("review_session_expected", serde_json::json!(expected))],
237 )
238 .await;
239}
240
241pub async fn store_increment_by_id(
249 store: &Option<Arc<TaskStore>>,
250 store_id: i64,
251 field: &str,
252) -> anyhow::Result<u64> {
253 let s = store
254 .as_ref()
255 .ok_or_else(|| anyhow!("task store unavailable"))?;
256 match s.increment(store_id, field).await {
257 Ok(new_val) => Ok(new_val as u64),
258 Err(e) => Err(anyhow!("store.increment failed: {}", e)),
259 }
260}
261
262pub async fn store_increment(
270 store: &Option<Arc<TaskStore>>,
271 repo: &str,
272 task_id: &str,
273 field: &str,
274) -> anyhow::Result<u64> {
275 let s = store
276 .as_ref()
277 .ok_or_else(|| anyhow!("task store unavailable"))?;
278 match s.resolve_task_id(repo, task_id).await {
279 Ok(Some(store_id)) => store_increment_by_id(store, store_id, field).await,
280 Ok(None) => Err(anyhow!("task not present in store: {}/{}", repo, task_id)),
281 Err(e) => {
282 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
283 Err(anyhow!("resolve_task_id failed: {}", e))
284 }
285 }
286}
287
288pub async fn get_task_field(
290 store: &Option<Arc<TaskStore>>,
291 repo: &str,
292 task_id: &str,
293 field: &str,
294) -> Option<String> {
295 let task = opt_store_get_task(store, repo, task_id).await?;
296 match field {
297 "no_code_last_agent" => Some(task.no_code_last_agent),
298 _ => {
299 tracing::warn!(task_id, field, "get_task_field: unknown field");
300 None
301 }
302 }
303}
304
305pub async fn get_task_field_direct(
307 store: &Arc<TaskStore>,
308 repo: &str,
309 task_id: &str,
310 field: &str,
311) -> Option<String> {
312 let store_opt: &Option<Arc<TaskStore>> = &Some(store.clone());
313 get_task_field(store_opt, repo, task_id, field).await
314}
315
316pub async fn store_reset_counters(store: &Option<Arc<TaskStore>>, repo: &str, task_id: &str) {
318 if let Some(ref store) = store {
319 match store.resolve_task_id(repo, task_id).await {
320 Ok(Some(store_id)) => {
321 if let Err(e) = store.reset_counters(store_id).await {
322 tracing::warn!(task_id, err = %e, "store reset_counters failed");
323 }
324 }
325 Ok(None) => {} Err(e) => {
327 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
328 }
329 }
330 }
331}
332
333pub async fn store_reset_failure_counters(
338 store: &Option<Arc<TaskStore>>,
339 repo: &str,
340 task_id: &str,
341) {
342 if let Some(ref store) = store {
343 match store.resolve_task_id(repo, task_id).await {
344 Ok(Some(store_id)) => {
345 if let Err(e) = store.reset_failure_counters(store_id).await {
346 tracing::warn!(task_id, err = %e, "failed to reset failure counters after review dispatch");
347 }
348 }
349 Ok(None) => {} Err(e) => {
351 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
352 }
353 }
354 }
355}
356
357pub async fn get_token_usage_result(
359 store: &Option<Arc<TaskStore>>,
360 repo: &str,
361 task_id: &str,
362) -> StoreResult<TokenUsage> {
363 let s = store
364 .as_ref()
365 .ok_or_else(|| anyhow!("task store unavailable"))?;
366 let store_id = s
367 .resolve_task_id(repo, task_id)
368 .await?
369 .ok_or_else(|| anyhow!("task {}/{} not found in store", repo, task_id))?;
370 let task = s.get(store_id).await?;
371 Ok(Some(TokenUsage {
372 input_tokens: token_i64_to_u64_non_negative(task.input_tokens),
373 output_tokens: token_i64_to_u64_non_negative(task.output_tokens),
374 }))
375}
376
377pub async fn get_token_usage(
382 store: &Option<Arc<TaskStore>>,
383 repo: &str,
384 task_id: &str,
385) -> TokenUsage {
386 match get_token_usage_result(store, repo, task_id).await {
387 Ok(Some(usage)) => usage,
388 Ok(None) => TokenUsage::default(),
389 Err(e) => {
390 tracing::warn!(task_id, error = %e, "get_token_usage failed — returning zero usage");
391 TokenUsage::default()
392 }
393 }
394}
395
396pub async fn get_cost_estimate_result(
398 store: &Option<Arc<TaskStore>>,
399 repo: &str,
400 task_id: &str,
401) -> StoreResult<CostEstimate> {
402 let s = store
403 .as_ref()
404 .ok_or_else(|| anyhow!("task store unavailable"))?;
405 let store_id = s
406 .resolve_task_id(repo, task_id)
407 .await?
408 .ok_or_else(|| anyhow!("task {}/{} not found in store", repo, task_id))?;
409 let task = s.get(store_id).await?;
410 Ok(Some(CostEstimate {
411 input_cost_usd: task.input_cost_usd,
412 output_cost_usd: task.output_cost_usd,
413 total_cost_usd: task.total_cost_usd,
414 }))
415}
416
417pub async fn get_cost_estimate(
422 store: &Option<Arc<TaskStore>>,
423 repo: &str,
424 task_id: &str,
425) -> CostEstimate {
426 match get_cost_estimate_result(store, repo, task_id).await {
427 Ok(Some(cost)) => cost,
428 Ok(None) => CostEstimate::default(),
429 Err(e) => {
430 tracing::warn!(task_id, error = %e, "get_cost_estimate failed — returning zero cost");
431 CostEstimate::default()
432 }
433 }
434}
435
436pub async fn get_recent_memory_result(
438 store: &Option<Arc<TaskStore>>,
439 repo: &str,
440 task_id: &str,
441 max: usize,
442) -> StoreResult<Vec<MemoryEntry>> {
443 let s = store
444 .as_ref()
445 .ok_or_else(|| anyhow!("task store unavailable"))?;
446 let store_id = s
447 .resolve_task_id(repo, task_id)
448 .await?
449 .ok_or_else(|| anyhow!("task {}/{} not found in store", repo, task_id))?;
450 let entries = s.recent_memory(store_id, max).await?;
451 Ok(Some(entries))
452}
453
454pub async fn get_recent_memory(
459 store: &Option<Arc<TaskStore>>,
460 repo: &str,
461 task_id: &str,
462 max: usize,
463) -> Vec<MemoryEntry> {
464 match get_recent_memory_result(store, repo, task_id, max).await {
465 Ok(Some(entries)) => entries,
466 Ok(None) => Vec::new(),
467 Err(e) => {
468 tracing::warn!(task_id, error = %e, "get_recent_memory failed — returning empty memory");
469 Vec::new()
470 }
471 }
472}