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
- Inspect the workspace/agent state after the failure and retry the stop once
- Fall back to the bulk/picker path (runStopBatch directly) to surface a more detailed batch error
- 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
- Verify the agent process exists before stopping
- Allow a short delay/retry for transient stop failures
- Inspect workspace state after a failed stop before escalating
- Use the picker/batch path when you need richer failure detail
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
- failed
- workspace is still being created and cannot be
- no such folder
- folder name must not be empty
- no such folder
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)