Shared forked agent — UI reads subconscious entries live

The forked agent is now behind Arc<tokio::sync::Mutex<Agent>>,
stored on SubconsciousAgent and passed to the spawned task. The
subconscious detail screen locks it via try_lock() to read entries
from the fork point — live during runs, persisted after completion.

Removes last_run_entries snapshot. Backend::Forked now holds the
shared Arc, all push operations go through the lock.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 03:09:06 -04:00
parent 77b68ecc50
commit 93f5f8b0c7
3 changed files with 78 additions and 81 deletions

View file

@ -285,15 +285,19 @@ const AGENTS: &[(&str, u64)] = &[
("subconscious-reflect", 100_000), // every ~100KB of conversation
];
/// Lightweight snapshot for the TUI.
#[derive(Clone, Default)]
/// Snapshot for the TUI — includes a handle to the forked agent
/// so the detail view can read entries live.
#[derive(Clone)]
pub struct SubconsciousSnapshot {
pub name: String,
pub running: bool,
pub current_phase: String,
pub turn: usize,
pub last_run_secs_ago: Option<f64>,
pub last_run_entries: Vec<ConversationEntry>,
/// Shared handle to the forked agent — UI locks to read entries.
pub forked_agent: Option<Arc<tokio::sync::Mutex<crate::agent::Agent>>>,
/// Entry index where the fork diverged.
pub fork_point: usize,
}
struct SubconsciousAgent {
@ -301,7 +305,11 @@ struct SubconsciousAgent {
auto: AutoAgent,
last_trigger_bytes: u64,
last_run: Option<Instant>,
last_run_entries: Vec<ConversationEntry>,
/// The forked agent for the current/last run. Shared with the
/// spawned task so the UI can read entries live.
forked_agent: Option<Arc<tokio::sync::Mutex<crate::agent::Agent>>>,
/// Entry index where the fork diverged from the conscious agent.
fork_point: usize,
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>,
}
@ -331,7 +339,7 @@ impl SubconsciousAgent {
Some(Self {
name: name.to_string(),
auto, last_trigger_bytes: 0, last_run: None,
last_run_entries: Vec::new(), handle: None,
forked_agent: None, fork_point: 0, handle: None,
})
}
@ -352,7 +360,8 @@ impl SubconsciousAgent {
current_phase: self.auto.current_phase.clone(),
turn: self.auto.turn,
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
last_run_entries: self.last_run_entries.clone(),
forked_agent: self.forked_agent.clone(),
fork_point: self.fork_point,
}
}
}
@ -393,7 +402,6 @@ impl Subconscious {
let (auto_back, result) = handle.await.unwrap_or_else(
|e| (AutoAgent::new(String::new(), vec![], vec![], 0.0, 0),
Err(format!("task panicked: {}", e))));
self.agents[idx].last_run_entries = auto_back.last_run_entries.clone();
self.agents[idx].auto = auto_back;
match result {
@ -490,11 +498,17 @@ impl Subconscious {
dbglog!("[subconscious] triggering {}", auto.name);
let forked = conscious.fork(auto.tools.clone());
let fork_point = forked.context.entries.len();
let shared_forked = Arc::new(tokio::sync::Mutex::new(forked));
self.agents[idx].forked_agent = Some(shared_forked.clone());
self.agents[idx].fork_point = fork_point;
let keys = memory_keys.clone();
let w = walked.clone();
self.agents[idx].handle = Some(tokio::spawn(async move {
let result = auto.run_forked(&forked, &keys, &w).await;
let result = auto.run_forked_shared(&shared_forked, &keys, &w).await;
(auto, result)
}));
}