can1357/oh-my-pi · error · ToolError

Invalid regex: ${message.replace(/^Invalid regular expressio

Error message

Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}

What it means

The JS fallback matcher (jsMatchedLineIndexes) compiles the user pattern with new RegExp; an invalid pattern throws a SyntaxError which is converted to a clean ToolError 'Invalid regex: <detail>' with the engine's boilerplate prefix stripped.

Source

Thrown at packages/coding-agent/src/tools/grep.ts:438

 * large for native grep (>`NATIVE_GREP_MAX_FILE_BYTES`, which native grep silently
 * skips). Mirrors the native probe's output (sorted, deduped indexes) so
 * `buildVirtualMatches` rebuilds context/ranges identically; only the regex dialect
 * differs for these oversized inputs (the pre-RE2-parity behavior).
 */
function jsMatchedLineIndexes(
	content: string,
	lines: readonly string[],
	pattern: string,
	ignoreCase: boolean,
	multiline: boolean,
): number[] {
	const flags = `${ignoreCase ? "i" : ""}${multiline ? "gm" : ""}`;
	let regex: RegExp;
	try {
		regex = new RegExp(pattern, flags);
	} catch (err) {
		const message = err instanceof Error ? err.message : String(err);
		throw new ToolError(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
	}
	if (!multiline) {
		const out: number[] = [];
		for (let i = 0; i < lines.length; i++) {
			regex.lastIndex = 0;
			if (regex.test(lines[i] ?? "")) out.push(i);
		}
		return out;
	}
	const { starts } = indexSearchLines(content);
	const seen = new Set<number>();
	const out: number[] = [];
	let match = regex.exec(content);
	while (match !== null) {
		const lineIndex = findLineIndex(starts, match.index);
		if (lineIndex >= 0 && !seen.has(lineIndex)) {
			seen.add(lineIndex);
			out.push(lineIndex);

View on GitHub (pinned to 9690622007)

Solutions

  1. Fix the regex syntax (close groups/brackets, valid quantifier bounds)
  2. Escape metacharacters in literal text: \( \[ \. etc.
  3. Pre-test the pattern with new RegExp(pattern, 'i') in a try/catch before calling

Example fix

// before
const pattern = `log(${level}`; // invalid
await grepTool.execute({ pattern, path })
// after
const pattern = `log\(${level}`; // escaped
await grepTool.execute({ pattern, path })
Defensive patterns

Strategy: validation

Validate before calling

function assertValidRegex(pattern: string, flags = "i"): void {
  try { new RegExp(pattern, flags); } catch (err) {
    throw new Error(`Pattern is not a valid JS RegExp: ${pattern}`, { cause: err });
  }
}

Type guard

function isValidRegex(pattern: string): boolean {
  try { new RegExp(pattern); return true; } catch { return false; }
}

Try / catch

try {
  return await grepTool.execute({ pattern, path });
} catch (err) {
  if (err instanceof ToolError && err.message.startsWith("Invalid regex:")) {
    const literal = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    return await grepTool.execute({ pattern: literal, path }); // retry as escaped literal
  }
  throw err;
}

Prevention

When it happens

Trigger: Patterns like 'foo(' , '[unclosed', 'a{2,1}', a stray '*' after nothing to repeat, or invalid backreferences passed as the grep pattern.

Common situations: Unescaped user text used as a regex (parentheses, brackets in prose or code snippets); patterns built by string concatenation; switching between tools where one accepts literal text and this one expects regex.

Related errors


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