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

  1. Remove the duplicate directive line so each id appears once
  2. 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

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


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