can1357/oh-my-pi · error · ToolError

Duplicate rewrite pattern: ${pat}

Error message

Duplicate rewrite pattern: ${pat}

What it means

ast_edit rejects duplicate patterns: two ops whose pat strings are identical. Since each pattern maps to exactly one replacement via normalizedRewrites (an object keyed by pattern), duplicates are ambiguous or redundant, so the second occurrence throws ToolError with the offending pattern text.

Source

Thrown at packages/coding-agent/src/tools/ast-edit.ts:280

		params: AstEditSchemaInfer,
		signal?: AbortSignal,
		_onUpdate?: AgentToolUpdateCallback<AstEditToolDetails>,
		_context?: AgentToolContext,
	): Promise<AgentToolResult<AstEditToolDetails>> {
		return untilAborted(signal, async () => {
			const ops = params.ops.map((entry, index) => {
				if (entry.pat.length === 0) {
					throw new ToolError(`\`ops[${index}].pat\` must be a non-empty pattern`);
				}
				return [entry.pat, entry.out] as const;
			});
			if (ops.length === 0) {
				throw new ToolError("`ops` must include at least one op entry");
			}
			const seenPatterns = new Set<string>();
			for (const [pat] of ops) {
				if (seenPatterns.has(pat)) {
					throw new ToolError(`Duplicate rewrite pattern: ${pat}`);
				}
				seenPatterns.add(pat);
			}
			const normalizedRewrites = Object.fromEntries(ops);
			const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);

			const scope = await resolveToolSearchScope({
				rawPaths: params.paths,
				cwd: this.session.cwd,
				internalUrlAction: "rewrite",
				settings: this.session.settings,
				signal,
				sessionFile: this.session.getSessionFile() ?? undefined,
				localProtocolOptions: this.session.localProtocolOptions,
				skills: this.session.skills,
				resolveExternalUrl: async rawPath => {
					if (!parseReadUrlTarget(rawPath)) return undefined;
					throw new ToolError(

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove the duplicate op, keeping the single intended replacement.
  2. Deduplicate before calling: build a Map keyed by pat (later entries overwrite earlier) and pass Array.from(map.values()).
  3. If both rewrites are genuinely needed for the same pattern, merge them into one combined `out` or split into multiple sequential ast_edit calls.

Example fix

// before
await astEdit.execute({ path, ops: [{ pat: 'foo()', out: 'bar()' }, { pat: 'foo()', out: 'baz()' }] });
// after
const deduped = [...new Map(ops.map(o => [o.pat, o])).values()];
await astEdit.execute({ path, ops: deduped });
Defensive patterns

Strategy: validation

Validate before calling

const pats = ops.map(o => o.pat);
if (new Set(pats).size !== pats.length) {
  throw new Error('ops contains duplicate patterns');
}

Try / catch

try {
  await astEditTool.execute({ path, ops }, signal);
} catch (err) {
  if (err instanceof ToolError && err.message.startsWith('Duplicate rewrite pattern')) {
    const pat = err.message.slice('Duplicate rewrite pattern: '.length);
    ops = [...new Map(ops.map(o => [o.pat, o])).values()];
  }
}

Prevention

When it happens

Trigger: ops contains two entries with the same pat, e.g. [{pat:'x',out:'y'},{pat:'x',out:'z'}] or an exact repeat — typically from model-generated ops lists that repeat a rewrite.

Common situations: LLM emitting the same rewrite twice with slightly different outputs; a caller accumulating ops in a loop without deduping; copying op entries when editing a request.

Related errors


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