can1357/oh-my-pi · critical · anyhow::Error

Overlapping replacements detected; refine pattern to avoid a

Error message

Overlapping replacements detected; refine pattern to avoid ambiguous edits

What it means

_write_json checks process.stdin before writing a command frame. If the subprocess's stdin pipe is None (process exited and pipes closed, or spawned without stdin=PIPE), it raises RpcProcessExitError, signalling the RPC server side is gone or misconfigured.

Source

Thrown at crates/pi-ast/src/ops.rs:316

	let mut sorted: Vec<&Edit<String>> = edits.iter().collect();
	sorted.sort_by(|a, b| {
		a.position
			.cmp(&b.position)
			.then(a.deleted_length.cmp(&b.deleted_length))
			.then(a.inserted_text.cmp(&b.inserted_text))
	});
	// Byte-identical edits (same span, same replacement) are one deterministic
	// edit: multiple patterns matching the same node collapse instead of
	// tripping the overlap check. Only divergent overlaps are ambiguous.
	sorted.dedup_by(|a, b| {
		a.position == b.position
			&& a.deleted_length == b.deleted_length
			&& a.inserted_text == b.inserted_text
	});
	let mut prev_end = 0usize;
	for edit in &sorted {
		if edit.position < prev_end {
			return Err(anyhow!(
				"Overlapping replacements detected; refine pattern to avoid ambiguous edits"
			));
		}
		prev_end = edit.position.saturating_add(edit.deleted_length);
	}

	let mut output = content.to_string();
	for edit in sorted.into_iter().rev() {
		let start = edit.position;
		let end = edit.position.saturating_add(edit.deleted_length);
		if end > output.len() || start > end {
			return Err(anyhow!("Computed edit range is out of bounds"));
		}
		let replacement = std::str::from_utf8(&edit.inserted_text)
			.map_err(|err| anyhow!("Replacement text is not valid UTF-8: {err}"))?;
		output.replace_range(start..end, replacement);
	}
	Ok(output)

View on GitHub (pinned to 9690622007)

Solutions

  1. Restart the client: stop() the current instance, create a new RpcClient, start() again, and retry the command.
  2. Check server stderr/logs to find why the child process exited before the write.
  3. Verify the spawn command and that the client constructs the process with stdin=PIPE.
  4. Catch RpcProcessExitError around requests and implement a restart-and-retry wrapper.

Example fix

// before
result = await client.request("prompt", {...})  # raises after server died
// after
try:
    result = await client.request("prompt", {...})
except RpcProcessExitError:
    await client.stop()
    client = RpcClient(cmd)
    await client.start()
    result = await client.request("prompt", {...})
Defensive patterns

Strategy: try-catch

Validate before calling

if client._process is None or client._process.poll() is not None:
    await client.stop(); await client.start()  # restart before sending

Type guard

def process_alive(client: RpcClient) -> bool:
    p = client._process
    return p is not None and p.poll() is None

Try / catch

try:
    result = await client.request("prompt", payload)
except RpcProcessExitError:
    await client.stop()
    client = RpcClient(cmd)
    await client.start()
    result = await client.request("prompt", payload)

Prevention

When it happens

Trigger: The child RPC process has crashed/exited and its stdin pipe was closed; the subprocess was created without stdin redirection; a concurrent stop() closed stdin while a write was in flight.

Common situations: Server binary crashed earlier (or never started correctly), OS killed the child, or long-running session where the server died mid-session and the next command is attempted.

Related errors


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