can1357/oh-my-pi · error · Error

experiment id must be a non-empty token of [A-Za-z0-9_.] (ru

Error message

experiment id must be a non-empty token of [A-Za-z0-9_.] (runs group as `<id>-<arm>`)

What it means

createExperiment validates the experiment id against `/^[A-Za-z0-9_.]+$/` and throws this error for anything else (empty, whitespace-only, or containing dashes/spaces/slashes). Dashes are structurally forbidden because `experimentOf` groups job names by the token before the first dash — a dashed id could never own its runs. The id must be a non-empty token of letters, digits, underscore, and dot.

Source

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

			this.#tick();
		});
		this.#store.registerLaunch({ ...record, pid: proc.pid });
		this.#tick();
		return proc.pid;
	}

	/** Liveness check that survives manager restarts: managed child, or a running row with a live pid. */
	#runLive(run: RunRow): boolean {
		return this.#children.has(run.jobName) || (run.status === "running" && pidAlive(run.pid));
	}

	/** Register an experiment id (with an optional goal) so it is browsable before its first arm. */
	createExperiment(req: CreateExperimentRequest): { id: string; goal: string } {
		const id = req.id?.trim() ?? "";
		// Dashes are structurally impossible: `experimentOf` groups job names by
		// the token before the first dash, so a dashed id could never own a run.
		if (!/^[A-Za-z0-9_.]+$/.test(id)) {
			throw new Error("experiment id must be a non-empty token of [A-Za-z0-9_.] (runs group as `<id>-<arm>`)");
		}
		const goal = req.goal ?? this.#store.getExperimentMeta(id)?.goal ?? "";
		this.#store.setExperimentGoal(id, goal);
		return { id, goal };
	}

	/** Apply goal + per-run role/note metadata; used by the UI and for backfill. */
	updateExperimentMeta(id: string, update: ExperimentMetaUpdate): { id: string; updatedRuns: string[] } {
		if (update.goal !== undefined) this.#store.setExperimentGoal(id, update.goal);
		const updatedRuns: string[] = [];
		for (const jobName in update.runs) {
			if (experimentOf(jobName) !== id) continue;
			if (this.#store.setRunMeta(jobName, update.runs[jobName])) updatedRuns.push(jobName);
		}
		this.#tick();
		return { id, updatedRuns };
	}

View on GitHub (pinned to 9690622007)

Solutions

  1. Sanitize the id: replace disallowed characters with `_` or `.` before calling createExperiment
  2. Remove dashes (e.g. `my-exp` → `my_exp` or `myexp`) since dashes are the run-name grouping separator
  3. Ensure the id is non-empty after trimming
  4. Validate with the same regex client-side: /^[A-Za-z0-9_.]+$/

Example fix

// before
server.createExperiment({ id: "feat/new-agent" }); // throws
// after
const raw = "feat/new-agent";
const id = raw.replace(/[^A-Za-z0-9_.]/g, "_"); // "feat_new_agent"
server.createExperiment({ id });
Defensive patterns

Strategy: validation

Validate before calling

const EXPERIMENT_ID_RE = /^[A-Za-z0-9_.]+$/;
function validExperimentId(id: string | undefined): boolean {
  return typeof id === "string" && EXPERIMENT_ID_RE.test(id.trim());
}

Type guard

function isValidExperimentId(id: unknown): id is string {
  return typeof id === "string" && /^[A-Za-z0-9_.]+$/.test(id);
}

Try / catch

try {
  server.createExperiment({ id });
} catch (err) {
  if (err instanceof Error && err.message.startsWith("experiment id must be")) {
    throw new Error(`invalid experiment id "${id}": use [A-Za-z0-9_.], no dashes`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling `createExperiment({ id })` with an empty/undefined id, an id containing `-`, spaces, `/`, or other punctuation, or an id with leading/trailing whitespace that trims to empty.

Common situations: Deriving the experiment id from a branch name or URL slug that contains dashes; passing a raw user string without sanitizing; forgetting to pass `id` at all.

Related errors


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