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
- Fetch the plan first and skip unarchive if archivedAt is null (treat 400 as already-done)
- Disable the unarchive action in the UI for non-archived plans
- Don't auto-retry this 400 — it's a permanent state conflict
- 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
- Check archivedAt before calling unarchive
- Only expose unarchive in the UI for archived plans
- Treat the 400 as an idempotent no-op in scripts
- Refresh plan state after any archive/unarchive to avoid stale guards
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
- Plan already archived
- Error decoding request: %v
- Custom model providers are not supported on Plandex Cloud
- Custom models are not supported on Plandex Cloud in Integrat
- Has duplicates:
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/8d9bd69788ce3a63.
Report an issue: GitHub.