Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/jp_cli/src/config_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,12 @@ impl ConfigPipeline {
unsets: &[String],
) -> Result<PartialAppConfig> {
let mut partial = conversation.fill_from(self.base.clone());
if !unsets.is_empty() {
debug!(
?unsets,
"Conversation clears suppressing base-layer values."
);
}
for path in unsets {
if let Err(error) = partial.unset(path) {
warn!(%path, %error, "Ignoring a config delta unset for an unknown field.");
Expand Down
13 changes: 10 additions & 3 deletions crates/jp_conversation/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,12 @@ impl ConversationStream {
Ok(partial)
}

/// Dotted config paths explicitly cleared and not subsequently set.
/// Dotted config paths explicitly cleared and still absent from the
/// accumulated state.
///
/// Reset deltas discard earlier clears.
/// A clear followed by a replacement in the same apply delta is not
/// included.
/// A path the accumulated state holds a value at is not included, whether
/// the value arrived in the clearing delta or a later one.
/// Unknown paths are ignored.
///
/// # Errors
Expand All @@ -252,6 +253,12 @@ impl ConversationStream {
}
}

// Nothing cleared means nothing to probe, and the fold below is a full
// merge per delta in the stream.
if unsets.is_empty() {
return Ok(Vec::new());
}

let partial = self.config_partial()?;
Ok(unsets
.into_iter()
Expand Down
12 changes: 12 additions & 0 deletions crates/jp_conversation/src/stream_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,18 @@ fn a_delta_that_only_clears_is_recorded() {
assert!(resolved_arguments(&stream).is_empty());
}

/// A stream that never cleared anything reports nothing, deltas or not.
#[test]
fn config_unsets_is_empty_without_a_clearing_delta() {
let mut stream = stream_with_server(&["serve"]);
stream.add_config_delta(ApplyDelta::new(
delta_timestamp(),
server_arguments_partial(&["run"]),
));

assert!(stream.config_unsets().unwrap().is_empty());
}

#[test]
fn config_unsets_keeps_clears_across_unrelated_deltas() {
let mut stream = stream_with_server(&["serve"]);
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/rfd-summaries.json
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@
"summary": "UPPERCASE keywords NONE and WORKSPACE reset config state; loader.reset entry setting provides local resets; ConfigDelta becomes enum to persist resets."
},
"079-config-sources-and-load-order.md": {
"hash": "f6dc6f90d9a17bd469d82db2b8c4998f43cd8d126e774dc117cc4a9337978321",
"hash": "1a9ca633586e826b8ecd5a44d5412e811eb26c3515d270a97c43b9b88e93e7fe",
"summary": "JP loads config from four implicit sources plus environment variables, with extends directives and inherit flags controlling precedence."
},
"074-eager-loading-with-command-declared-data-requirements.md": {
Expand Down
17 changes: 17 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,23 @@ Set configuration in conversation <conversation id>
This appends a delta to the conversation's event stream, leaving earlier turns
untouched.

An option is cleared by querying with it set to `null`:

```bash
$ jp query --id <conversation id> --cfg assistant.name:=null "..."
```

A cleared option stays cleared for the rest of the conversation.
Later turns run without it even when a configuration file sets that option.
The conversation records the option as empty, and the file layer cannot fill it
back in.

Two things retire a clear:

- Setting a value at the same path, such as `--cfg assistant.name=Bot`.
- A reset (`--cfg=WORKSPACE`), which discards everything the conversation
accumulated, including its clears.

To edit the files directly, use `jp conversation edit`:

```bash
Expand Down
5 changes: 5 additions & 0 deletions docs/rfd/079-config-sources-and-load-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ init`), the sequence is:
11. The result is the **base partial** for this invocation.
12. Load the conversation's `base_config.json` and event-stream `ConfigDelta`s
(for continuing or forking invocations only).
A path those deltas cleared and never set again ([RFD 070]) is also cleared
from the base partial, so the file layer cannot restore it.
Step 13 can set it again.
13. Apply `--cfg` and `--no-cfg` ([RFD 038]) directives left-to-right ([RFD
008]).
14. Apply CLI shortcut flags (`--model`, `--reasoning`, etc.).
Expand All @@ -278,6 +281,7 @@ pipeline may care about; see `ConfigPipeline::partial_without_conversation` and
`config_load_paths` resolution across roots.
- [RFD 054]: Split Conversation Config and Events — how `base_config.json` and
event-stream `ConfigDelta`s are structured.
- [RFD 070]: Negative Config Deltas — how a delta records a cleared path.
- `crates/jp_cli/src/lib.rs` — `load_partial_configs_from_files` and
`load_base_partial`.
- `crates/jp_config/src/util.rs` — `load_partials_with_inheritance`,
Expand All @@ -290,4 +294,5 @@ pipeline may care about; see `ConfigPipeline::partial_without_conversation` and
[RFD 035]: 035-multi-root-config-load-path-resolution.md
[RFD 038]: 038-config-reset-keywords.md
[RFD 054]: 054-split-conversation-config-and-events.md
[RFD 070]: 070-negative-config-deltas.md
[RFD 080]: 080-editor-as-a-config-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# No surface lists the config options a conversation has cleared

- **Status**: Todo
- **Kind**: Feature
- **Authors**: jp
- **Date**: 2026-09-16
- **Label**: client=cli
- **Label**: domain=conversation
- **Label**: package=jp_cli
- **Label**: type=enhancement

A conversation can clear a config option (`jp query --cfg
assistant.name:=null`), and the clear holds for the rest of the conversation:
the file layer cannot restore the option, because the pipeline reapplies the
stream's cleared paths after filling from the base.

Nothing reports which paths a conversation currently holds clear.

`jp config show` resolves the file, env, and `--cfg` layers only —
`Commands::Config(_)` returns an empty conversation layer
(`crates/jp_cli/src/cmd.rs:200-207`), and `cmd/config/show.rs` never touches a
conversation.
So the option reads as absent, with no indication that a conversation-level
clear is what holds it there.
The only trace is the `unsets` array inside the stored event stream, reachable
through `jp conversation edit --events`.

The named input: a user clears `assistant.name` in a conversation, sets
`assistant.name` in `.jp/config.toml` a week later, and has no way to find out
why that conversation ignores it.
`RUST_LOG=debug` logs the suppressed paths at the moment they apply, which helps
a user who already suspects the cause, but no read surface answers the question
directly.

## Shape

Two candidates, not exclusive:

- Teach `jp config show` to apply the conversation layer when given `--id`, and
mark cleared paths distinctly from unset ones.
- Report them in `jp conversation show`, alongside the conversation's other
stored state.

`ConversationStream::config_unsets()` already computes this list, so either
surface is a read away.

Related: [RFD 060] (Config Explain) would subsume this if it lands first — a
per-field provenance display answers "what is holding this field empty?" as a
special case.

[RFD 060]: https://jp.computer/rfd/060
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# jp config set reports success for a clear it never records

- **Status**: Todo
- **Kind**: Bug
- **Authors**: jp
- **Date**: 2026-09-16
- **Label**: client=cli
- **Label**: domain=conversation
- **Label**: package=jp_cli
- **Label**: type=bug

`jp config set --id <id> --cfg assistant.name:=null` prints `Set configuration
in conversation <id>` and records nothing.
The same argument through `jp query` records a delta whose `unsets` holds
`assistant.name`, and the option stays cleared for the rest of the conversation.

## Why

`Set::run` builds its payload with
`config_pipeline::build_partial_from_cfg_args`
(`crates/jp_cli/src/cmd/config/set.rs:28`), which applies the arguments onto
`PartialAppConfig::empty()` (`config_pipeline.rs:578`).
A `:=null` assignment clears a field, so clearing a field of an already-empty
partial produces an empty partial: the payload carries no trace of what the user
asked to clear.

`override_to_record` then merges that empty partial onto the conversation's
accumulated state, resolves both sides, finds them equal, and returns `None`
(`config_pipeline.rs:64-78`), so no `ConfigDelta` is appended.
The success line is printed unconditionally afterwards (`set.rs:68-69`).

The file target has the same hole: `set_in_file` merges the empty partial into
the config file and writes it back unchanged (`set.rs:85-90`).

A partial cannot express "cleared" — that is what `ApplyDelta::unsets` exists
for ([RFD 070]).
The query path reaches it through `turn_config_delta`/`delta_with_unsets`
(`cmd/query.rs:2333-2351`); the `config set` path has no equivalent.

## Fix

Carry the cleared paths alongside the partial, the way the conversation layer
now does: have `build_partial_from_cfg_args` report the paths its arguments
cleared, and have `set_in_conversations` pass them to `ApplyDelta::with_unsets`
so a clear becomes a recorded delta.
For the file target, a clear means removing the key from the file, which
`ConfigFile`'s format-preserving edit already knows how to express.

Failing either, the command should refuse `:=null` with an error naming the
working alternative, rather than reporting a change it did not make.

## Verifying

A test asserting that `jp config set --id <id> --cfg assistant.name:=null`
leaves the conversation resolving `assistant.name` to `None` on the next
invocation, mirroring
`resolve_config_keeps_a_cleared_conversation_field_across_invocations` in
`crates/jp_cli/src/cmd/query_tests.rs`.

[RFD 070]: https://jp.computer/rfd/070
Loading