{"record":{"id":"1e03a73072ffd201","repo":"can1357/oh-my-pi","slug":"sharpshooter-consolidation-must-call-replace-memor","errorCode":null,"errorMessage":"sharpshooter consolidation must call replace_memory_files exactly once","messagePattern":"sharpshooter consolidation must call replace_memory_files exactly once","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/sharpshooter/consolidate.ts","lineNumber":235,"sourceCode":"\tfor (const name of [\"AGENTS.md\", \"CLAUDE.md\"]) {\n\t\tconst content = await Bun.file(path.join(cwd, name))\n\t\t\t.text()\n\t\t\t.catch(() => \"\");\n\t\tif (content.trim()) blocks.push(`--- ${name} ---\\n${content.trim()}`);\n\t}\n\treturn truncateApproxTokens(blocks.join(\"\\n\\n\"), PROJECT_DOC_TOKEN_LIMIT);\n}\n\nfunction parseReplacementFiles(\n\tcontent: readonly unknown[],\n\tcurrentFiles: Readonly<Record<SharpshooterMemoryFile, string>>,\n): ReplacementFile[] {\n\tconst toolCalls = content.filter(\n\t\t(block): block is { type: \"toolCall\"; name: string; arguments: unknown } =>\n\t\t\ttypeof block === \"object\" && block !== null && \"type\" in block && block.type === \"toolCall\",\n\t);\n\tif (toolCalls.length !== 1 || toolCalls[0]?.name !== replaceMemoryFilesTool.name) {\n\t\tthrow new Error(\"sharpshooter consolidation must call replace_memory_files exactly once\");\n\t}\n\n\tconst args = toolCalls[0].arguments;\n\tif (!args || typeof args !== \"object\" || !(\"files\" in args) || !Array.isArray(args.files)) {\n\t\tthrow new Error(\"replace_memory_files requires a files array\");\n\t}\n\n\tconst seen = new Set<SharpshooterMemoryFile>();\n\tconst files: ReplacementFile[] = [];\n\tfor (const item of args.files) {\n\t\tif (!item || typeof item !== \"object\" || !(\"name\" in item) || !(\"content\" in item)) {\n\t\t\tthrow new Error(\"replace_memory_files contains an invalid file entry\");\n\t\t}\n\t\tconst name = item.name;\n\t\tconst rawContent = item.content;\n\t\tif (!isMemoryFileName(name) || typeof rawContent !== \"string\") {\n\t\t\tthrow new Error(\"replace_memory_files contains an invalid file entry\");\n\t\t}","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/sharpshooter/consolidate.ts#L217-L253","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-run the consolidation, ideally with a stronger model or with toolChoice:'required' enforced.","Check the consolidation prompt/tool schema so the model is clearly instructed to call replace_memory_files exactly once.","Inspect the raw response content (log it) to see what the model actually emitted and adjust the prompt.","Switch to a provider/model known to honor tool-choice reliably for this step.","Add retry-around-parse logic that re-prompts when the contract is violated."],"exampleFix":"// before\n// model replied with prose, no tool call -> throws\n// after\n// enforce tool selection\n...complete({ ..., toolChoice: \"required\", tools: [replaceMemoryFilesTool] })","handlingStrategy":"validation","validationCode":"const toolCalls = response.content.filter(b => b?.type === \"toolCall\");\nif (toolCalls.length !== 1 || toolCalls[0].name !== \"replace_memory_files\") {\n  // re-prompt or reject before calling parseReplacementFiles\n}\n","typeGuard":"function isSingleReplaceCall(content: unknown[]): content is [{ type: \"toolCall\"; name: \"replace_memory_files\"; arguments: unknown }] {\n  const calls = content.filter((b): b is { type: \"toolCall\"; name: string } =>\n    typeof b === \"object\" && b !== null && \"type\" in b && (b as { type: string }).type === \"toolCall\");\n  return calls.length === 1 && calls[0].name === \"replace_memory_files\";\n}","tryCatchPattern":"try {\n  const files = parseReplacementFiles(response.content, currentFiles);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"must call replace_memory_files exactly once\")) {\n    // re-run consolidation with toolChoice:'required' or log response for prompt tuning\n  } else throw err;\n}","preventionTips":["Always pass toolChoice:'required' with only replace_memory_files in the tools list for consolidation.","Log raw model output on parse failure to tune the prompt.","Prefer models/providers with reliable forced tool-use for this step.","Retry the consolidation automatically when the tool contract is violated."],"tags":["tool-calling","llm-output","contract-violation"],"backgroundTag":"tool-call-contract-violation","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}