Forked agents: stop gracefully on context overflow instead of compacting

Subconscious agents (observe, etc.) fork the conscious agent's context
to share the KV cache prefix. When a multi-step agent fills the context
window, compacting blows the KV cache and evicts the step prompts,
leaving the model with no idea what it was doing.

Fix: forked agents set no_compact=true. On overflow, turn() returns the
error immediately (no compact+retry), and run_with_backend catches it
and returns Ok — the output tool has already written results to
Subconscious.state, so collect_results still picks them up.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-08 23:00:12 -04:00
parent 850008ece7
commit 44a0bc376a
2 changed files with 24 additions and 8 deletions

View file

@ -155,6 +155,9 @@ pub struct AgentState {
pub generation: u64, pub generation: u64,
pub memory_scoring_in_flight: bool, pub memory_scoring_in_flight: bool,
pub active_tools: tools::ActiveTools, pub active_tools: tools::ActiveTools,
/// Forked agents should not compact on overflow — it blows the
/// KV cache prefix and evicts the step prompts.
pub no_compact: bool,
pub changed: Arc<tokio::sync::Notify>, pub changed: Arc<tokio::sync::Notify>,
} }
@ -214,6 +217,7 @@ impl Agent {
generation: 0, generation: 0,
memory_scoring_in_flight: false, memory_scoring_in_flight: false,
active_tools, active_tools,
no_compact: false,
changed: Arc::new(tokio::sync::Notify::new()), changed: Arc::new(tokio::sync::Notify::new()),
}), }),
}); });
@ -249,6 +253,7 @@ impl Agent {
generation: 0, generation: 0,
memory_scoring_in_flight: false, memory_scoring_in_flight: false,
active_tools: tools::ActiveTools::new(), active_tools: tools::ActiveTools::new(),
no_compact: true,
changed: Arc::new(tokio::sync::Notify::new()), changed: Arc::new(tokio::sync::Notify::new()),
}), }),
}) })
@ -354,12 +359,17 @@ impl Agent {
// Check for stream/parse errors // Check for stream/parse errors
match parser_handle.await { match parser_handle.await {
Ok(Err(e)) => { Ok(Err(e)) => {
if context::is_context_overflow(&e) && overflow_retries < 2 { if context::is_context_overflow(&e) {
overflow_retries += 1; if agent.state.lock().await.no_compact {
agent.state.lock().await.notify( return Err(e);
format!("context overflow — retrying ({}/2)", overflow_retries)); }
agent.compact().await; if overflow_retries < 2 {
continue; overflow_retries += 1;
agent.state.lock().await.notify(
format!("context overflow — retrying ({}/2)", overflow_retries));
agent.compact().await;
continue;
}
} }
return Err(e); return Err(e);
} }

View file

@ -186,8 +186,14 @@ impl AutoAgent {
for _ in 0..max_turns { for _ in 0..max_turns {
self.turn += 1; self.turn += 1;
let result = Agent::turn(backend.0.clone()).await let result = match Agent::turn(backend.0.clone()).await {
.map_err(|e| format!("{}: {}", self.name, e))?; Ok(r) => r,
Err(e) if super::context::is_context_overflow(&e) => {
dbglog!("[auto] {} context full, stopping gracefully", self.name);
return Ok(String::new());
}
Err(e) => return Err(format!("{}: {}", self.name, e)),
};
if result.had_tool_calls { if result.had_tool_calls {
continue; continue;