can1357/oh-my-pi · error

replace_memory_files requires a files array

Error message

replace_memory_files requires a files array

What it means

parseReplacementFiles validates the arguments of the single `replace_memory_files` tool call that sharpshooter consolidation requires. This error is thrown when the call's arguments are missing, not a plain object, or lack an array-valued `files` field. It guards the downstream per-entry validation loop which assumes `args.files` is an array.

Source

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

	}
	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");
		}
		if (seen.has(name)) throw new Error(`replace_memory_files contains duplicate ${name}`);
		seen.add(name);
		const redacted = redactSecrets(rawContent);
		let lines = redacted.length > 0 ? 1 : 0;
		for (let index = 0; index + 1 < redacted.length; index++) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Ensure the model call forces a strict tool schema so `files` is always an array (toolChoice required plus a JSON-schema-typed tool definition).
  2. Check the raw tool-call arguments in the response to see what shape was actually emitted.
  3. Retry the consolidation run; malformed arguments from an LLM are usually transient.
  4. If integrating programmatically, pass `{ files: [{ name, content }, ...] }` with `files` as an array.

Example fix

// before: args shaped as an object map
replace_memory_files({ "AGENTS.md": "..." })
// after: files must be an array of {name, content}
replace_memory_files({ files: [{ name: "AGENTS.md", content: "..." }] })
Defensive patterns

Strategy: type-guard

Validate before calling

function hasFilesArray(args: unknown): args is { files: unknown[] } {
  return !!args && typeof args === "object" && "files" in args && Array.isArray((args as { files?: unknown }).files);
}

Type guard

const isFilesArgs = (a: unknown): a is { files: Array<{ name: string; content: string }> } =>
  typeof a === "object" && a !== null && Array.isArray((a as any).files);

Try / catch

try {
  const files = parseReplacementFiles(toolCall, currentFiles);
} catch (err) {
  logger.warn("invalid replace_memory_files arguments; skipping consolidation", { err });
}

Prevention

When it happens

Trigger: The LLM calls `replace_memory_files` with no arguments object, with `files` absent, or with `files` set to a non-array value (e.g. an object keyed by filename, or a string).

Common situations: Model schema drift or weak tool-calling models emitting malformed arguments; hand-crafted or replayed tool calls in tests; a prompt change causing the model to pass files as a map instead of an array.

Related errors


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