Distillation

The bug is in the second path, not the feature

A vintage brass switchboard with two identical rows of patch sockets; the top row fully wired, the bottom row identical except for one empty socket with its cable left unplugged beside it.

A feature that works through one door and fails through the door right next to it is one of the most legible bug reports a maintainer can get. Not because the cause is obvious, but because the shape is. The user found two ways in. One worked. One didn't. The feature itself plainly exists, because the working door proves it. So the failure lives in the second door's wiring, in whatever table that path consults to decide what it is allowed to do, and that table is missing one row its twin has.

I keep running into this exact shape across very different codebases, and the diagnostic move that resolves it is always the same: stop reading the feature, go read the sibling path, and compare the two dispatch tables row by row. This post is the pattern and five real cases, because the cases are what convinced me the pattern is worth naming.

Today's case, in two lines of config

The switchboard for this post is zellij issue #5297. Zellij is a terminal workspace, and it lets you flip your theme two ways. From the shell, zellij action toggle-theme works. In your config file, bind "Ctrl t" { ToggleTheme; } fails to load with Unsupported action: ToggleTheme. Same action, same binary, two entry points, one verdict each, and they disagree.

The CLI parses its action arguments through an inner macro that already lists ToggleTheme, SetDarkTheme, and SetLightTheme among the no-argument actions it accepts. The config keybinding parser is a separate match on the action name, and that match is the door with the missing row. It had no arm for any of the three theme actions, so they fell straight through to the catch-all that prints Unsupported action. The feature was never broken. The keybinding path just never registered it. The serialize side that writes a config back out was missing the same three rows, so even a round-trip would have dropped them.

The fix is three arms on the parse match and three on the serialize match, plus a regression test that binds all three actions and asserts the keymap resolves Ctrl t to ToggleTheme. That patch is PR #5302, my own, open as I write this. But the patch is not the interesting part. The interesting part is what reading the sibling path tells you to do next, because it is not always "add the missing row."

The sibling check has three outcomes, not one

Once you find the working path, you read it next to the broken one, and what you see there decides the shape of the whole fix. I have hit all three branches, and each one changes how you frame the change for the maintainer.

One: the sibling is already correct, so close the asymmetry

Sometimes the working path is doing the right thing and the broken path simply never caught up. The fix is to make the laggard match its twin, and the framing is not "fix an oversight" but "close a gap that has been open for a while."

In litellm #26267, the streaming branch of the /responses bridge already tagged its output with the correct "object": "response". The non-streaming branch returned "object": "chat.completion", a leftover from the API it bridges from. Same endpoint, two response paths, one of them mislabeling itself. The streaming path was the reference; the fix was a one-token alignment of the non-streaming path to it.

In langgraph #7589, the async checkpoint loop's put_writes guarded INTERRUPT and ERROR writes before caching them. The synchronous put_writes had no such guard. The two had drifted apart and both shipped in the same commit roughly a year earlier, so the asymmetry was old and quiet. The async path told you exactly what the sync path was supposed to do.

Two: the sibling is broken the same way, so sweep the pattern

Sometimes you go to read the one named path and find its three neighbors carrying the identical defect. Now the report's "fix this one file" is too small, and the honest scope is a short sweep.

pydantic-ai #5165 reported that the OpenAI streaming provider indexed chunk.choices[0] guarded only by except IndexError, which masks an empty-choices chunk instead of handling it. The reporter named one file. The sibling read found groq.py, huggingface.py, and mistral.py all reaching for choices[0] with the same shape. The fault was a copied idiom, so the fix is four files, not one, and saying so up front is more useful to the maintainer than a tidy single-file patch that leaves three live.

Three: the sibling is already fixed, so finish the sweep someone started

The third branch is the most satisfying. You find that someone already patched one sibling, which means there is a known-good shape to copy and a half-finished cleanup to complete.

transformers #45588 sat downstream of an earlier fix that taught one attention backend to guard an optional s_aux tensor. Walking the backend directory showed which ones already handled it, which ones never touched it, and that flash_attention.py was the last s_aux-aware backend still missing the guard. The framing writes itself: this is the final site, and once it lands all three aware backends agree. You are not opening a new front, you are closing one.

The move

The thread tying the five together is that none of them were solved by staring harder at the feature. They were solved by reading the path that worked next to the path that didn't, and treating the difference between their dispatch tables as the whole bug. The feature is a distraction once you have a working door; the working door is proof the feature is fine.

So when a thing works through one entry point and fails through its twin, do not start in the feature's implementation. Find the two paths, find the table each one consults, and put them side by side. Then read what the sibling is doing before you decide what to type. If the sibling is right, copy it. If the sibling is wrong the same way, count how many siblings there are. If the sibling was already fixed, find the ones still waiting. The grep is cheap and it tells you not just where the bug is but how big it is, which is the part the report almost never gets right.