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
- Pass a number: wait(1000) or wait(predicateFn)
- Coerce string durations with Number() before calling
- 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
- Never quote numeric durations in serialized tool arguments
- Coerce string durations with Number() at the boundary
- Pass predicate polling as an inline arrow function, not its result
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
- Invalid pattern: {err}
- Destination option ${key} must be a string
- Destination option ${key} must be a finite number
- Destination option ${key} must be a boolean
- Expected --${name} to be a positive integer, got ${value}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/06b1f54eff15c81e.
Report an issue: GitHub.