Repository navigation
ctx_note write fails when required-all tool schemas fill both ID fields #460
Description
Summary
ctx_note(action="write") can become unusable on tool-calling surfaces that require every declared property to be present. The model fills the action-irrelevant optional fields with schema-valid placeholder values (observed as note_id: 0 and note_ids: [1]), and Magic Context rejects the call before dispatching on action:
Error: 'note_id' and 'note_ids' cannot be used together; provide one or the other.
This is a Magic Context action-normalization issue exposed by a required-all provider/tool surface.
Environment
- Magic Context:
0.42.5 - OpenCode:
1.18.31 - Client: OpenCode TUI (CLI)
- Platform: Linux x64
- Bun used for repository verification:
1.4.2
Reproduction
Ask the agent to create a conditional note using only the documented write parameters:
{
"action": "write",
"content": "Re-run the integration test after the next release.",
"surface_condition": "When the latest release tag is newer than v1.0.0"
}On the affected tool-calling surface, the actual generated tool payload repeatedly becomes:
{
"action": "write",
"content": "Re-run the integration test after the next release.",
"note_id": 0,
"note_ids": [1],
"surface_condition": "When the latest release tag is newer than v1.0.0"
}The call then fails with:
Error: 'note_id' and 'note_ids' cannot be used together; provide one or the other.
Retrying with an explicit instruction to omit both fields does not help because the provider-visible tool surface requires all declared properties and the model must produce values for them.
Expected behavior
For action: "write", note_id and note_ids are irrelevant and should not prevent the note from being created.
The documented action contract is:
write: requirescontent; accepts optionalsurface_condition; uses neither ID field.read: uses neither ID field.update: requiresnote_id; does not usenote_ids.dismiss: accepts exactly one ofnote_idornote_ids.
Actual behavior
createCtxNoteTool() computes the action and immediately applies a global mutual-exclusion check before action-specific dispatch:
const action = args.action ?? (args.content?.trim() ? "write" : "read");
const hasNoteId = args.note_id !== undefined;
const hasNoteIds = args.note_ids !== undefined;
if (hasNoteId && hasNoteIds) {
return "Error: 'note_id' and 'note_ids' cannot be used together; provide one or the other.";
}Location: packages/plugin/src/tools/ctx-note/tools.ts around lines 317–325.
As a result, fields that have no meaning for write are validated as though they were part of the selected action.
Root-cause evidence
- The Magic Context source correctly declares
note_idandnote_idswith.optional()inctxNoteArgsShape. - A direct handler call containing only
actionandcontentsucceeds. Existingtools.test.tscoverage exercises this path. - The OpenCode 1.18.31 plugin registry wraps plugin arguments with
z.object(args)and the resulting object JSON Schema keeps these fields out ofrequired. A local same-instance and cross-Zod-instance conversion check produced norequiredarray for the optional fields. - The provider-facing tool surface in the affected session nevertheless requires every tool property. The model therefore emits schema-valid filler values. The values are generated to satisfy the visible schema; they are not inserted by
ctx_note.execute(). - The implementation already recognizes this provider behavior for another field:
// GPT-family models fill every optional param (content:"" for a read) ...However, that compatibility handling currently covers action inference from empty content only. It does not normalize action-irrelevant ID fields.
Suggested fix
Normalize arguments according to the selected action before applying ID validation:
write/read: ignorenote_idandnote_ids.update: use and validatenote_id; ignore action-irrelevantnote_idsfiller.dismiss: retain strictnote_idversusnote_idsmutual exclusion and all existing positive-ID/list-size checks.
This preserves strict validation where the IDs are semantically meaningful while making the multi-action tool robust on required-all tool protocols.
An alternative would be an action-discriminated provider schema, but the current OpenCode v1 plugin API exposes args as a raw Zod shape rather than a discriminated object union, so handler-side action normalization appears to be the narrowest compatible fix.
Regression coverage requested
Please add tests proving that:
writesucceeds withnote_id: 0andnote_ids: [1]present as irrelevant filler.readsucceeds with both filler fields present.updatesucceeds with a validnote_ideven ifnote_idsfiller is present.dismissstill rejects a request that intentionally supplies both meaningful forms.dismissstill validates positive integer IDs and the 1–50 batch-size contract.
Verification already performed
bun test src/tools/ctx-note/tools.test.ts
25 pass, 0 fail
bun test src/plugin/tool-registry.test.ts src/tools/ctx-note/schema.test.ts
15 pass, 0 fail
These passing tests confirm the current source-level schema and normal direct-handler path, but they do not cover required-all provider filler values crossing into action-specific validation.
Activity
magic-alfonso commentedon Sep 17, 2026
Thanks — exact diagnosis, and the reproduction matched at source.
We took it one step further than the suggested per-action normalization. Having both
note_idandnote_idswas the actual defect: two fields for one idea, plus a mutual-exclusion rule that ran before the action was even looked at. On master now:ctx_notehas a single id field,note_ids: exactly one id forupdate, one to fifty fordismiss, ignored bywriteandread.note_id, the exclusion error and the "only valid for dismiss" rule are gone on OpenCode, Pi/OMP and the Claude Code facade.writewithnote_id: 0andnote_ids: [1]as filler) now creates the note. Tests cover filler onwriteandread, exactly-one onupdate, and the 1–50 contract ondismiss.note_idsto the module, so batch dismiss had been failing there since it shipped. Fixed in the same change.The same class (models filling every declared property) is being audited on the other four tools as handler-only changes. Ships in v0.42.6.
magic-alfonso commentedon Sep 19, 2026
Shipped in v0.42.6:
ctx_notehas one id field,note_ids, and filler values on unused properties are ignored rather than rejected across OpenCode, Pi and the Rust facade.