Start new sessions without the previous thread's history
Opening a new thread in Zed showed a [Conversation summary] from an earlier session. The session handlers cleared the on-device engine but not the history a cloud backend keeps for itself, and the startup routing to the cloud tier then carried that history into the backend it installed. A provider override had the same leak. new/load/fork now share begin_conversation, which also strips the active remote backend down to its system prompt before routing. Fixes #107 Co-Authored-By: siGit Code <noreply@sigit.si>
Seto Elkahfi committed
Sep 16, 2026 at 21:26 UTC
3c62e4c35d0cc66067c05c59669e48d6002012cb
2 files changed
+65
-30
src/backend.rs
+37
@@ -1200,6 +1200,21 @@ pub fn carryover_history(snapshot: Vec<serde_json::Value>) -> Vec<serde_json::Va
1200
carried
1201
}
1202
1203
+/// Drop everything from `backend`'s history except the system messages it
1204
+/// seeded for itself. Used at a session boundary: a remote backend keeps its own
1205
+/// history, so clearing the on-device engine alone leaves the previous thread
1206
+/// (compaction summary included) live there, and the next model switch or
1207
+/// startup routing carries it into the new session.
1208
+pub async fn clear_conversation(backend: &dyn InferenceBackend) {
1209
+ let seeded: Vec<serde_json::Value> = backend
1210
+ .history_snapshot()
1211
+ .await
1212
+ .into_iter()
1213
+ .take_while(|message| message["role"] == "system")
1214
+ .collect();
1215
+ backend.restore_history(seeded).await;
1216
+}
1217
+
1218
/// Replay `carried` (from [`carryover_history`]) into `backend`, on top of the
1219
/// system messages `backend` seeded for itself. Used when a model switch
1220
/// installs a new backend — or reloads the on-device engine, which wipes its
@@ -1478,6 +1493,28 @@ mod tests {
1493
assert_eq!(history[0]["content"], "new prompt");
1494
}
1495
1496
+ #[tokio::test]
1497
+ async fn clear_conversation_keeps_only_the_seeded_system_prompt() {
1498
+ let backend = OpenAiBackend::new("http://localhost", "", "m", Some("prompt".into()));
1499
+ adopt_carryover(
1500
+ &backend,
1501
+ vec![
1502
+ serde_json::json!({ "role": "user", "content": "[Conversation summary]\nold" }),
1503
+ serde_json::json!({ "role": "assistant", "content": "ok" }),
1504
+ ],
1505
+ )
1506
+ .await;
1507
+
1508
+ clear_conversation(&backend).await;
1509
+
1510
+ let history = backend.history_snapshot().await;
1511
+ assert_eq!(history.len(), 1);
1512
+ assert_eq!(history[0]["role"], "system");
1513
+ assert_eq!(history[0]["content"], "prompt");
1514
+ // Nothing left for a later backend switch to carry over.
1515
+ assert!(carryover_history(history).is_empty());
1516
+ }
1517
+
1518
#[test]
1519
fn estimate_tokens_scales_with_serialized_size() {
1520
assert_eq!(estimate_tokens(&[]), 0);
src/main.rs
+28
-30
@@ -1440,6 +1440,31 @@ impl SiGitAgent {
1440
additional_roots
1441
}
1442
1443
+ /// Start a clean conversation for a session entry point (new/load/fork):
1444
+ /// wipe the previous thread from the engine *and* the active backend, seed
1445
+ /// the session context, then route inference per the Local Inference mode.
1446
+ ///
1447
+ /// The backend has to be cleared too. A cloud tier or provider override
1448
+ /// keeps its own history, and `switch_to_cloud_tier` carries whatever it
1449
+ /// finds there into the backend it installs — so a new thread would open
1450
+ /// holding the last one's conversation.
1451
+ async fn begin_conversation(&self, cwd: &std::path::Path, additional_roots: &[PathBuf]) {
1452
+ self.engine.clear_history().await;
1453
+ self.engine
1454
+ .push_history(onde::inference::ChatMessage::system(
1455
+ session_context_message(cwd, additional_roots),
1456
+ ))
1457
+ .await;
1458
+
1459
+ let backend = self.backend.lock().await.clone();
1460
+ if backend.is_remote() {
1461
+ backend::clear_conversation(backend.as_ref()).await;
1462
+ }
1463
+
1464
+ // Honor the persisted Local Inference toggle (off + signed in → cloud).
1465
+ self.apply_startup_inference_mode().await;
1466
+ }
1467
+
1468
/// Save a session's history plus the sidecar that says where it ran.
1469
///
1470
/// The sidecar is written here rather than at session start so a thread the
@@ -1527,16 +1552,7 @@ impl SiGitAgent {
1552
permissions::reset_all();
1553
1554
// start from a clean slate; a stored session (below) replaces it
1530
- self.engine.clear_history().await;
1531
-
1532
- self.engine
1533
- .push_history(onde::inference::ChatMessage::system(
1534
- session_context_message(&args.cwd, &additional_roots),
1535
- ))
1536
- .await;
1537
-
1538
- // Honor the persisted Local Inference toggle (off + signed in → cloud).
1539
- self.apply_startup_inference_mode().await;
1555
+ self.begin_conversation(&args.cwd, &additional_roots).await;
1556
1557
// Durable sessions: when this session id was saved before, restore its
1558
// history into the active backend. The snapshot includes the system
@@ -1601,16 +1617,7 @@ impl SiGitAgent {
1617
let additional_roots = self.enter_session_roots(&args.cwd, &args.additional_directories);
1618
1619
// no persistence, so fork == fresh session
1604
- self.engine.clear_history().await;
1605
-
1606
- self.engine
1607
- .push_history(onde::inference::ChatMessage::system(
1608
- session_context_message(&args.cwd, &additional_roots),
1609
- ))
1610
- .await;
1611
-
1612
- // Honor the persisted Local Inference toggle (off + signed in → cloud).
1613
- self.apply_startup_inference_mode().await;
1620
+ self.begin_conversation(&args.cwd, &additional_roots).await;
1621
1622
let config_options = {
1623
let guard = self.current_model.lock().unwrap();
@@ -1648,16 +1655,7 @@ impl SiGitAgent {
1655
1656
let additional_roots = self.enter_session_roots(&args.cwd, &args.additional_directories);
1657
1651
- self.engine.clear_history().await;
1652
-
1653
- self.engine
1654
- .push_history(onde::inference::ChatMessage::system(
1655
- session_context_message(&args.cwd, &additional_roots),
1656
- ))
1657
- .await;
1658
-
1659
- // Honor the persisted Local Inference toggle (off + signed in → cloud).
1660
- self.apply_startup_inference_mode().await;
1658
+ self.begin_conversation(&args.cwd, &additional_roots).await;
1659
1660
let config_options = {
1661
let guard = self.current_model.lock().unwrap();