Skip to content
Merged
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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,23 @@ npm: <https://www.npmjs.com/package/@hasna/codewith/v/0.1.87>
Compare: <https://github.com/hasna/codewith/compare/rust-v0.1.86...rust-v0.1.87>

This release prevents capability-shaped values stored in coordination metadata
from reaching the model through compact mission-control overview fields.
from reaching the model through compact mission-control overview fields. It
also keeps long-lived prompt input responsive, scopes spawned-child listeners
to their parent lineage, and avoids reusing incompatible app-server daemons.

### Fixed

- Mission control: redact capability-shaped values from session titles and
compact schedule or interaction previews before returning model-visible
overview data. (#525)
- App server: inherit spawned-child listeners only from connections subscribed
to the parent thread, while preserving global manual-thread discovery.
- TUI: update pending-approval state incrementally and compact buffered
transcript events when a persisted closed thread can be reloaded.
- TUI: reuse the default app-server daemon only when its reported version
exactly matches the TUI; fall back to the embedded server after a mismatch,
failed handshake, or two-second version-probe timeout without restarting the
existing daemon.

## [0.1.86] - 2026-08-09

Expand Down
7 changes: 1 addition & 6 deletions codex-rs/app-server/src/in_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,13 +492,8 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
created = thread_created_rx.recv(), if listen_for_threads => {
match created {
Ok(thread_id) => {
let connection_ids = if session.initialized() {
vec![IN_PROCESS_CONNECTION_ID]
} else {
Vec::<ConnectionId>::new()
};
processor
.try_attach_thread_listener(thread_id, connection_ids)
.try_attach_thread_lineage_listeners(thread_id)
.await;
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
Expand Down
11 changes: 1 addition & 10 deletions codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1121,17 +1121,8 @@ pub async fn run_main_with_transport_options(
created = thread_created_rx.recv(), if listen_for_threads => {
match created {
Ok(thread_id) => {
let mut initialized_connection_ids = Vec::new();
for (connection_id, connection_state) in &connections {
if connection_state.session.initialized() {
initialized_connection_ids.push(*connection_id);
}
}
processor
.try_attach_thread_listener(
thread_id,
initialized_connection_ids,
)
.try_attach_thread_lineage_listeners(thread_id)
.await;
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
Expand Down
8 changes: 2 additions & 6 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -904,13 +904,9 @@ impl MessageProcessor {
.await;
}

pub(crate) async fn try_attach_thread_listener(
&self,
thread_id: ThreadId,
connection_ids: Vec<ConnectionId>,
) {
pub(crate) async fn try_attach_thread_lineage_listeners(&self, thread_id: ThreadId) {
self.thread_processor
.try_attach_thread_listener(thread_id, connection_ids)
.try_attach_thread_lineage_listeners(thread_id)
.await;
}

Expand Down
72 changes: 62 additions & 10 deletions codex-rs/app-server/src/request_processors/thread_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2933,12 +2933,9 @@ impl ThreadRequestProcessor {
self.thread_watch_manager.subscribe_running_turn_count()
}

/// Best-effort: ensure initialized connections are subscribed to this thread.
pub(crate) async fn try_attach_thread_listener(
&self,
thread_id: ThreadId,
connection_ids: Vec<ConnectionId>,
) {
/// Best-effort: inherit listeners from the newly created thread's parent.
pub(crate) async fn try_attach_thread_lineage_listeners(&self, thread_id: ThreadId) {
let mut connection_ids = Vec::new();
let mut raw_events_enabled = false;
if let Ok(thread) = self.thread_manager.get_thread(thread_id).await {
let config_snapshot = thread.config_snapshot().await;
Expand All @@ -2949,7 +2946,14 @@ impl ThreadRequestProcessor {
thread.rollout_path(),
);
self.thread_watch_manager.upsert_thread(loaded_thread).await;
if let Some(parent_thread_id) = config_snapshot.parent_thread_id {
if let Some(parent_thread_id) = resolve_thread_parent_id(
config_snapshot.parent_thread_id,
&config_snapshot.session_source,
) {
connection_ids = self
.thread_state_manager
.subscribed_connection_ids(parent_thread_id)
.await;
raw_events_enabled = self
.thread_state_manager
.thread_state(parent_thread_id)
Expand Down Expand Up @@ -4948,16 +4952,24 @@ fn permission_profile_trusts_project(
}
}

fn resolve_thread_parent_id(
explicit_parent_thread_id: Option<ThreadId>,
session_source: &CoreSessionSource,
) -> Option<ThreadId> {
explicit_parent_thread_id.or_else(|| session_source.parent_thread_id())
}

fn build_thread_from_snapshot(
thread_id: ThreadId,
session_id: String,
config_snapshot: &ThreadConfigSnapshot,
path: Option<PathBuf>,
) -> Thread {
let now = time::OffsetDateTime::now_utc().unix_timestamp();
let parent_thread_id = config_snapshot
.parent_thread_id
.or_else(|| config_snapshot.session_source.parent_thread_id());
let parent_thread_id = resolve_thread_parent_id(
config_snapshot.parent_thread_id,
&config_snapshot.session_source,
);
Thread {
id: thread_id.to_string(),
session_id,
Expand Down Expand Up @@ -5001,6 +5013,46 @@ fn build_thread_from_loaded_snapshot(
)
}

#[cfg(test)]
mod lineage_parent_tests {
use super::*;

#[test]
fn explicit_parent_takes_precedence_over_session_source() {
let explicit_parent_thread_id = ThreadId::new();
let source_parent_thread_id = ThreadId::new();
let session_source = CoreSessionSource::SubAgent(CoreSubAgentSource::ThreadSpawn {
parent_thread_id: source_parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
});

assert_eq!(
resolve_thread_parent_id(Some(explicit_parent_thread_id), &session_source),
Some(explicit_parent_thread_id)
);
}

#[test]
fn spawned_thread_uses_session_source_parent() {
let parent_thread_id = ThreadId::new();
let session_source = CoreSessionSource::SubAgent(CoreSubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
});

assert_eq!(
resolve_thread_parent_id(/*explicit_parent_thread_id*/ None, &session_source),
Some(parent_thread_id)
);
}
}

#[cfg(test)]
#[path = "thread_processor_tests.rs"]
mod thread_processor_tests;
Loading
Loading