Expose loaded thread environments in app-server responses (#42386)

## What changed

- Add the experimental `Thread.environments` field with each selected environment's ID, working directory, and runtime workspace roots.
- Populate the field from live thread state in start, resume, read, list, and unarchive responses. Use `null` for unloaded threads and an empty list when a loaded thread has no environment selection.
- Document that the field reports selection rather than connection status and does not affect executor selection or resume behavior.

## Testing

- Cover selection updates, unloaded and resumed threads, loaded pathless unarchives, and clients that subscribe after environment connection notifications.

GitOrigin-RevId: a6daa06aff7089d7c5a1e4c7d03109b41f12fa3c
This commit is contained in:
Anton Panasenko
2026-09-02 23:41:14 +00:00
committed by copyberry
parent cff76fa96f
commit 2b554fd3f9
42 changed files with 681 additions and 11 deletions
@@ -1994,7 +1994,7 @@ impl ThreadRequestProcessor {
if let Ok(loaded_thread) = self.thread_manager.get_thread(thread_uuid).await {
thread.session_id = loaded_thread.session_configured().session_id.to_string();
let config_snapshot = loaded_thread.config_snapshot().await;
apply_live_model_settings(&mut thread, &config_snapshot);
apply_live_thread_settings(&mut thread, &config_snapshot);
}
self.attach_thread_name(thread_uuid, &mut thread).await;
thread.status = resolve_thread_status(
@@ -2049,6 +2049,10 @@ impl ThreadRequestProcessor {
let (mut thread, _) =
thread_from_stored_thread(stored_thread, fallback_provider.as_str(), &self.config.cwd);
if let Ok(loaded_thread) = self.thread_manager.get_thread(thread_id).await {
let config_snapshot = loaded_thread.config_snapshot().await;
apply_live_thread_settings(&mut thread, &config_snapshot);
}
thread.status = resolve_thread_status(
self.thread_watch_manager
.loaded_status_for_thread(&thread.id)
@@ -2985,7 +2989,7 @@ impl ThreadRequestProcessor {
} else {
fallback_thread
};
apply_live_model_settings(&mut thread, &config_snapshot);
apply_live_thread_settings(&mut thread, &config_snapshot);
self.apply_thread_read_store_fields(thread_id, &mut thread, include_turns, loaded_thread)
.await?;
Ok(thread)
@@ -4284,7 +4288,7 @@ impl ThreadRequestProcessor {
);
thread_summary.session_id = existing_thread.session_configured().session_id.to_string();
thread_summary.thread_source = config_snapshot.thread_source.clone().map(Into::into);
apply_live_model_settings(&mut thread_summary, &config_snapshot);
apply_live_thread_settings(&mut thread_summary, &config_snapshot);
thread_summary.can_accept_direct_input = Some(can_accept_direct_input(
existing_thread.multi_agent_version(),
&config_snapshot.session_source,
@@ -4649,7 +4653,7 @@ impl ThreadRequestProcessor {
)),
};
let mut thread = thread?;
apply_live_model_settings(&mut thread, &config_snapshot);
apply_live_thread_settings(&mut thread, &config_snapshot);
thread.can_accept_direct_input = Some(can_accept_direct_input);
thread.id = thread_id.to_string();
thread.session_id = session_id;
@@ -5147,7 +5151,7 @@ impl ThreadRequestProcessor {
));
thread.session_id = session_configured.session_id.to_string();
thread.thread_source = config_snapshot.thread_source.clone().map(Into::into);
apply_live_model_settings(&mut thread, &config_snapshot);
apply_live_thread_settings(&mut thread, &config_snapshot);
if thread.path.is_none() {
thread.project_id = inherited_project_id.clone();
}
@@ -5908,6 +5912,7 @@ pub(crate) fn thread_from_stored_thread(
let thread_id = thread.thread_id.to_string();
let thread = Thread {
id: thread_id.clone(),
environments: None,
extra: None,
session_id: thread_id,
forked_from_id: thread.forked_from_id.map(|id| id.to_string()),
@@ -6096,6 +6101,13 @@ fn build_thread_from_snapshot(
let now = time::OffsetDateTime::now_utc().unix_timestamp();
Thread {
id: thread_id.to_string(),
environments: Some(
config_snapshot
.environment_selections()
.iter()
.map(Into::into)
.collect(),
),
extra: None,
session_id,
forked_from_id: None,