can1357/oh-my-pi · error · ToolError

wait(...) expects milliseconds (number) or a predicate funct

Error message

wait(...) expects milliseconds (number) or a predicate function to poll

What it means

waitForRun in run-scope accepts either a number of milliseconds to sleep or a predicate function to poll. If msOrPredicate is neither (e.g. a string, undefined, or an object), it throws this ToolError immediately instead of guessing a default.

Source

Thrown at packages/coding-agent/src/tools/run-scope.ts:349

 * - `wait(ms)` sleeps for `ms` milliseconds.
 * - `wait(fn, { timeout?, interval? })` polls `fn` (sync or async) until it returns a
 *   truthy value and resolves with that value; throws a named `ToolError` on timeout
 *   instead of stalling into the whole-cell deadline. Predicate errors propagate.
 */
export function waitForRun(
	msOrPredicate: number | (() => unknown),
	signal: AbortSignal,
	opts?: WaitPredicateOptions,
): Promise<unknown> {
	const promise = (async (): Promise<unknown> => {
		throwIfAborted(signal);
		if (typeof msOrPredicate === "number") {
			await untilAborted(signal, async () => await Bun.sleep(msOrPredicate));
			throwIfAborted(signal);
			return undefined;
		}
		if (typeof msOrPredicate !== "function") {
			throw new ToolError("wait(...) expects milliseconds (number) or a predicate function to poll");
		}
		const timeout =
			opts?.timeout !== undefined && Number.isFinite(opts.timeout) && opts.timeout > 0
				? opts.timeout
				: DEFAULT_PREDICATE_TIMEOUT_MS;
		const interval = Math.max(opts?.interval ?? 100, 10);
		const deadline = Date.now() + timeout;
		for (;;) {
			const value = await untilAborted(signal, async () => await msOrPredicate());
			throwIfAborted(signal);
			if (value) return value;
			if (Date.now() + interval > deadline) {
				throw new ToolError(`wait(predicate) timed out after ${timeout}ms — predicate never returned truthy`);
			}
			await untilAborted(signal, async () => await Bun.sleep(interval));
		}
	})();
	return trackBrowserRunPromise(promise);

View on GitHub (pinned to 9690622007)

Solutions

  1. Pass a number: wait(1000) or wait(predicateFn)
  2. Coerce string durations with Number() before calling
  3. If polling, pass an actual function, not a boolean/expression result

Example fix

// before
await wait("500");
// after
await wait(500); // or
await wait(() => run.status === "done");
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof arg !== "number" && typeof arg !== "function") throw new Error("wait() needs a number (ms) or a predicate function");

Type guard

function isWaitArg(v: unknown): v is number | ((run: RunState) => unknown) { return typeof v === "number" || typeof v === "function"; }

Try / catch

try { await wait(arg); } catch (e) { if (e instanceof ToolError && e.message.includes("expects milliseconds")) { await wait(Number(arg) || DEFAULT_MS); } else throw e; }

Prevention

When it happens

Trigger: wait("1000") (string instead of number), wait() with no argument, wait({ms: 100}), or passing a predicate-looking object that is not callable — typically from JSON-serialized tool arguments where functions become strings/objects.

Common situations: LLM passes duration as a quoted string in JSON args; code forwards an optional value that was undefined; a wrapper binds the wrong argument position.

Understand the failure class

Background: Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained — this error's family across 15 libraries.

Related errors


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