fix: only remove provisional workspace when child exits non-zero
Adds a child-exit-code guard to handleProvisional so a `ctask new` workspace is reclaimed only when the agent was actually canceled before real work (trust prompt rejected, Esc during startup, Ctrl+C mid-launch — all confirmed empirically as exit code 1). A zero exit means the user entered the agent and exited cleanly, which is a legitimate workflow that must preserve the workspace even with an empty manifest diff — for example when the user wants the workspace directory established so they can populate context/ before resuming. The exit code reaches handleProvisional via a new childExitCode helper that unwraps *exec.ExitError from cmd.Run's return. Non-exit errors (agent not found, OS-level failure) map to -1 so the workspace is still cleaned up — the child never actually ran. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+21
-5
@@ -3,6 +3,7 @@ package session
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@@ -153,11 +154,12 @@ func Run(opts LaunchOpts) error {
|
||||
}
|
||||
|
||||
// ---- Provisional-workspace cleanup ----
|
||||
// If `ctask new` launched this session and the child exited without
|
||||
// changing any files, remove the workspace entirely and skip finalize.
|
||||
// Nothing to log, nothing to summarize, and the .ctask state files die
|
||||
// with the directory.
|
||||
if handleProvisional(opts, startManifest) {
|
||||
// If `ctask new` launched this session, the child exited non-zero
|
||||
// (canceled before real work), and no files changed, remove the workspace
|
||||
// entirely and skip finalize. A zero child exit means the user entered
|
||||
// the agent and exited normally — those workspaces are preserved even
|
||||
// with an empty diff.
|
||||
if handleProvisional(opts, startManifest, childExitCode(childErr)) {
|
||||
return childErr
|
||||
}
|
||||
|
||||
@@ -173,6 +175,20 @@ func Run(opts LaunchOpts) error {
|
||||
return childErr
|
||||
}
|
||||
|
||||
// childExitCode extracts the exit code from the error returned by cmd.Run().
|
||||
// Returns 0 when err is nil (clean exit). Returns the reported code for
|
||||
// *exec.ExitError. Returns -1 for any other error (agent not found, OS-level
|
||||
// failure) so provisional cleanup still kicks in — the child never ran.
|
||||
func childExitCode(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
return ee.ExitCode()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// finalize runs all end-of-session metadata writes under one write-lock
|
||||
// acquisition: session log append (best-effort), last-session-summary.json
|
||||
// (best-effort), lease removal (best-effort, if we owned it), and
|
||||
|
||||
Reference in New Issue
Block a user