agent: don't hold agent lock across I/O

The agent lock was held for the entire duration of turn() — including
API streaming and tool dispatch awaits. This blocked the UI thread
whenever it needed the lock (render tick, compaction check, etc.),
causing 20+ second freezes.

Fix: turn() takes Arc<Mutex<Agent>> and manages locking internally.
Lock is held briefly for prepare/process phases, released during all
I/O (streaming, tool awaits, sleep retries). Also:

- check_compaction: spawns task instead of awaiting on event loop
- start_memory_scoring: already spawned, no change needed
- dispatch_tool_call_unlocked: drops lock before tool handle await
- Subconscious screen: renders all agents from state dynamically
  (no more hardcoded SUBCONSCIOUS_AGENTS list)
- Memory scoring shows n/m progress in snapshots

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-04 04:23:29 -04:00
parent 6fa881f811
commit fb54488f30
5 changed files with 301 additions and 269 deletions

View file

@ -110,6 +110,8 @@ pub struct AgentCycleState {
pub memory_scoring_in_flight: bool,
/// Latest per-memory scores from incremental scoring.
pub memory_scores: Vec<(String, f64)>,
/// Total unique memories in the context (updated when scoring starts).
pub memory_total: usize,
}
const AGENT_CYCLE_NAMES: &[&str] = &["surface-observe", "journal", "reflect"];
@ -139,6 +141,7 @@ impl AgentCycleState {
memory_score_cursor: 0,
memory_scoring_in_flight: false,
memory_scores: Vec::new(),
memory_total: 0,
}
}
@ -189,11 +192,11 @@ impl AgentCycleState {
name: "memory-scoring".to_string(),
pid: None,
phase: if self.memory_scoring_in_flight {
Some(format!("scoring (cursor: {})", self.memory_score_cursor))
Some(format!("scoring {}/{}", self.memory_scores.len(), self.memory_total))
} else if self.memory_scores.is_empty() {
None
} else {
Some(format!("{} memories scored", self.memory_scores.len()))
Some(format!("{}/{} scored", self.memory_scores.len(), self.memory_total))
},
log_path: None,
});