can1357/oh-my-pi · error · ToolError

Malformed `conflict://*` per-id block: ${stray.length} line(

Error message

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).

What it means

A `conflict://*` per-id bulk block contained lines that are not `<id>: @side` directives. Per-id blocks accept only single-line side tokens (@ours/@theirs/@base/@both); literal or multi-line replacement content must go through individual conflict://<N> writes.

Source

Thrown at packages/coding-agent/src/tools/write.ts:265

		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;
}

/**
 * Resolve per-id directives, preferring the pre-strip `raw` content and falling
 * back to the hashline-stripped `stripped` content.
 *
 * Raw is preferred because the `<id>:` directive heads look exactly like
 * hashline `LINE:` prefixes and would be eaten by stripping. When the two
 * contents are identical (hashline mode off) a single parse decides everything,
 * so a malformed-block error propagates straight through — the previous
 * `?? parseBulkDirectives(...)` chain would have swallowed it and silently

View on GitHub (pinned to 9690622007)

Solutions

  1. Make every non-empty line a strict '<id>: @ours' (or @theirs/@base/@both) directive
  2. Move literal/multi-line replacement content into individual write({ path: "conflict://<N>", content }) calls, issued in parallel
  3. Remove any non-directive lines from the bulk block

Example fix

// before
"1: @ours\n2: replace whole file with ..."
// after
write({ path: "conflict://*", content: "1: @ours" })
write({ path: "conflict://2", content: "replacement content" })
Defensive patterns

Strategy: validation

Validate before calling

const DIRECTIVE_RE = /^\s*\d+:\s*@(ours|theirs|base|both)\s*$/;
function isPurePerIdBlock(block: string): boolean {
  return block.split("\n").filter(l => l.trim().length > 0).every(l => DIRECTIVE_RE.test(l));
}
// if false, split out literal content into individual conflict://<N> writes

Try / catch

try {
  await write({ path: "conflict://*", content: block });
} catch (e) {
  if (e instanceof ToolError && e.message.includes("per-id block")) {
    const { directives, literals } = splitBlock(block);
    await write({ path: "conflict://*", content: directives });
    for (const [id, text] of literals) await write({ path: `conflict://${id}`, content: text });
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: write({ path: "conflict://*", content }) where content has directive lines plus stray lines — either arbitrary text (bad token) or lines starting like directives with unsupported payloads.

Common situations: Models trying to supply replacement file content inline in the bulk block, or pasting conflict hunks into the per-id list.

Understand the failure class

Related errors


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