diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 81bcb09..c2cb365 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -320,7 +320,7 @@ impl MindState { /// Background task completion events. enum BgEvent { ScoringDone, - FinetuneCandidates(Vec), + FinetuneCandidate(learn::FinetuneCandidate), } // --- Mind: cognitive state machine --- @@ -656,7 +656,12 @@ impl Mind { /// once this runs continuously, we'll just train whatever lands at full /// context without filtering. pub fn start_finetune_scoring(&self) { - let threshold = self.shared.lock().unwrap().learn_threshold; + let threshold = { + let mut s = self.shared.lock().unwrap(); + // Clear the previous run's candidates so this run's stream in fresh. + s.finetune_candidates.clear(); + s.learn_threshold + }; let agent = self.agent.clone(); let bg_tx = self.bg_tx.clone(); @@ -678,12 +683,12 @@ impl Mind { activity.update(format!("finetune: scoring {} responses...", responses_considered)).await; + let bg_tx_cb = bg_tx.clone(); let stats = match learn::score_finetune_candidates( - &context, score_count, &client, threshold, + &context, score_count, &client, threshold, &activity, + |c| { let _ = bg_tx_cb.send(BgEvent::FinetuneCandidate(c)); }, ).await { - Ok((candidates, max_div)) => { - let above_threshold = candidates.len(); - let _ = bg_tx.send(BgEvent::FinetuneCandidates(candidates)); + Ok((above_threshold, max_div)) => { FinetuneScoringStats { responses_considered, above_threshold, @@ -801,8 +806,8 @@ impl Mind { BgEvent::ScoringDone => { self.shared.lock().unwrap().scoring_in_flight = false; } - BgEvent::FinetuneCandidates(candidates) => { - self.shared.lock().unwrap().finetune_candidates = candidates; + BgEvent::FinetuneCandidate(c) => { + self.shared.lock().unwrap().finetune_candidates.push(c); } } } diff --git a/src/subconscious/learn.rs b/src/subconscious/learn.rs index 3c12efc..2424fa5 100644 --- a/src/subconscious/learn.rs +++ b/src/subconscious/learn.rs @@ -492,22 +492,28 @@ pub struct FinetuneCandidate { /// Score and enrich finetune candidates with full context. /// -/// Returns (candidates, max_divergence) - candidates ready for review with -/// context/continuation token IDs, and the highest divergence seen. +/// Candidates are delivered via `on_candidate` one-at-a-time as they become +/// ready: scoring happens once (one /score call), then for each candidate +/// that passes the threshold we optionally generate an alternate response +/// and then emit it. The activity status is updated during the alternate +/// phase so the UI doesn't look stuck. +/// +/// Returns (count_above_threshold, max_divergence). pub async fn score_finetune_candidates( context: &ContextState, count: usize, client: &ApiClient, min_divergence: f64, -) -> anyhow::Result<(Vec, f64)> { + activity: &crate::agent::ActivityGuard, + mut on_candidate: impl FnMut(FinetuneCandidate), +) -> anyhow::Result<(usize, f64)> { let scores = score_finetune(context, count, client).await?; let max_divergence = scores.iter().map(|(_, d)| *d).fold(0.0f64, f64::max); let entries = context.conversation(); - let mut candidates = Vec::new(); - let trained = load_trained(); + let mut candidates: Vec = Vec::new(); for (entry_idx, divergence) in scores { if divergence < min_divergence { @@ -522,7 +528,7 @@ pub async fn score_finetune_candidates( continue; } - // Extract response text + // Extract response text. let response_text = match node { AstNode::Branch { children, .. } => { children.iter() @@ -536,7 +542,7 @@ pub async fn score_finetune_candidates( _ => continue, }; - // Build token IDs: context = everything before response, continuation = response + // Build token IDs: context = everything before response, continuation = response. let (context_ids, _) = build_token_ids(context, 0..entry_idx, Filter::None); let continuation_ids: Vec = node.token_ids().into_iter().collect(); @@ -551,17 +557,23 @@ pub async fn score_finetune_candidates( }); } - // Generate alternates if enabled - if alternates_enabled() && !candidates.is_empty() { - for candidate in &mut candidates { + let total = candidates.len(); + let gen_alternates = alternates_enabled() && total > 0; + + for (i, mut candidate) in candidates.into_iter().enumerate() { + if gen_alternates { + activity.update( + format!("finetune: generating alternate {}/{}", i + 1, total) + ).await; match generate_alternate(context, candidate.entry_idx, client).await { Ok(text) => candidate.alternate_text = Some(text), Err(e) => dbglog!("[finetune] alternate generation failed: {:#}", e), } } + on_candidate(candidate); } - Ok((candidates, max_divergence)) + Ok((total, max_divergence)) } /// Generate what the model would say without memories for a given entry.