From 3a40ae27e76df9a4c57d9a187fcf18d0cdbeb199 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Tue, 19 May 2026 10:27:01 +0000 Subject: [PATCH] runtime: don't record an unmodified temp as the --resume target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On launch an empty temp project is created but, by design (ADR-0015), auto-deleted on quit while still empty. The unconditional `write_last_project` at startup recorded that temp's path anyway, so a later `--resume` resolved to a since-deleted directory and printed a confusing "recorded project … no longer exists". All three resume-pointer writes are now gated on `!project.is_unmodified_temp()`: the startup write, the on-switch write (a `new`-command switch to a fresh temp no longer records it), and a new on-quit write. The quit write is where a launch-temp the user *filled with content* finally gets remembered — startup skipped it while it was still empty. An unmodified empty temp is deleted, never recorded; the two dispositions are mutually exclusive. The "no previous project" friendly error the user asked for already exists (`project.resume_no_previous`, wired in the resume resolution) — verified, no change needed. The gate predicate `is_unmodified_temp` is covered by existing integration tests. 1131 passing, clippy clean. --- src/runtime.rs | 62 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/src/runtime.rs b/src/runtime.rs index 7ba898c..9921b6e 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -174,11 +174,18 @@ pub async fn run(args: Args) -> Result<()> { ); } - // Record the just-opened project as the new resume target. - // Write failures here are non-fatal: --resume on the next - // launch will report the missing/stale state, which is the - // safer default than refusing to launch. - if let Err(e) = write_last_project(&data_root, project.path()) { + // Record the just-opened project as the `--resume` target — + // but only if it is worth resuming. A freshly-created empty + // temp project is NOT recorded: it is auto-deleted on quit + // while still empty (ADR-0015), and a `last_project` + // pointing at a since-deleted directory makes the next + // `--resume` fail confusingly. A named project, or a temp + // that already carries content, is recorded. A temp that + // *gains* content this session is recorded on quit instead. + // Write failures are non-fatal. + if !project.is_unmodified_temp() + && let Err(e) = write_last_project(&data_root, project.path()) + { warn!(error = %e, "could not update last_project"); } let db_path = project.db_path(); @@ -482,18 +489,29 @@ async fn run_loop( let _ = tokio::time::timeout(SHUTDOWN_GRACE, reader_handle).await; - // Auto-delete the active project on quit if it's an - // unmodified temp — same rule as on project switch (see - // perform_switch). Captures the path first, drops the - // project (releasing the lock), then asks - // safely_delete_temp_project to verify the directory - // before removing it. - let cleanup_on_quit: Option = session - .project - .as_ref() + // Decide the active project's fate on quit, before dropping + // it. An unmodified empty temp is auto-deleted — same rule + // as on project switch (see perform_switch). Anything else — + // a named project, or a temp that *gained* content this + // session — is recorded as the `--resume` target: this is + // the point at which a launch-temp the user filled with + // content finally gets remembered (the `run()` startup + // write skipped it while it was still empty). The two are + // mutually exclusive (one needs an unmodified temp, the + // other anything else). + let project_at_quit = session.project.as_ref(); + let cleanup_on_quit: Option = project_at_quit .and_then(|p| p.is_unmodified_temp().then(|| p.path().to_path_buf())); + let resume_target_on_quit: Option = project_at_quit + .filter(|p| !p.is_unmodified_temp()) + .map(|p| p.path().to_path_buf()); let _ = session.database.take(); let _ = session.project.take(); + if let Some(target) = resume_target_on_quit + && let Err(e) = write_last_project(&session.data_root, &target) + { + tracing::warn!(error = %e, "could not update last_project on quit"); + } if let Some(stale) = cleanup_on_quit { match safely_delete_temp_project(&stale, &session.data_root) { Ok(()) => tracing::info!( @@ -754,6 +772,9 @@ async fn perform_switch( let display_name = new_project.display_name().to_string(); let is_temp = matches!(new_project.kind(), ProjectKind::Temp); + // Worth recording as the resume target? A switch to a fresh + // empty temp (`new`) is not — see the gate in `run()`. + let new_worth_recording = !new_project.is_unmodified_temp(); session.project = Some(new_project); session.database = Some(new_database); @@ -764,11 +785,14 @@ async fn perform_switch( // Persistence handle for this single line. let _ = Persistence::new(new_path.clone()).append_history(&source); - // Update the resume pointer so the next `--resume` - // launch reopens the project we just switched to. Write - // failures are non-fatal — see the same rationale at - // `run()` startup. - if let Err(e) = write_last_project(&session.data_root, &new_path) { + // Update the resume pointer so the next `--resume` launch + // reopens the project we just switched to — unless it is a + // fresh empty temp (a `new` command), which must not be + // recorded (see the gate in `run()`). Write failures are + // non-fatal. + if new_worth_recording + && let Err(e) = write_last_project(&session.data_root, &new_path) + { tracing::warn!(error = %e, "could not update last_project after switch"); }