feat: add config-change extension contributor (#22488)

## Why

Extensions can observe thread and turn lifecycle events today, but there
was no single host-owned hook for changes to the effective thread
configuration. That makes features that need to react to model,
permission, or tool-suggest updates either depend on individual mutation
paths or risk going stale after runtime config refreshes.

This adds a typed config-change contributor so extension-owned state can
stay synchronized with the effective thread config while the host
remains responsible for deciding when config changed.

## What Changed

- Added `ConfigContributor<C>` to `codex_extension_api`, with
before/after immutable snapshots of the effective config plus
session/thread extension stores.
- Added registry builder/accessor support through `config_contributor`
and `config_contributors`.
- Emits config-change callbacks after committed updates from session
settings, per-turn setting updates, and `refresh_runtime_config`.
- Builds effective config snapshots only when config contributors are
registered, and suppresses no-op callbacks when the before/after
snapshots are equal.
- Added a core session regression test that verifies contributors
observe both model changes and user-layer runtime config changes,
including access to session and thread extension stores.

## Validation

Added `config_change_contributor_observes_effective_config_changes` in
`codex-rs/core/src/session/tests.rs` to cover the new contributor path.
This commit is contained in:
jif-oai
2026-05-13 17:13:34 +02:00
committed by GitHub
Unverified
parent 87de4e3290
commit 34bb85519f
6 changed files with 235 additions and 3 deletions
+23
View File
@@ -438,6 +438,18 @@ impl Session {
per_turn_config
}
pub(crate) fn build_effective_session_config(
session_configuration: &SessionConfiguration,
) -> Config {
let mut config =
Self::build_per_turn_config(session_configuration, session_configuration.cwd.clone());
config.model = Some(session_configuration.collaboration_mode.model().to_string());
config.permissions.approval_policy = session_configuration.approval_policy.clone();
config.permissions.active_permission_profile =
session_configuration.active_permission_profile.clone();
config
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn make_turn_context(
thread_id: ThreadId,
@@ -588,6 +600,7 @@ impl Session {
sub_id: String,
updates: SessionSettingsUpdate,
) -> CodexResult<Arc<TurnContext>> {
let notify_config_contributors = !self.services.extensions.config_contributors().is_empty();
let update_result: CodexResult<_> = {
let mut state = self.state.lock().await;
match state.session_configuration.clone().apply(&updates) {
@@ -612,6 +625,11 @@ impl Session {
previous_permission_profile != next_permission_profile;
let codex_home = next.codex_home.clone();
let session_source = next.session_source.clone();
let previous_config = notify_config_contributors.then(|| {
Self::build_effective_session_config(&state.session_configuration)
});
let new_config = notify_config_contributors
.then(|| Self::build_effective_session_config(&next));
state.session_configuration = next.clone();
Ok((
next,
@@ -620,6 +638,8 @@ impl Session {
previous_cwd,
codex_home,
session_source,
previous_config,
new_config,
))
}
Err(err) => Err(CodexErr::InvalidRequest(err.to_string())),
@@ -633,6 +653,8 @@ impl Session {
previous_cwd,
codex_home,
session_source,
previous_config,
new_config,
) = match update_result {
Ok(update) => update,
Err(err) => {
@@ -649,6 +671,7 @@ impl Session {
}
};
self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref());
self.maybe_refresh_shell_snapshot_for_cwd(
&previous_cwd,
&session_configuration.cwd,