@sigitcode-old / sigit / commits / 89bc619
Fork 1

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");