sinelaw/fresh · error

workspace cannot be

Error message

workspace cannot be ${action === "archive" ? "archived" : "deleted"}

What it means

requireEligible throws this for the "archive" and "delete" bulk actions when the workspace is not eligible for the requested verb — the message reads 'workspace cannot be archived' or 'workspace cannot be deleted'. As with stop, the goal is to give programmatic callers the reason a picker would have shown by disabling the button.

Solutions

  1. Stop the workspace's agent before requesting archive/delete
  2. Check eligibility (bulkEligible or an equivalent status read) before calling
  3. Skip or log already-archived/ineligible targets instead of retrying

Example fix

// before
await api.archiveWorkspace(id);
// after
const ws = api.getWorkspace(id);
if (ws?.hasRunningAgent) api.stopWorkspace(id);
await api.archiveWorkspace(id);
Defensive patterns

Strategy: validation

Validate before calling

const ws = api.getWorkspace(id);
if (!ws || ws.pending || ws.archived || ws.hasRunningAgent) {
  throw new Error(`workspace ${id} not eligible for ${action}`);
}

Type guard

function isLifecycleEligible(s, action) {
  return s != null && !s.pending && !s.archived &&
    (action === "stop" ? s.hasRunningAgent === true : true);
}

Try / catch

try {
  await api.archiveWorkspace(id);
} catch (e) {
  if (String(e).includes("cannot be archived")) {
    api.stopWorkspace(id);
    await api.archiveWorkspace(id);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the archive/delete API (apiArchiveWorkspace/apiDeleteWorkspace) on a workspace for which bulkEligible returns false — e.g. a still-running agent that must be stopped first, a pending/placeholder session, or an already-archived workspace for a second archive.

Common situations: Bulk scripts that archive workspaces without stopping running agents; retry loops re-archiving an already-archived workspace; deleting a workspace in a mid-creation pending state.

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 sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/997fbd1076906b9f. Report an issue: GitHub.

Appendix: source

Thrown at crates/fresh-editor/plugins/orchestrator.ts:10677

}

function apiDeleteFolder(folderId: string): boolean {
  if (!folderById(folderId)) return false;
  // `deleteFolder` reparents the subtree one level up, so nothing inside is
  // lost — same as the dock's "Delete Folder".
  deleteFolder(folderId);
  refreshDockTree();
  return true;
}

/// Shared guard for the three lifecycle verbs: the picker disables a button
/// it cannot honour, and a caller deserves the reason rather than a no-op.
function requireEligible(s: AgentSession, action: BulkAction): void {
  if (bulkEligible(action, s.id)) return;
  if (action === "stop") {
    throw new Error("workspace has no agent process to stop");
  }
  throw new Error(`workspace cannot be ${action === "archive" ? "archived" : "deleted"}`);
}

function apiStopWorkspace(target: string | number): boolean {
  const s = resolveWorkspace(target);
  if (!s) return false;
  requireEligible(s, "stop");
  // Same batch runner as the picker's confirmed Stop, over one id.
  if (runStopBatch([s.id]) === 0) throw new Error("stop failed");
  refreshOpenDialog();
  return true;
}

/// Archive / delete over a single workspace, through the same batch runner
/// the picker's confirmed action uses — so the cross-machine manifest sync,
/// and anything added to that path later, applies to a scripted archive too.
async function runLifecycle(
  target: string | number,
  action: "archive" | "delete",

View on GitHub (pinned to 67894ca546)