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
- Remove the duplicate op, keeping the single intended replacement.
- Deduplicate before calling: build a Map keyed by pat (later entries overwrite earlier) and pass Array.from(map.values()).
- 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
- Deduplicate ops by pat with a Map before every call.
- In generation pipelines, instruct/parse so each pattern appears once.
- Unit-test op builders for duplicate emission when merging sources.
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
- rewrite received invalid arguments: ${params.summary}
- approve received invalid arguments: ${params.summary}
- RPC host tool names must be unique
- `ops[${index}].pat` must be a non-empty pattern
- `ops` must include at least one op entry
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/5502adb6318eeb25.
Report an issue: GitHub.