plandex-ai/plandex · warning

Plan isn't archived

Error message

Plan isn't archived

What it means

UnarchivePlanHandler rejects unarchiving a plan whose archived_at is NULL, returning HTTP 400 "Plan isn't archived". It's a state guard ensuring unarchive only applies to plans that are currently archived.

Source

Thrown at app/server/handlers/plans_changes.go:500

	if auth == nil {
		return
	}

	log.Println("Received request for UnarchivePlanHandler")

	vars := mux.Vars(r)
	planId := vars["planId"]
	log.Println("planId: ", planId)

	plan := authorizePlanArchive(w, planId, auth)

	if plan == nil {
		return
	}

	if plan.ArchivedAt == nil {
		log.Println("Plan isn't archived")
		http.Error(w, "Plan isn't archived", http.StatusBadRequest)
		return
	}

	res, err := db.Conn.Exec("UPDATE plans SET archived_at = NULL WHERE id = $1", planId)

	if err != nil {
		log.Printf("Error archiving plan: %v\n", err)
		http.Error(w, "Error archiving plan: "+err.Error(), http.StatusInternalServerError)
		return
	}

	rowsAffected, err := res.RowsAffected()
	if err != nil {
		log.Printf("Error getting rows affected: %v\n", err)
		http.Error(w, "Error getting rows affected: "+err.Error(), http.StatusInternalServerError)
		return
	}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Fetch the plan first and skip unarchive if archivedAt is null (treat 400 as already-done)
  2. Disable the unarchive action in the UI for non-archived plans
  3. Don't auto-retry this 400 — it's a permanent state conflict
  4. If unarchiving is genuinely needed, it means state changed elsewhere; refresh plan data

Example fix

// before
await unarchivePlan(planId);
// after
const plan = await getPlan(planId);
if (plan.archivedAt) await unarchivePlan(planId);
Defensive patterns

Strategy: validation

Validate before calling

const plan = await getPlan(planId);
if (!plan.archivedAt) { /* not archived, nothing to unarchive */ return; }
await unarchivePlan(planId);

Type guard

function isArchived(v) { return v != null; } // archivedAt non-null means the plan is archived
if (!isArchived(plan.archivedAt)) skipUnarchive();

Try / catch

try { await unarchivePlan(planId); } catch (e) { if (e.status === 400 && e.message === "Plan isn't archived") return; /* treat as success */ throw e; }

Prevention

When it happens

Trigger: Calling the unarchive endpoint for a planId whose archived_at is NULL — the plan was never archived or was already unarchived.

Common situations: Double-clicking an unarchive button; concurrent unarchive by another session; retrying a request that already succeeded; scripts that unarchive unconditionally on startup.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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