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(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(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(store) = store {
143 let store_id = store
144 .resolve_task_id(repo, task_id)
145 .await?
146 .ok_or_else(|| anyhow::anyhow!("task {task_id} not found in store"))?;
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(s) = store {
157 match s.resolve_task_id(repo, task_id).await {
158 Ok(Some(store_id)) => {
159 match s.touch_updated_at(store_id).await {
160 Err(e) => {
161 tracing::warn!(task_id, error = %e, "store touch_updated_at failed");
162 }
163 _ => {
164 tracing::debug!(task_id, store_id, "store touch_updated_at succeeded");
168 }
169 }
170 }
171 Ok(None) => {} Err(e) => {
173 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
174 }
175 }
176 }
177}
178
179#[expect(
181 clippy::too_many_arguments,
182 reason = "the arguments are the activity row columns"
183)]
184pub async fn store_log_activity(
185 store: &Option<Arc<TaskStore>>,
186 repo: &str,
187 task_id: &str,
188 event_type: &str,
189 from_status: Option<&str>,
190 to_status: Option<&str>,
191 agent: Option<&str>,
192 model: Option<&str>,
193 details: Option<&serde_json::Value>,
194) {
195 if let Some(s) = store {
196 match s.resolve_task_id(repo, task_id).await {
197 Ok(Some(store_id)) => {
198 if let Err(e) = s
199 .append_activity(
200 store_id,
201 event_type,
202 from_status,
203 to_status,
204 agent,
205 model,
206 details,
207 )
208 .await
209 {
210 tracing::warn!(task_id, event_type, error = %e, "store append_activity failed");
211 }
212 }
213 Ok(None) => {} Err(e) => {
215 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
216 }
217 }
218 }
219}
220
221pub async fn store_increment_by_id(
229 store: &Option<Arc<TaskStore>>,
230 store_id: i64,
231 field: &str,
232) -> anyhow::Result<u64> {
233 let s = store
234 .as_ref()
235 .ok_or_else(|| anyhow!("task store unavailable"))?;
236 match s.increment(store_id, field).await {
237 Ok(new_val) => Ok(u64::try_from(new_val).unwrap_or(0)),
238 Err(e) => Err(anyhow!("store.increment failed: {e}")),
239 }
240}
241
242pub async fn store_increment(
250 store: &Option<Arc<TaskStore>>,
251 repo: &str,
252 task_id: &str,
253 field: &str,
254) -> anyhow::Result<u64> {
255 let s = store
256 .as_ref()
257 .ok_or_else(|| anyhow!("task store unavailable"))?;
258 match s.resolve_task_id(repo, task_id).await {
259 Ok(Some(store_id)) => store_increment_by_id(store, store_id, field).await,
260 Ok(None) => Err(anyhow!("task not present in store: {repo}/{task_id}")),
261 Err(e) => {
262 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
263 Err(anyhow!("resolve_task_id failed: {e}"))
264 }
265 }
266}
267
268pub async fn get_task_field(
270 store: &Option<Arc<TaskStore>>,
271 repo: &str,
272 task_id: &str,
273 field: &str,
274) -> Option<String> {
275 let task = opt_store_get_task(store, repo, task_id).await?;
276 if field == "no_code_last_agent" {
277 Some(task.no_code_last_agent)
278 } else {
279 tracing::warn!(task_id, field, "get_task_field: unknown field");
280 None
281 }
282}
283
284pub async fn get_task_field_direct(
286 store: &Arc<TaskStore>,
287 repo: &str,
288 task_id: &str,
289 field: &str,
290) -> Option<String> {
291 let store_opt: &Option<Arc<TaskStore>> = &Some(std::sync::Arc::clone(store));
292 get_task_field(store_opt, repo, task_id, field).await
293}
294
295pub async fn store_reset_counters(store: &Option<Arc<TaskStore>>, repo: &str, task_id: &str) {
297 if let Some(store) = store {
298 match store.resolve_task_id(repo, task_id).await {
299 Ok(Some(store_id)) => {
300 if let Err(e) = store.reset_counters(store_id).await {
301 tracing::warn!(task_id, err = %e, "store reset_counters failed");
302 }
303 }
304 Ok(None) => {} Err(e) => {
306 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
307 }
308 }
309 }
310}
311
312pub async fn store_reset_failure_counters(
317 store: &Option<Arc<TaskStore>>,
318 repo: &str,
319 task_id: &str,
320) {
321 if let Some(store) = store {
322 match store.resolve_task_id(repo, task_id).await {
323 Ok(Some(store_id)) => {
324 if let Err(e) = store.reset_failure_counters(store_id).await {
325 tracing::warn!(task_id, err = %e, "failed to reset failure counters after review dispatch");
326 }
327 }
328 Ok(None) => {} Err(e) => {
330 tracing::warn!(task_id, error = %e, "resolve_task_id failed in store write helper");
331 }
332 }
333 }
334}
335
336pub async fn get_token_usage_result(
338 store: &Option<Arc<TaskStore>>,
339 repo: &str,
340 task_id: &str,
341) -> StoreResult<TokenUsage> {
342 let s = store
343 .as_ref()
344 .ok_or_else(|| anyhow!("task store unavailable"))?;
345 let store_id = s
346 .resolve_task_id(repo, task_id)
347 .await?
348 .ok_or_else(|| anyhow!("task {repo}/{task_id} not found in store"))?;
349 let task = s.get(store_id).await?;
350 Ok(Some(TokenUsage {
351 input_tokens: token_i64_to_u64_non_negative(task.input_tokens),
352 output_tokens: token_i64_to_u64_non_negative(task.output_tokens),
353 }))
354}
355
356pub async fn get_token_usage(
361 store: &Option<Arc<TaskStore>>,
362 repo: &str,
363 task_id: &str,
364) -> TokenUsage {
365 match get_token_usage_result(store, repo, task_id).await {
366 Ok(Some(usage)) => usage,
367 Ok(None) => TokenUsage::default(),
368 Err(e) => {
369 tracing::warn!(task_id, error = %e, "get_token_usage failed — returning zero usage");
370 TokenUsage::default()
371 }
372 }
373}
374
375pub async fn get_cost_estimate_result(
377 store: &Option<Arc<TaskStore>>,
378 repo: &str,
379 task_id: &str,
380) -> StoreResult<CostEstimate> {
381 let s = store
382 .as_ref()
383 .ok_or_else(|| anyhow!("task store unavailable"))?;
384 let store_id = s
385 .resolve_task_id(repo, task_id)
386 .await?
387 .ok_or_else(|| anyhow!("task {repo}/{task_id} not found in store"))?;
388 let task = s.get(store_id).await?;
389 Ok(Some(CostEstimate {
390 input_cost_usd: task.input_cost_usd,
391 output_cost_usd: task.output_cost_usd,
392 total_cost_usd: task.total_cost_usd,
393 }))
394}
395
396pub async fn get_cost_estimate(
401 store: &Option<Arc<TaskStore>>,
402 repo: &str,
403 task_id: &str,
404) -> CostEstimate {
405 match get_cost_estimate_result(store, repo, task_id).await {
406 Ok(Some(cost)) => cost,
407 Ok(None) => CostEstimate::default(),
408 Err(e) => {
409 tracing::warn!(task_id, error = %e, "get_cost_estimate failed — returning zero cost");
410 CostEstimate::default()
411 }
412 }
413}
414
415pub async fn get_recent_memory_result(
417 store: &Option<Arc<TaskStore>>,
418 repo: &str,
419 task_id: &str,
420 max: usize,
421) -> StoreResult<Vec<MemoryEntry>> {
422 let s = store
423 .as_ref()
424 .ok_or_else(|| anyhow!("task store unavailable"))?;
425 let store_id = s
426 .resolve_task_id(repo, task_id)
427 .await?
428 .ok_or_else(|| anyhow!("task {repo}/{task_id} not found in store"))?;
429 let entries = s.recent_memory(store_id, max).await?;
430 Ok(Some(entries))
431}
432
433pub async fn get_recent_memory(
438 store: &Option<Arc<TaskStore>>,
439 repo: &str,
440 task_id: &str,
441 max: usize,
442) -> Vec<MemoryEntry> {
443 match get_recent_memory_result(store, repo, task_id, max).await {
444 Ok(Some(entries)) => entries,
445 Ok(None) => Vec::new(),
446 Err(e) => {
447 tracing::warn!(task_id, error = %e, "get_recent_memory failed — returning empty memory");
448 Vec::new()
449 }
450 }
451}