sinelaw/fresh · error

stop failed

Error message

stop failed

What it means

apiStopWorkspace resolves the target, asserts stop eligibility, then delegates to runStopBatch — the same runner the picker's confirmed Stop uses. If the batch reports zero stopped workspaces, the stop definitively failed and the API throws 'stop failed' rather than returning true while nothing was stopped.

Solutions

  1. Inspect the workspace/agent state after the failure and retry the stop once
  2. Fall back to the bulk/picker path (runStopBatch directly) to surface a more detailed batch error
  3. If the agent process is already dead, treat the workspace as stopped and clean up via archive/delete instead

Example fix

// before
api.stopWorkspace(id);
// after
try {
  api.stopWorkspace(id);
} catch (e) {
  if (String(e).includes("stop failed")) {
    await new Promise(r => setTimeout(r, 500));
    api.stopWorkspace(id); // one retry
  }
}
Defensive patterns

Strategy: retry

Validate before calling

const ws = api.getWorkspace(id);
if (!ws || !ws.hasRunningAgent) return; // nothing to stop

Try / catch

try {
  api.stopWorkspace(id);
} catch (e) {
  if (String(e) === "Error: stop failed") {
    await new Promise(r => setTimeout(r, 500));
    api.stopWorkspace(id); // single retry for transient batch failure
  } else throw e;
}

Prevention

When it happens

Trigger: runStopBatch([id]) returning 0: the underlying stop of the agent process failed (process already gone in a bad state, batch runner error, cross-machine manifest conflict), even though the workspace initially looked stop-eligible.

Common situations: Agent process crashed between eligibility check and stop; permission issues terminating the process; transient backend errors during the batch execution.

Related errors


AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/40ac1168b2932e52. Report an issue: GitHub.

Appendix: source

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

  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",
): Promise<boolean> {
  // Before `requireEligible`, which rejects an ineligible workspace.
  await yieldToCaller();
  const s = resolveWorkspace(target);
  if (!s) return false;
  requireEligible(s, action);
  const res = await runLifecycleBatch(action, [s.id]);
  if (res.ok === 0) {

View on GitHub (pinned to 67894ca546)