plandex-ai/plandex · error

error deleting draft plan dir: %v

Error message

error deleting draft plan dir: %v

What it means

After collecting ids of deleted draft plans, DeleteDraftPlans waits on errCh for each per-plan directory deletion. This error wraps the first error returned by DeletePlanDir for any plan, meaning the on-disk plan directory could not be removed.

Source

Thrown at app/server/db/plan_helpers.go:389

	errCh := make(chan error, len(ids))
	for _, planId := range ids {
		go func(planId string) {
			defer func() {
				if r := recover(); r != nil {
					log.Printf("panic in DeleteDraftPlans: %v\n%s", r, debug.Stack())
					errCh <- fmt.Errorf("panic in DeleteDraftPlans: %v\n%s", r, debug.Stack())
					runtime.Goexit() // don't allow outer function to continue and double-send to channel
				}
			}()
			errCh <- DeletePlanDir(orgId, planId)
		}(planId)
	}

	for i := 0; i < len(ids); i++ {
		err := <-errCh
		if err != nil {
			return fmt.Errorf("error deleting draft plan dir: %v", err)
		}
	}

	if len(ids) > 0 {
		log.Println("Deleted", len(ids), "draft plans")
	}

	return nil
}

func DeleteOwnerPlans(orgId, projectId, userId string) error {
	res, err := Conn.Query("DELETE FROM plans WHERE project_id = $1 AND owner_id = $2 RETURNING id;", projectId, userId)
	if err != nil {
		return fmt.Errorf("error deleting plans: %v", err)
	}

	defer res.Close()

View on GitHub (pinned to e2d772072e)

Solutions

  1. Check the wrapped OS error from DeletePlanDir (EACCES/ENOENT/EBUSY)
  2. Treat already-deleted directories (ENOENT) as success to tolerate races
  3. Fix filesystem permissions/ownership on the plan storage root
  4. Serialize or lock plan deletion per planId to avoid concurrent delete races

Example fix

// before
return fmt.Errorf("error deleting draft plan dir: %v", err)
// after
if os.IsNotExist(err) {
    return nil
}
return fmt.Errorf("error deleting draft plan dir: %w", err)
Defensive patterns

Strategy: fallback

Validate before calling

if _, err := os.Stat(planDir); err == nil {
    // dir exists and can be deleted
}

Try / catch

err := DeleteDraftPlans(orgId, projectId, userId)
if err != nil {
    var pe *fs.PathError
    if errors.As(err, &pe) && errors.Is(pe.Err, syscall.ENOENT) {
        // already deleted; safe to ignore
    } else {
        return err
    }
}

Prevention

When it happens

Trigger: DeletePlanDir(orgId, planId) returns an error: permission denied on plan directory, directory already removed by a concurrent request (ENOENT handling differs), or filesystem I/O failure.

Common situations: Multiple tabs/requests creating and deleting drafts concurrently causing races; read-only or full disk; wrong ownership of plan data directories after a container user change; NFS stale-handle errors.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/6c9076020016c628. Report an issue: GitHub.