From bf25b39dffb6f646263b54578fb26072ecef9667 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:27:48 +0400 Subject: [PATCH] fix: count retry credits --- src-tauri/src/agent_tasks/batch.rs | 24 +++++++++++++++- src-tauri/src/agent_tasks/mod.rs | 46 +++++++++++++++++++++++------- src-tauri/src/agent_tasks/model.rs | 14 ++++++++- 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/agent_tasks/batch.rs b/src-tauri/src/agent_tasks/batch.rs index b105ab7c..6f34c5a8 100644 --- a/src-tauri/src/agent_tasks/batch.rs +++ b/src-tauri/src/agent_tasks/batch.rs @@ -155,7 +155,7 @@ impl Batch { pub fn tally(&self) -> Tally { let mut tally = Tally::default(); for task in &self.tasks { - tally.credits_used += task.credits_used; + tally.credits_used += task.total_credits(); match task.status { TaskStatus::Queued => tally.queued += 1, TaskStatus::Done => tally.done += 1, @@ -275,4 +275,26 @@ mod tests { assert_eq!(batch.tasks[1].status, TaskStatus::Queued); assert_eq!(batch.take_next(), Some(1)); } + + #[test] + fn a_retry_keeps_the_credits_of_every_attempt() { + let mut batch = batch(2, 2); + batch.tasks[0].status = TaskStatus::Done; + batch.tasks[0].credits_used = 10; + batch.tasks[1].fail("AGENT_BUSY".into(), 1); + batch.tasks[1].credits_used = 7; + assert_eq!(batch.retry_failed(), 1); + assert_eq!( + batch.tasks[1].credits_used, 0, + "the new attempt starts at zero" + ); + assert_eq!(batch.tasks[1].total_credits(), 7); + assert_eq!(batch.tally().credits_used, 17); + batch.tasks[1].credits_used = 4; + batch.tasks[1].fail("AGENT_BUSY".into(), 2); + batch.retry_failed(); + batch.tasks[1].credits_used = 3; + assert_eq!(batch.tasks[1].total_credits(), 14); + assert_eq!(batch.tally().credits_used, 24); + } } diff --git a/src-tauri/src/agent_tasks/mod.rs b/src-tauri/src/agent_tasks/mod.rs index 7c876540..71c89b5d 100644 --- a/src-tauri/src/agent_tasks/mod.rs +++ b/src-tauri/src/agent_tasks/mod.rs @@ -153,7 +153,7 @@ impl From<&Task> for TaskView { approval: task.approval.clone(), result: task.result.clone(), error: task.error.clone(), - credits_used: task.credits_used, + credits_used: task.total_credits(), started_at: task.started_at, finished_at: task.finished_at, } @@ -379,15 +379,10 @@ fn finalize(batch_id: &str, index: usize) { task.profile_id, task.task_id.as_deref().unwrap_or("-"), task.status.run_status(), - task.credits_used + task.total_credits() ); if let Some(run_id) = task.run_id.clone() { - let patch = cloud::run_patch( - task.status.run_status(), - task.result.as_deref(), - task.credits_used, - task.error.as_deref(), - ); + let patch = final_report(&task); tokio::spawn(async move { cloud::report_run(&run_id, patch).await }); } if task.launched_here && headless { @@ -397,6 +392,16 @@ fn finalize(batch_id: &str, index: usize) { waker.notify_one(); } +/// What the cloud records when a task ends: every attempt's credits count. +fn final_report(task: &Task) -> Value { + cloud::run_patch( + task.status.run_status(), + task.result.as_deref(), + task.total_credits(), + task.error.as_deref(), + ) +} + fn load_profile(profile_id: &str) -> Result { crate::profile::ProfileManager::instance() .list_profiles() @@ -554,14 +559,15 @@ async fn start_task(batch_id: String, index: usize, profile_id: String, order: O let run = task .run_id .clone() - .filter(|_| !finished && !task.status.is_final()); + .filter(|_| !finished && !task.status.is_final()) + .map(|run_id| (run_id, task.total_credits())); publish(batch); (finished, run) }; match finished { (true, _) => finalize(&batch_id, index), - (false, Some(run_id)) => { - cloud::report_run(&run_id, cloud::run_patch("running", None, 0, None)).await; + (false, Some((run_id, spent))) => { + cloud::report_run(&run_id, cloud::run_patch("running", None, spent, None)).await; } _ => {} } @@ -1121,6 +1127,24 @@ pub async fn run_agent_routine( mod tests { use super::*; + #[test] + fn the_final_report_counts_the_credits_of_every_attempt() { + let mut task = Task::queued("p1".into()); + task.run_id = Some("r1".into()); + task.credits_used = 12; + task.fail("AGENT_BUSY".into(), 1); + task.requeue(); + assert_eq!(task.run_id.as_deref(), Some("r1")); + task.credits_used = 30; + task.status = TaskStatus::Done; + task.result = Some("Found it".into()); + assert_eq!( + final_report(&task), + json!({ "status": "done", "creditsUsed": 42, "summary": "Found it" }) + ); + assert_eq!(TaskView::from(&task).credits_used, 42); + } + #[test] fn a_start_carries_only_what_was_asked() { let order = Order { diff --git a/src-tauri/src/agent_tasks/model.rs b/src-tauri/src/agent_tasks/model.rs index 4c507938..a4540473 100644 --- a/src-tauri/src/agent_tasks/model.rs +++ b/src-tauri/src/agent_tasks/model.rs @@ -167,7 +167,10 @@ pub struct Task { pub result: Option, /// A backend error code, or the engine's own words when it failed. pub error: Option, + /// Credits of this attempt, as the engine reports them. pub credits_used: u64, + /// Credits of the earlier attempts: a retry spends them again, never back. + pub credits_before: u64, pub started_at: Option, pub finished_at: Option, /// Donut started this browser for the task, so it stops it after. @@ -187,17 +190,26 @@ impl Task { result: None, error: None, credits_used: 0, + credits_before: 0, started_at: None, finished_at: None, launched_here: false, } } - /// Back to the queue for another attempt; the run record stays. + /// Back to the queue for another attempt; the run record and the credits + /// spent so far stay. pub fn requeue(&mut self) { let run_id = self.run_id.take(); + let spent = self.total_credits(); *self = Self::queued(std::mem::take(&mut self.profile_id)); self.run_id = run_id; + self.credits_before = spent; + } + + /// Credits of every attempt. + pub fn total_credits(&self) -> u64 { + self.credits_before + self.credits_used } pub fn fail(&mut self, code: String, now_ms: u64) {