can1357/oh-my-pi · error · Error

experiment ${id} has running arms (${live.map(r => r.jobName

Error message

experiment ${id} has running arms (${live.map(r => r.jobName).join(", ")}); cancel them first

What it means

deleteExperiment refuses to delete an experiment while any of its arms (runs) are still live. It throws listing the running job names so you can cancel them first. This prevents destroying run processes/DB rows and job dirs underneath active executions.

Source

Thrown at packages/metaharness/src/server.ts:557

			if (experimentOf(jobName) !== id) continue;
			if (this.#store.setRunMeta(jobName, update.runs[jobName])) updatedRuns.push(jobName);
		}
		this.#tick();
		return { id, updatedRuns };
	}

	/**
	 * Delete an experiment: every arm's DB row, job dir, and manager log, plus
	 * the goal row. Refuses while any arm is live (cancel first — deleting a
	 * job dir under a writing runner would corrupt it). Returns null when the
	 * id names neither runs nor a registered experiment.
	 */
	deleteExperiment(id: string): { id: string; deletedRuns: string[] } | null {
		const runs = this.#store.listRuns().filter(r => experimentOf(r.jobName) === id);
		if (runs.length === 0 && !this.#store.getExperimentMeta(id)) return null;
		const live = runs.filter(r => this.#runLive(r));
		if (live.length > 0) {
			throw new Error(
				`experiment ${id} has running arms (${live.map(r => r.jobName).join(", ")}); cancel them first`,
			);
		}
		for (const run of runs) this.#destroyRun(run.jobName);
		this.#store.deleteExperimentMeta(id);
		this.#tick();
		return { id, deletedRuns: runs.map(r => r.jobName) };
	}

	/**
	 * Permanently delete a run: DB row + trials, job dir, and manager log.
	 * Disk removal is not optional — discover() would resurrect a surviving
	 * job dir as a fresh row on the next restart. Refuses while the run is
	 * live; returns false when the run is unknown.
	 */
	deleteRun(jobName: string): boolean {
		const run = this.#store.getRun(jobName);
		if (!run) return false;

View on GitHub (pinned to 9690622007)

Solutions

  1. Cancel each listed running arm first, then call deleteExperiment again
  2. Wait for the arms to finish before deleting
  3. If a run is stale (process actually dead but state says running), restart the manager or mark exit to clear the live state before deleting

Example fix

// before
server.deleteExperiment("sweep1"); // throws: has running arms (sweep1-a, sweep1-b)
// after
for (const arm of ["sweep1-a", "sweep1-b"]) {
  server.cancelRun(arm); // stop live arms first
}
server.deleteExperiment("sweep1");
Defensive patterns

Strategy: try-catch

Validate before calling

// check no arms are live before deleting
const running = server.listRuns().filter(
  r => experimentOf(r.jobName) === id && server.isRunRunning(r.jobName)
);
if (running.length > 0) throw new Error(`cancel ${running.map(r => r.jobName)} first`);

Try / catch

try {
  server.deleteExperiment(id);
} catch (err) {
  if (err instanceof Error && err.message.includes("has running arms")) {
    // parse job names from message and cancel each, then retry once
  } else throw err;
}

Prevention

When it happens

Trigger: Calling `deleteExperiment(id)` when at least one run whose `experimentOf(jobName)` equals id is currently live per `#runLive` (process still running).

Common situations: Cleanup scripts deleting old experiments while a long trial is still executing; a crashed manager left runs marked running; concurrent sessions where another operator started a new arm.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/8ded61133bfcba7f. Report an issue: GitHub.