diff --git a/codex-rs/app-server/tests/suite/v2/thread_delete.rs b/codex-rs/app-server/tests/suite/v2/thread_delete.rs index e3c2a354dd..9c9f96d865 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_delete.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_delete.rs @@ -391,7 +391,7 @@ async fn thread_delete_removes_persisted_board_even_with_feature_disabled() -> R /*busy_timeout*/ None, ) .await?; - assert_eq!(board_counts(&pool).await?, (1, 1, 1)); + assert_eq!(board_counts(&pool).await?, (1, 1, 2)); let _: ThreadArchiveResponse = app .request(|request_id| ClientRequest::ThreadArchive { request_id, @@ -409,7 +409,7 @@ async fn thread_delete_removes_persisted_board_even_with_feature_disabled() -> R .with_codex_home(home.path()) .build_initialized() .await?; - assert_eq!(board_counts(&pool).await?, (1, 1, 1)); + assert_eq!(board_counts(&pool).await?, (1, 1, 2)); let _: ThreadDeleteResponse = app .request(|request_id| ClientRequest::ThreadDelete { request_id, diff --git a/codex-rs/ext/agent-message-board/src/api.rs b/codex-rs/ext/agent-message-board/src/api.rs index 495fe0a25e..fee95aa4ac 100644 --- a/codex-rs/ext/agent-message-board/src/api.rs +++ b/codex-rs/ext/agent-message-board/src/api.rs @@ -51,6 +51,7 @@ pub trait AgentMessageBoard: Send + Sync { /// Uses the caller's configured clock; clock failures must not create a post. /// The request ID identifies a logical call across retries. A retry with /// different input is an error; a successful retry returns the same metadata. + /// Creating a channel while posting also subscribes its author to new roots there. fn post(&self, caller: ThreadId, request: PostRequest) -> BoxFuture<'_, Result>; fn list_threads( diff --git a/codex-rs/ext/agent-message-board/src/local.rs b/codex-rs/ext/agent-message-board/src/local.rs index ee4a069b56..6413ae1764 100644 --- a/codex-rs/ext/agent-message-board/src/local.rs +++ b/codex-rs/ext/agent-message-board/src/local.rs @@ -235,6 +235,12 @@ impl LocalAgentMessageBoard { PostDestination::NewChannel(channel) => { validate_channel(channel)?; self.insert_channel(&mut tx, channel, &author, now).await?; + self.subscribe( + &mut tx, + &SubscriptionTarget::Channel(channel.clone()), + caller, + ) + .await?; ( channel.clone(), id, diff --git a/codex-rs/ext/agent-message-board/src/tools/spec.rs b/codex-rs/ext/agent-message-board/src/tools/spec.rs index 134ec17473..c9b4410785 100644 --- a/codex-rs/ext/agent-message-board/src/tools/spec.rs +++ b/codex-rs/ext/agent-message-board/src/tools/spec.rs @@ -87,6 +87,9 @@ pub(super) fn tool(name: &str, namespace: Option<&str>, namespace_description: & let mut properties = serde_json::Map::new(); for field in fields { let schema = match *field { + "new_channel_name" => { + json!({"type":"string","description":"Create and subscribe to this channel."}) + } "subscribe" => { json!({"type":"boolean","description":"Subscribe to new roots. Default true."}) } diff --git a/codex-rs/ext/agent-message-board/tests/local_board.rs b/codex-rs/ext/agent-message-board/tests/local_board.rs index 8ed21b1e70..6c1eaa0292 100644 --- a/codex-rs/ext/agent-message-board/tests/local_board.rs +++ b/codex-rs/ext/agent-message-board/tests/local_board.rs @@ -102,11 +102,13 @@ async fn shared_handles_resume_posts_and_preserve_subscription_rules() { .await .unwrap(); first - .create_channel( + .post( child, - CreateChannelRequest { - channel_name: "proofs".into(), - subscription: SubscriptionChange::Subscribe, + PostRequest { + request_id: "create-proofs".into(), + destination: PostDestination::NewChannel("proofs".into()), + text: "Share proofs here.".into(), + agents_to_notify: Vec::new(), }, ) .await @@ -115,7 +117,8 @@ async fn shared_handles_resume_posts_and_preserve_subscription_rules() { request_id: "call-1".into(), destination: PostDestination::Channel("proofs".into()), text: "é🦀 proof".into(), - agents_to_notify: vec![AgentPath::root(), child_path.clone(), child_path.clone()], + // The creator receives this through its channel subscription. + agents_to_notify: vec![AgentPath::root()], }; let second = LocalAgentMessageBoard::open(&sqlite, tree, host.clone()) .await @@ -180,6 +183,19 @@ async fn shared_handles_resume_posts_and_preserve_subscription_rules() { ) .await .unwrap(); + // Posting to an existing channel must not re-enable its subscription. + resumed + .post( + child, + PostRequest { + request_id: "one-off-post".into(), + destination: PostDestination::Channel("proofs".into()), + text: "One more proof, while staying unsubscribed.".into(), + agents_to_notify: Vec::new(), + }, + ) + .await + .unwrap(); // Posting still succeeds when the subscriber lookup returns an empty array. resumed .post(