Skip to content

fix(memory): tolerate null/empty operation field via zod catch - #2112

Open
pansz wants to merge 1 commit into
XiaomiMiMo:mainfrom
pansz:fix/memory-tool-null-operation
Open

fix(memory): tolerate null/empty operation field via zod catch#2112
pansz wants to merge 1 commit into
XiaomiMiMo:mainfrom
pansz:fix/memory-tool-null-operation

Conversation

@pansz

@pansz pansz commented Aug 13, 2026

Copy link
Copy Markdown

Summary / 概要

The memory tool's operation parameter is declared as z.enum(["search"]).default("search") in packages/opencode/src/tool/memory.ts. Serialized to JSON Schema, this becomes a field that is simultaneously required, has a default, and has an enum with a single value — a contradictory shape that some reasoning models interpret as "send null to mean 'use the default'".

zod's .default() fires for missing keys and explicit undefined, but not for explicit null or "". Calls from those models fail at the parameter-validation step with:

Invalid arguments for the memory tool:
✖ Invalid input: expected "search"
  → at operation

…before the tool ever runs. Most models (Claude / GPT / Gemini) emit "search" correctly and never hit this path, which is why the bug has been latent since the initial open-source release and only surfaces on certain reasoning-model providers (e.g. MiniMax-M3).

memory 工具的 operation 参数在 packages/opencode/src/tool/memory.ts 中声明为 z.enum(["search"]).default("search")。序列化到 JSON Schema 时,这个字段同时是 required、有 default、且 enum 只有一个值——一种自相矛盾的形态。部分推理类模型会把它解读成"发 null 等于使用默认值"。

zod 的 .default() 会在字段缺失或显式 undefined 时触发,但对显式 null"" 不会触发。来自这些模型的调用会在参数校验阶段直接失败(错误如上),工具根本无法运行。Claude / GPT / Gemini 等大多数模型会正确发出 "search",所以这个 bug 从开源初始版本起就一直潜伏,只在部分推理模型(例如 MiniMax-M3)上才暴露。

Reproduction / 复现

const parameters = z.object({
  operation: z.enum(["search"]).default("search").describe("Memory operation to perform"),
  query: z.string(),
})
parameters.parse({ operation: null, query: "x" })
// → throws ZodError: "Invalid input: expected \"search\" → at operation"

Verified against zod@4.1.8 (catalog version, unchanged from prior releases).

zod@4.1.8(catalog 版本,自此前版本未变)下验证。

Fix / 修复

One-line change — add .catch("search") between .default(...) and .describe(...):

一行修改——在 .default(...).describe(...) 之间加 .catch("search")

-operation: z.enum(["search"]).default("search").describe("Memory operation to perform"),
+operation: z.enum(["search"]).default("search").catch("search").describe("Memory operation to perform"),

.catch("search") swallows any parse failure on this field and substitutes "search" — the only legal value, which matches the schema's semantic intent ("search is the only operation"). The enum, default, and describe are all preserved, so any future refactor of this field stays patch-friendly.

.catch("search") 吞掉这个字段上所有解析失败并替换为 "search"——即唯一合法值,与 schema 的语义意图一致("search 是唯一的操作")。enum、default、describe 都保留,未来如果有人重构这个字段,本补丁依然可以轻松叠上去。

Verification / 验证

Input / 输入 Before / 修复前 After / 修复后
{operation: "search", ...}
{...} (field omitted / 字段省略)
{operation: undefined, ...}
{operation: null, ...} expected "search" ✓ falls back to "search"
{operation: "", ...} expected "search" ✓ falls back to "search"

Manually tested in TUI with minimax-cn-coding-plan/MiniMax-M3 — the failing call now returns search results instead of Invalid input: expected "search".

在 TUI 中用 minimax-cn-coding-plan/MiniMax-M3 实测:原本失败的调用现在能正常返回搜索结果,而不是 Invalid input: expected "search"

Notes / 备注

  • The schema, default, and describe are all unchanged; this is purely a defensive fallback that only fires when the upstream value is unparseable. It does not alter behavior for any model that already emits "search" correctly.

  • Alternative considered: remove the operation field entirely. Rejected because it is a structural breaking change for any tool consumer / future schema work; the catch keeps the schema shape stable.

  • schema、default、describe 全部未改;这只是一个防御性的兜底,只在上游值无法解析时才会触发。对任何已经能正确发出 "search" 的模型,行为完全不变。

  • 备选方案:直接把 operation 字段从 schema 里移除。否决理由:这是结构性破坏性变更,对未来工具消费者与 schema 演进不友好;.catch 方案保持了 schema 形状的稳定。

The memory tool operation schema is z.enum(["search"]).default("search").
Serialized to JSON Schema, this becomes a field that is simultaneously
required, has a default, and has an enum with a single value — a
contradictory shape that some reasoning models (notably MiniMax-M3)
interpret as "send null to mean use the default". zod .default() fires
for undefined/missing keys but not for explicit null or "", so those
calls fail with "Invalid input: expected \"search\"" at the operation
path before the tool ever runs.

Add .catch("search") so any invalid operation value falls back to the
only legal value, matching the schema semantic intent (search is the
only operation). One-line change; the enum, default, and describe are
unchanged, so upstream refactors that touch this field stay
patch-friendly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant