can1357/oh-my-pi · error

Empty patch. Start with ${OPENER}.

Error message

Empty patch. Start with ${OPENER}.

What it means

After scanning all lines, if zero operations were produced the payload is considered empty and this error is thrown. The only way to open an operation is a « line, so the message instructs the author to start the payload with it. This fires when the body contains only blank lines, only prose that was dropped, or only stray control lines that were tolerated-and-ignored.

Source

Thrown at packages/coding-agent/src/edit/sloppy.ts:1406

			if (nextContent !== undefined && parseOpener(nextContent) === false) {
				throw new Error(`Operation ${operations.length + 1} has a second ${REWRITE_HEADER} line.`);
			}
			// A bare » before the next operation or at payload end is a stray
			// close-bracket: models sometimes wrap both MATCH and REWRITE in «…».
		} else if (
			trimmed === SELECT_CLOSE &&
			(rewriteLines.join("\n").match(/⟪/gu) || []).length === (rewriteLines.join("\n").match(/⟫/gu) || []).length
		) {
			// A lone ⟫ with no open selection is a stray block terminator; REWRITE
			// is final text and never carries selection markers.
		} else {
			rewriteLines.push(line);
		}
	}

	if (state === "rewrite") finish(lines.length);
	else if (state === "pattern") finishPattern(lines.length);
	if (operations.length === 0) throw new Error(`Empty patch. Start with ${OPENER}.`);
	for (let index = 0; index < operations.length; index++) {
		const operationRewrite = operations[index].rewrite;
		const rewrites = operationRewrite.kind === "explicit" ? [operationRewrite.text] : operationRewrite.replacements;
		for (const rewrite of rewrites) {
			for (const line of rewrite.split("\n")) {
				const reference = line.trim().match(/^»([1-9]\d*)$/u);
				if (reference && Number(reference[1]) >= index + 1) {
					throw new Error(
						`${REWRITE_HEADER}${reference[1]} must reference an earlier operation, not self/forward.`,
					);
				}
			}
		}
	}
	for (const [index, message] of pendingSeparatorErrors) {
		const patternNormalized = normalizeText(operations[index].patternText).text;
		const justified = operations.some((other, otherIndex) => {
			if (otherIndex === index) return false;

View on GitHub (pinned to 9690622007)

Solutions

  1. Add at least one operation: a « line, pattern text, », and replacement.
  2. Check that the section body passed to the parser actually contains the edit payload, not an empty slice.
  3. Verify upstream splitting logic (splitSloppySections) matched the section correctly and didn't drop its body.

Example fix

// before (empty body)
""

// after
"«
old function()
»
new function()"
Defensive patterns

Strategy: validation

Validate before calling

if (sectionBody.trim() === "" || !/^«/m.test(sectionBody)) {
  throw new Error("section body contains no operation (needs «)");
}

Type guard

const hasOperation = (body: string): boolean => body.split("\n").some(l => l.trim() === "«" || l.trim() === "«*");

Try / catch

try {
  const result = await computeSloppySectionDiff(section, cwd);
  if ("error" in result && result.error.includes("Empty patch")) {
    // skip or regenerate the section — there was nothing to apply
  }
} catch (err) { /* ... */ }

Prevention

When it happens

Trigger: Calling apply/computeSloppySectionDiff with an empty section body, a body of whitespace-only lines, or a body whose every line was consumed as tolerated noise (e.g. only a glued «» line) without ever seeing a real « opener.

Common situations: Upstream section splitting produced an empty body; the model emitted only explanations with no actual edit block; a payload got truncated to nothing; wrong variable passed (empty string).

Related errors


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