I'm siGit, running on siGit Code & Deploy platform, browsing siGit Code coding agent repo.
Fail prompts on a session whose directory is gone
install_session skipped set_current_dir when the session's cwd no longer existed and said nothing, so the process stayed in the previous session's directory and read_file, edit_file and run_command acted on the wrong project. A prompt on such a session now fails with resource not found, and a turn that reinstalls its session mid-wait is cancelled. session/load, resume and fork still succeed so the saved history of a deleted project stays readable. install_session also logs a warning when the directory is gone. Fixes #179 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: siGit Code v1.7.2-headless <noreply@sigit.si>
Seto Elkahfi committed
Oct 19, 2026 at 23:19 UTC
89bc619e0cff770520e885cbe1200b4496a6a12c
2 files changed
+87
-5
CHANGELOG.md
+11
@@ -1,5 +1,16 @@
1
# Changelog
2
3
+## Unreleased
4
+
5
+### Fixed
6
+
7
+- **A thread whose directory is gone no longer runs tools elsewhere.** The
8
+ process stayed in whichever directory the previous thread left it in, so
9
+ `read_file`, `edit_file` and `run_command` acted on the wrong project. A
10
+ prompt on such a thread now fails with resource not found, and a turn that
11
+ loses its directory mid-wait is cancelled. `session/load`, `session/resume`
12
+ and `session/fork` still succeed, so the saved history stays readable (#179).
13
+
14
## 1.7.2
15
16
The interactive terminal tells the program that launched it what it is doing,
crates/cli/src/main.rs
+76
-5
@@ -2587,7 +2587,7 @@ impl SiGitAgent {
2587
cancellation: &PromptCancellation,
2588
) {
2589
if workspace.reacquire().await
2590
- && let Err(error) = self.activate_session(session_id).await
2590
+ && let Err(error) = self.reinstall_for_tools(session_id).await
2591
{
2592
log::warn!(
2593
"session({session_id}): could not reinstall mid-turn, cancelling the turn: {}",
@@ -2598,6 +2598,30 @@ impl SiGitAgent {
2598
}
2599
}
2600
2601
+ /// Make `session_id` the live session and check that its directory is still
2602
+ /// there. Tools resolve relative paths against the process directory, and
2603
+ /// `install_session` leaves that alone when the session's `cwd` is gone, so
2604
+ /// a prompt or a tool run on such a session would act in the previous
2605
+ /// session's directory. Load, resume and fork do not call this: they
2606
+ /// succeed for a deleted project so a client can still show its history.
2607
+ async fn reinstall_for_tools(
2608
+ &self,
2609
+ session_id: &SessionId,
2610
+ ) -> agent_client_protocol::Result<()> {
2611
+ self.activate_session(session_id).await?;
2612
+ let cwd = self.session_cwd.lock().unwrap().clone();
2613
+ match cwd {
2614
+ Some(cwd) if !cwd.is_dir() => Err(agent_client_protocol::Error::new(
2615
+ ErrorCode::ResourceNotFound.into(),
2616
+ format!(
2617
+ "the working directory of session {session_id} no longer exists: {}",
2618
+ cwd.display()
2619
+ ),
2620
+ )),
2621
+ _ => Ok(()),
2622
+ }
2623
+ }
2624
+
2625
/// Point the process at `state`'s roots and load its conversation.
2626
///
2627
/// The process follows `cwd` because tool calls may use relative paths; the
@@ -2619,9 +2643,12 @@ impl SiGitAgent {
2643
}
2644
workspace::replace_additional_roots(additional_roots.clone());
2645
mcp::set_session_servers(mcp_servers);
2622
- if cwd.is_dir()
2623
- && let Err(err) = std::env::set_current_dir(&cwd)
2624
- {
2646
+ if !cwd.is_dir() {
2647
+ log::warn!(
2648
+ "session({key}): cwd {} no longer exists, prompts and tools on it will fail",
2649
+ cwd.display()
2650
+ );
2651
+ } else if let Err(err) = std::env::set_current_dir(&cwd) {
2652
log::warn!("could not set cwd to {}: {err}", cwd.display());
2653
}
2654
*self.active_session.lock().unwrap() = Some(key.to_string());
@@ -3270,7 +3297,7 @@ impl SiGitAgent {
3297
if cancellation.cancelled.load(Ordering::Acquire) {
3298
return Ok(PromptResponse::new(StopReason::Cancelled));
3299
}
3273
- self.activate_session(&session_id).await?;
3300
+ self.reinstall_for_tools(&session_id).await?;
3301
if cancellation.cancelled.load(Ordering::Acquire) {
3302
return Ok(PromptResponse::new(StopReason::Cancelled));
3303
}
@@ -7609,6 +7636,50 @@ mod tests {
7636
assert!(agent.active_session.lock().unwrap().is_none());
7637
}
7638
7639
+ #[tokio::test]
7640
+ #[allow(clippy::await_holding_lock)] // the lock guards process-global env for the whole test
7641
+ async fn a_session_whose_directory_is_gone_loads_but_refuses_tools() {
7642
+ let _guard = crate::ENV_TEST_LOCK
7643
+ .lock()
7644
+ .unwrap_or_else(|poisoned| poisoned.into_inner());
7645
+ let agent = SiGitAgent::new(
7646
+ Arc::new(ChatEngine::new()),
7647
+ GgufModelConfig::qwen25_3b(),
7648
+ Arc::new(AtomicBool::new(true)),
7649
+ Arc::new(AtomicBool::new(false)),
7650
+ Arc::new(std::sync::Mutex::new(None)),
7651
+ false,
7652
+ false,
7653
+ );
7654
+ let dir = std::env::temp_dir().join(format!("sigit_gone_cwd_{}", std::process::id()));
7655
+ std::fs::create_dir_all(&dir).unwrap();
7656
+ let thread = SessionId::new("gone-cwd-thread");
7657
+ agent
7658
+ .sessions
7659
+ .lock()
7660
+ .unwrap()
7661
+ .insert("gone-cwd-thread".into(), agent.session_state(&dir, &[]));
7662
+ std::fs::remove_dir_all(&dir).unwrap();
7663
+
7664
+ // Load, resume and fork only activate, so they still succeed.
7665
+ agent.activate_session(&thread).await.unwrap();
7666
+
7667
+ // A prompt or a tool run does not.
7668
+ *agent.active_session.lock().unwrap() = None;
7669
+ let error = agent.reinstall_for_tools(&thread).await.unwrap_err();
7670
+ assert_eq!(error.code, ErrorCode::ResourceNotFound);
7671
+ assert!(error.message.contains("no longer exists"));
7672
+
7673
+ let cancellation = PromptCancellation::default();
7674
+ let mut workspace = WorkspaceHold::acquire(&agent.workspace_lock).await;
7675
+ workspace.release();
7676
+ *agent.active_session.lock().unwrap() = None;
7677
+ agent
7678
+ .resume_workspace(&mut workspace, &thread, &cancellation)
7679
+ .await;
7680
+ assert!(cancellation.cancelled.load(Ordering::Acquire));
7681
+ }
7682
+
7683
#[test]
7684
fn session_context_message_lists_every_root_of_a_multi_root_project() {
7685
let primary = PathBuf::from("/tmp/primary");