plandex-ai/plandex · warning
Plan already archived
Error message
Plan already archived
What it means
ArchivePlanHandler rejects archiving a plan that already has a non-nil archived_at timestamp, returning HTTP 400 'Plan already archived'. This is an intentional idempotency/state guard: a plan can only be archived once.
Source
Thrown at app/server/handlers/plans_changes.go:452
if auth == nil {
return
}
log.Println("Received request for ArchivePlanHandler")
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 already archived")
http.Error(w, "Plan already archived", http.StatusBadRequest)
return
}
res, err := db.Conn.Exec("UPDATE plans SET archived_at = NOW() 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 archiving if archivedAt is already set (treat 400 as success in idempotent flows)
- Dedupe UI submissions (disable button while in flight)
- Don't retry this request automatically — it's a permanent 400, not transient
- If you need the plan active again, call unarchive instead
Example fix
// before await archivePlan(planId); // throws on second call // after const plan = await getPlan(planId); if (!plan.archivedAt) await archivePlan(planId);
Defensive patterns
Strategy: validation
Validate before calling
const plan = await getPlan(planId);
if (plan.archivedAt) { /* already archived, skip */ return; }
await archivePlan(planId); Type guard
function isAlreadyArchived(v) { return v != null; } // plan.ArchivedAt (*time.Time) non-nil means archived
if (isAlreadyArchived(plan.archivedAt)) skipArchive(); Try / catch
try { await archivePlan(planId); } catch (e) { if (e.status === 400 && e.message === 'Plan already archived') return; /* treat as success (idempotent) */ throw e; } Prevention
- Check archivedAt before calling archive
- Treat 'Plan already archived' 400 as an idempotent success
- Disable archive buttons for already-archived plans
- Debounce/dedupe rapid archive submissions
When it happens
Trigger: POST/PUT to the archive endpoint for a planId whose plans.archived_at is already set — i.e. the plan was archived previously by this or another user/session.
Common situations: Double-clicking an archive button; retrying a request that actually succeeded the first time; two operators archiving the same plan concurrently; automated scripts running archive jobs repeatedly.
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 isn't 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/25d2b6a955011364.
Report an issue: GitHub.