runtime: don't record an unmodified temp as the --resume target
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.
This commit is contained in:
+43
-19
@@ -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<std::path::PathBuf> = 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<std::path::PathBuf> = project_at_quit
|
||||
.and_then(|p| p.is_unmodified_temp().then(|| p.path().to_path_buf()));
|
||||
let resume_target_on_quit: Option<std::path::PathBuf> = 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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user