feat(config): Let every list state how it merges - #1130
Open
JeanMertz wants to merge 7 commits into
Open
Conversation
JeanMertz
force-pushed
the
config-list-strategies
branch
from
September 8, 2026 08:09
ebccf46 to
4524ddd
Compare
`partial_via` substituted the partial's field type only when the field was also `nested`, and `nested` requires the element type to be a `Config`. A `Vec<String>` could therefore not carry a wrapper in its partial, which is what a list needs in order to declare its own merge strategy: five of JP's six plain-list config fields hold scalars. The substitution now applies to a plain field too, and `generate_from_partial_value` converts back to the field's own type on the way out. The conversion wraps the inner access rather than the container, since it is the value whose type differs and not its `Option` or its `Box`, and the nullable case maps over the option instead of converting it. A field opting in gains a partial of `Option<Wrapper<T>>` while the resolved config keeps `Vec<T>`, so callers see no change and the wrapper is confined to the merge path. Signed-off-by: Jean Mertz <git@jeanmertz.com>
`editor.envs` appended across layers with no way to say otherwise. It
now carries a strategy of its own, so a user can replace the inherited
list rather than extend it:
```toml
[editor]
envs = { value = ["MY_EDITOR"], strategy = "replace" }
```
The plain list still works and still appends, since that is the
declared default.
This is the first plain-list field to carry a wrapper in its partial,
which the `partial_via` change makes possible. Two pieces make it work
for a list of scalars rather than of configs: `try_some_mergeable_
strings` accepts either shape from `--cfg`, telling them apart the way
the type's own deserializer does, and `delta_opt_mergeable_vec` diffs
the field through the wrapper so a removal or a reorder is recorded as
a replacement instead of being dropped.
`editor.envs` is also the last list in the config whose removals were
recorded through `unsets`; that mechanism is now only needed for
scalars, and for the five remaining plain lists until they follow.
Signed-off-by: Jean Mertz <git@jeanmertz.com>
`providers.llm.anthropic.beta_headers` appended across layers with no
way to say otherwise. It now carries a strategy of its own:
```toml
[providers.llm.anthropic]
beta_headers = { value = ["interleaved-thinking-2025-05-14"], strategy = "replace" }
```
Deduplication is unchanged. `vec_with_strategy` deduplicates any
combining merge unless a config opts out, which is what
`append_vec_dedup` did for this field, so a plain list behaves exactly
as before.
`PartialAnthropicConfig` loses its `delta_with_unsets` override
entirely: with the last of its fields able to state `replace` on the
wire, nothing here needs a path reported, and the trait's default diff
is correct. That is the shape the rest of the conversion takes as it
lands, one field at a time.
Signed-off-by: Jean Mertz <git@jeanmertz.com>
`providers.mcp.<server>.arguments` and `.variables` appended across
layers with no way to say otherwise. Each now carries a strategy of its
own:
```toml
[providers.mcp.bookworm]
arguments = { value = ["serve"], strategy = "replace" }
```
That closes the last of the reported bug. Dropping an argument is now
recorded: the delta says `replace` and carries the whole list, where
before appending could not shorten one, the difference went unrecorded,
and every turn recomputed the same non-delta. `PartialMcpProviderConfig`
loses its `delta_with_unsets` override, since no field there needs a
path reported.
The new `ordered_vec_with_strategy` merge keeps duplicates unless a
config asks for deduplication, the opposite of the default. An argument
list is a command line: `["--flag", "x", "--flag", "y"]` means
something else once the second `--flag` is dropped. The opinion has to
live on the merge function rather than on the field's default, because
config layering merges partials before any defaults are filled in --
carrying it as a value let a plain list silently deduplicate, which a
test caught.
Signed-off-by: Jean Mertz <git@jeanmertz.com>
`assistant.model.parameters.stop_words` appended across layers with no
way to say otherwise. It now carries a strategy of its own, at every
site the model parameters are reached from -- the assistant, the inquiry
assistant, the title generator and the reasoning summary model:
```toml
[assistant.model.parameters]
stop_words = { value = ["\n\n"], strategy = "replace" }
```
The field also deduplicates now, matching every other converted list.
Two identical stop words have the same effect as one, so nothing about
generation changes; `providers.mcp.*.arguments` stays the sole exception,
because a repeated flag in a command line is not a duplicate.
`PartialParametersConfig` keeps its `delta_with_unsets` override for the
scalars around it, but `stop_words` no longer contributes to it: a
dropped word is recorded as a replacement at whichever site it was
reached from, with no path to report.
Signed-off-by: Jean Mertz <git@jeanmertz.com>
`config_load_paths` accumulated across layers with duplicates dropped
and no way to ask for anything else. It now carries a strategy of its
own, which matters because `--cfg <name>` walks the list in order and
takes the first directory holding a match:
```toml
config_load_paths = { value = [".jp/agents"], strategy = "prepend" }
```
A user-level config can put its own directory ahead of the workspace's
rather than behind it, and a workspace can replace an inherited list
instead of adding to it. The plain list still appends and deduplicates,
so nothing changes for a config that does not ask.
This is the last plain list in the configuration, so every list-valued
field now states how it merges. Four helpers that existed only to guess
at it are gone: `delta_opt_vec`, `delta_opt_vec_at`, `append_vec_dedup`
and `try_some_vec`. A list no longer needs a path reported in `unsets`
to record a removal -- it carries `replace` and the fold does the rest,
leaving `unsets` to the scalars, which genuinely cannot say it.
`try_some_mergeable_vec` takes an element parser, so a list of anything
that is not a `String` -- here `RelativePathBuf` -- can accept both the
plain and the `{ value, strategy }` shape from `--cfg`.
Signed-off-by: Jean Mertz <git@jeanmertz.com>
JeanMertz
force-pushed
the
config-list-strategies
branch
from
September 10, 2026 19:55
4524ddd to
3768fc1
Compare
Signed-off-by: Jean Mertz <git@jeanmertz.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A list-valued setting can say how it combines with the layer beneath
it:
Six fields gain it:
editor.envs,assistant.model.parameters.stop_words,providers.llm.anthropic.beta_headers,providers.mcp.*.argumentsand.variables, andconfig_load_paths. A plain list merges the way itsfield declares, so nothing changes for a config that does not ask.
What it buys beyond user control: a conversation can record a removed or
reordered element, which an appending merge cannot express. Four helpers
that existed only to guess at a strategy are deleted.
argumentsandvariableskeep duplicates, since a repeated flag on acommand line is not a duplicate. Every other list deduplicates.
Reviewer note: the first commit changes the vendored
schematicmacroso
partial_viaworks on a field whose element type is not a nestedconfig, which is what a list of strings needs.