can1357/oh-my-pi · error · ToolError
Bulk directive lists conflict #${id} twice — each id may app
Error message
Bulk directive lists conflict #${id} twice — each id may appear once. What it means
Inside a `conflict://*` bulk-resolution block, each conflict id may carry exactly one `<id>: @side` directive. The parser throws when the same id appears in two directive lines, because a conflict can only be resolved to one side per pass.
Source
Thrown at packages/coding-agent/src/tools/write.ts:254
* block and still reported success. Per-id bulk is token-only; literal or
* multi-line replacements must go through individual `conflict://<N>` writes.
*/
function parseBulkDirectives(content: string): Map<number, string> | null {
const map = new Map<number, string>();
const stray: string[] = [];
let sawDirective = false;
for (const raw of content.split("\n")) {
const line = raw.trim();
if (line.length === 0) continue;
const match = line.match(BULK_DIRECTIVE_RE);
if (!match) {
stray.push(line);
continue;
}
sawDirective = true;
const id = Number.parseInt(match[1], 10);
if (map.has(id)) {
throw new ToolError(`Bulk directive lists conflict #${id} twice — each id may appear once.`);
}
map.set(id, match[2]);
}
// No directive lines at all → not a per-id block; caller uses uniform mode.
if (!sawDirective) return null;
if (stray.length > 0) {
const sample = stray[0]!;
const tokenHint = BULK_DIRECTIVE_HEAD_RE.test(sample)
? `Per-id bulk only accepts the tokens @ours/@theirs/@base/@both — one side per id, single line. `
: "";
throw new ToolError(
`Malformed \`conflict://*\` per-id block: ${stray.length} line(s) are not \`<id>: @side\` directives (first: \`${truncateDirectiveLine(sample)}\`). ` +
tokenHint +
`Literal or multi-line replacement content isn't supported in a per-id block — resolve those blocks with individual \`write({ path: "conflict://<N>", content })\` calls (you can issue several at once). ` +
`For a pure pick-a-side pass, make every non-empty line \`<id>: @ours\` (or @theirs/@base/@both).`,
);
}
return map;View on GitHub (pinned to 9690622007)
Solutions
- Remove the duplicate directive line so each id appears once
- Decide one side per conflict and keep only the intended '@ours'/'@theirs'/'@base'/'@both' line
Example fix
// before "1: @ours\n2: @theirs\n2: @ours" // after "1: @ours\n2: @theirs"
Defensive patterns
Strategy: validation
Validate before calling
function assertUniqueIds(block: string): void {
const seen = new Set<number>();
for (const line of block.split("\n")) {
const m = line.match(/^\s*(\d+):\s*@(ours|theirs|base|both)\s*$/);
if (!m) continue;
const id = Number(m[1]);
if (seen.has(id)) throw new Error(`duplicate conflict id ${id}`);
seen.add(id);
}
} Try / catch
try {
await write({ path: "conflict://*", content: block });
} catch (e) {
if (e instanceof ToolError && e.message.includes("twice")) {
const id = Number(e.message.match(/#(\d+)/)?.[1]);
block = dedupeDirective(block, id);
return write({ path: "conflict://*", content: block });
}
throw e;
} Prevention
- Build bulk blocks programmatically from a Map keyed by id (dedupes automatically)
- Never hand-concatenate directive lines
- Deduplicate ids before emitting <id>: @side lines
When it happens
Trigger: A per-id bulk block passed to write targeting conflict://* that contains duplicate lines such as '3: @ours' appearing twice for the same id.
Common situations: Hand-assembled or model-generated bulk resolution blocks where a line was duplicated while editing sides.
Related errors
- Malformed `conflict://*` per-id block: ${stray.length} line(
- Invalid conflict URI '${raw}': wildcard 'conflict://*' does
- Invalid conflict URI '${raw}': must be 'conflict://<N>', 'co
- Invalid conflict URI '${raw}': id must be ≥ 1.
- Invalid conflict URI '${raw}': scope must be one of 'ours',
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/c9f4652555102807.
Report an issue: GitHub.