can1357/oh-my-pi · error

sharpshooter consolidation must call replace_memory_files ex

Error message

sharpshooter consolidation must call replace_memory_files exactly once

What it means

parseReplacementFiles enforces the consolidation model's tool-use contract: the response content must contain exactly one tool call and it must be the replace_memory_files tool. Anything else — zero calls, multiple calls, or a different tool — throws this error, protecting the memory bank from ambiguous or malformed consolidation output.

Source

Thrown at packages/coding-agent/src/sharpshooter/consolidate.ts:235

	for (const name of ["AGENTS.md", "CLAUDE.md"]) {
		const content = await Bun.file(path.join(cwd, name))
			.text()
			.catch(() => "");
		if (content.trim()) blocks.push(`--- ${name} ---\n${content.trim()}`);
	}
	return truncateApproxTokens(blocks.join("\n\n"), PROJECT_DOC_TOKEN_LIMIT);
}

function parseReplacementFiles(
	content: readonly unknown[],
	currentFiles: Readonly<Record<SharpshooterMemoryFile, string>>,
): ReplacementFile[] {
	const toolCalls = content.filter(
		(block): block is { type: "toolCall"; name: string; arguments: unknown } =>
			typeof block === "object" && block !== null && "type" in block && block.type === "toolCall",
	);
	if (toolCalls.length !== 1 || toolCalls[0]?.name !== replaceMemoryFilesTool.name) {
		throw new Error("sharpshooter consolidation must call replace_memory_files exactly once");
	}

	const args = toolCalls[0].arguments;
	if (!args || typeof args !== "object" || !("files" in args) || !Array.isArray(args.files)) {
		throw new Error("replace_memory_files requires a files array");
	}

	const seen = new Set<SharpshooterMemoryFile>();
	const files: ReplacementFile[] = [];
	for (const item of args.files) {
		if (!item || typeof item !== "object" || !("name" in item) || !("content" in item)) {
			throw new Error("replace_memory_files contains an invalid file entry");
		}
		const name = item.name;
		const rawContent = item.content;
		if (!isMemoryFileName(name) || typeof rawContent !== "string") {
			throw new Error("replace_memory_files contains an invalid file entry");
		}

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-run the consolidation, ideally with a stronger model or with toolChoice:'required' enforced.
  2. Check the consolidation prompt/tool schema so the model is clearly instructed to call replace_memory_files exactly once.
  3. Inspect the raw response content (log it) to see what the model actually emitted and adjust the prompt.
  4. Switch to a provider/model known to honor tool-choice reliably for this step.
  5. Add retry-around-parse logic that re-prompts when the contract is violated.

Example fix

// before
// model replied with prose, no tool call -> throws
// after
// enforce tool selection
...complete({ ..., toolChoice: "required", tools: [replaceMemoryFilesTool] })
Defensive patterns

Strategy: validation

Validate before calling

const toolCalls = response.content.filter(b => b?.type === "toolCall");
if (toolCalls.length !== 1 || toolCalls[0].name !== "replace_memory_files") {
  // re-prompt or reject before calling parseReplacementFiles
}

Type guard

function isSingleReplaceCall(content: unknown[]): content is [{ type: "toolCall"; name: "replace_memory_files"; arguments: unknown }] {
  const calls = content.filter((b): b is { type: "toolCall"; name: string } =>
    typeof b === "object" && b !== null && "type" in b && (b as { type: string }).type === "toolCall");
  return calls.length === 1 && calls[0].name === "replace_memory_files";
}

Try / catch

try {
  const files = parseReplacementFiles(response.content, currentFiles);
} catch (err) {
  if (err instanceof Error && err.message.includes("must call replace_memory_files exactly once")) {
    // re-run consolidation with toolChoice:'required' or log response for prompt tuning
  } else throw err;
}

Prevention

When it happens

Trigger: The consolidation model returns no tool call (plain text answer), multiple tool calls, or calls a different tool instead of replace_memory_files; also occurs when toolChoice:'required' is not honored by the provider/model.

Common situations: Using a model/provider that poorly supports forced tool choice; prompt drift causing the model to answer in prose; a model emitting parallel tool calls; provider quirk returning toolCall blocks with unexpected names after a schema change.

Related errors


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