can1357/oh-my-pi · error · ToolError

Conflict #${entry.id} has no base section (2-way merge). `@b

Error message

Conflict #${entry.id} has no base section (2-way merge). `@base` is only valid for diff3 conflicts.

What it means

expandContentTokens throws when the @base token is used in replacement content for a conflict that has no base section. Two-way (non-diff3) merges record only ours/theirs sides; @base only exists for diff3-style conflicts, so expanding @base to nothing would silently corrupt the resolution and the tool refuses instead.

Source

Thrown at packages/coding-agent/src/tools/conflict-detect.ts:568

 * - `@base`    → expands to `baseLines`; throws if no base section was
 *               recorded (i.e. the conflict was 2-way, not diff3).
 * - `@both`    → expands to `oursLines` then `theirsLines`.
 */
export function expandContentTokens(content: string, entry: ConflictEntry): string {
	const inputLines = content.split("\n");
	const out: string[] = [];
	for (const rawLine of inputLines) {
		const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
		switch (line) {
			case "@ours":
				out.push(...entry.oursLines);
				break;
			case "@theirs":
				out.push(...entry.theirsLines);
				break;
			case "@base":
				if (!entry.baseLines) {
					throw new ToolError(
						`Conflict #${entry.id} has no base section (2-way merge). \`@base\` is only valid for diff3 conflicts.`,
					);
				}
				out.push(...entry.baseLines);
				break;
			case "@both":
				out.push(...entry.oursLines, ...entry.theirsLines);
				break;
			default:
				out.push(rawLine);
				break;
		}
	}
	return out.join("\n");
}

/** Reconstruct a conflict-marker line from prefix and optional label. */
function markerLine(prefix: string, label: string | undefined): string {

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove the @base token and resolve using @ours/@theirs/@both, or paste the intended base content explicitly.
  2. Re-register conflicts with diff3 style (git config merge.conflictStyle diff3, re-trigger the merge) if a true base is needed.
  3. Use the conflict's ours/theirs sides as a stand-in for base when the ancestor version isn't available.

Example fix

// before
write({ path: 'conflict://2', content: '@base' }); // 2-way conflict
// after
write({ path: 'conflict://2', content: '@theirs' }); // no base side exists
Defensive patterns

Strategy: validation

Validate before calling

if (content.includes('@base') && entry.baseLines === undefined) {
  throw new Error(`conflict #${entry.id} is 2-way; @base unavailable`);
}

Type guard

function hasBase(entry) {
  return entry.baseLines !== undefined;
}

Try / catch

try {
  await write({ path: `conflict://${id}`, content });
} catch (err) {
  if (String(err?.message).includes('has no base section')) {
    await write({ path: `conflict://${id}`, content: content.replaceAll('@base', '@ours') });
  } else throw err;
}

Prevention

When it happens

Trigger: Writing content containing '@base' (e.g. via conflict://<id> or conflict://*) against a conflict registered from a 2-way merge (merge.conflictStyle diff, not diff3/zdiff3); a generic @ours/@theirs/@base template applied to all conflicts where some are 2-way.

Common situations: Repo git config switched from diff3 to plain merge style (or was never diff3) while tooling assumes diff3; bulk wildcard writes with @base hitting a mix of conflict styles; agents copying resolution templates that assume diff3.

Related errors


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