Blog · August 16, 2026
feature_request_resolve's input schema had an idempotency_key field. Its implementation had zero references to it — not dropped at the RPC boundary, dropped one line earlier, inside the function that was supposed to use it.
Ledgenter has two feature-request write paths: create, which files a new request, and resolve, which a maintainer calls to close one out with a status, a note, and optionally a shipped ref. Both are exposed as MCP tools, both get their input validated by a zod schema before anything touches the database, and until this fix, both schemas advertised the same idempotency_key field for retry-safety — the pattern Ledgenter uses elsewhere so a caller that times out and resends doesn't double-file or double-act.
One of the two actually used it
resolve(input) {
return run(ctx, featureRequestResolveInput, input, async ({ input, client }) => {
const res = await client.rpc("feature_request_resolve", {
p_id: input.id,
p_status: input.status,
...(input.status_note !== undefined ? { p_status_note: input.status_note } : {}),
...(input.shipped_ref !== undefined ? { p_shipped_ref: input.shipped_ref } : {}),
});
...
Four fields go from input into the RPC call. idempotency_key isn't one of them — not filtered out, not conditionally omitted, just never named. create(), a few lines above it in the same file, does the opposite: it resolves the key and forwards it as p_idempotency_key, because migration 0063 gave feature_request_create's underlying RPC a parameter to receive it and an idempotency_keys ledger row to check it against. feature_request_resolve's RPC has neither. Nothing was wired wrong — there was simply nothing on the other end for resolve()'s implementation to wire.
Validated is not the same claim as honored
The schema didn't know that. idempotency_key was declared as an ordinary optional field on featureRequestResolveInputObject, which is .strict() — a caller who included it got no rejection, no warning; the field was legal input, exactly as legal as status_note or shipped_ref on the same object. The MCP tool's generated JSON schema, built straight from that zod object, advertised it as a real parameter of the tool, sitting next to the two that actually do something. An agent reading the tool description and retrying a resolve call after a timeout, passing the same idempotency_key both times expecting the second call to no-op safely, was doing exactly what the interface told it to do. The value made it through validation and then evaporated — not rejected, not logged, not silently clamped to a default, just absent from every line of code that ran after.
It happened to be harmless here. resolve()'s writes are forward-only and coalesce-based — set the status, keep or overwrite the note, keep or overwrite the ref — so a bare retry with no idempotency key at all already converges to the same row a properly-keyed retry would have. There's no double-file, no double-charge, no duplicate row this bug could have produced; the ledger it was reaching for was never necessary in the first place. That's what made it survive: a field that does nothing is invisible on every green test run, because there was never a wrong outcome to catch — only a promise in the schema that the implementation had no way to keep.
The fix is the comment that should have shipped with the field
// No idempotency_key: the RPC (0063) takes no p_idempotency_key param — status/note/ref
// writes are forward-only + coalesce-based, so a bare retry converges to the same row
// with no idempotency ledger needed. A prior schema draft carried the field anyway,
// which meant the tool's generated JSON schema advertised a parameter the RPC silently
// discarded on every call.
create() keeps its idempotency_key — its RPC has a real parameter and a real ledger check waiting for it. Only resolve() drops the field, because only resolve() had nothing behind it. The schema now says exactly as much as the implementation does, in both directions: no more, no less.
This is the same bug class as two defect-audit fixes from the same week — repo_query and decision_query both silently dropped filter arguments a caller had every reason to believe were being applied. Those were reads: a caller thought it was narrowing a query and wasn't. This is a write: a caller thought it was buying retry-safety and wasn't. Same shape, opposite direction — read-side validation that doesn't reach the query, write-side validation that doesn't reach the RPC call — and the same fix in both cases: make the schema stop promising something the code underneath it never does.
Found via the write-path counterpart to the audit that caught those two: instead of asking "does every accepted filter reach the query," asking "does every accepted field reach the RPC call it's supposedly configuring" — read every write-path domain function's RPC-arg construction end to end and check it against its own input schema, field by field, in both directions.
Start at ledgenter.com.