From b05e11e4c79d6e572242ce92afa1c610c8ac2309 Mon Sep 17 00:00:00 2001 From: Yash Datta Date: Sun, 26 Jul 2026 23:35:38 +0800 Subject: [PATCH] feat: carry role-child fields in the protocol crate (lockstep) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lockstep half of the daemon's P1b (highflame-ai/codeoid#250-ish — collaboration role-children). CONTRIBUTING requires this crate to move with the daemon's packages/protocol. - `write: Option` on CollaborationRole. None/false = read-only; the daemon turns that into a leaf identity with no write scope plus a hard tool deny at its fence, so a reviewer provably cannot write. Opt-in per role. - New `CollaborationRoleRef` + `collaboration_role` on SessionInfo: parent session id, role name, ordinal, write. The mirror of `collaboration` (set on the orchestrating parent), so the TUI can group a fleet without inferring it from session names. - Six SessionInfo fixtures gain `collaboration_role: None`. PROTOCOL_VERSION stays at 1 on both sides, same reasoning as #35: purely additive optional fields, which CONTRIBUTING calls additive-safe and lib.rs scopes the bump to breaking changes. Bumping one side alone would manufacture a mismatch warning against a fully compatible daemon. The wire_format SessionInfo fixture now populates collaboration_role (a real session is either a parent or a child, never both — populated anyway so the recursive camelCase walker visits parentSessionId/roleName and would catch a rename_all slip). cargo build + clippy + test green: 354 tests, no new clippy warnings (7 before, 7 after on codeoid-protocol). --- crates/codeoid-protocol/src/lib.rs | 4 +-- crates/codeoid-protocol/src/session.rs | 32 ++++++++++++++++++++ crates/codeoid-protocol/tests/wire_format.rs | 10 ++++++ crates/codeoid-tui/src/app.rs | 1 + crates/codeoid-tui/src/state/mod.rs | 1 + crates/codeoid-tui/src/state/sessions.rs | 1 + crates/codeoid-tui/src/ui/approval.rs | 1 + crates/codeoid-tui/src/ui/scrollback.rs | 1 + crates/codeoid-tui/src/ui/worker.rs | 1 + 9 files changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/codeoid-protocol/src/lib.rs b/crates/codeoid-protocol/src/lib.rs index 4b3cfdf..aafdd83 100644 --- a/crates/codeoid-protocol/src/lib.rs +++ b/crates/codeoid-protocol/src/lib.rs @@ -52,8 +52,8 @@ pub use message::{ ContentPart, IdentityType, MessageIdentity, MessageRole, SessionMessage, SessionMessageDelta, }; pub use session::{ - CollaborationConfig, CollaborationRole, ForkedFrom, SessionInfo, SessionMode, SessionStatus, - SessionUsage, SessionWorktree, Subagent, TurnUsage, + CollaborationConfig, CollaborationRole, CollaborationRoleRef, ForkedFrom, SessionInfo, + SessionMode, SessionStatus, SessionUsage, SessionWorktree, Subagent, TurnUsage, }; pub use tool::{CancelReason, ConfirmedBy, ToolInfo, ToolPhase, ToolState}; diff --git a/crates/codeoid-protocol/src/session.rs b/crates/codeoid-protocol/src/session.rs index 953f5da..1384456 100644 --- a/crates/codeoid-protocol/src/session.rs +++ b/crates/codeoid-protocol/src/session.rs @@ -101,6 +101,12 @@ pub struct SessionInfo { /// restart the way `role`/`provider_id` already do. #[serde(default, skip_serializing_if = "Option::is_none")] pub collaboration: Option, + + /// Set when this session is a role-CHILD of a collaborative session. + /// Absent = not a collaboration child. Pairs with `collaboration` above, + /// which marks the orchestrating parent. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub collaboration_role: Option, } /// Where a forked session came from — the parent id, the parent's name at @@ -148,6 +154,32 @@ pub struct CollaborationRole { /// What this role is for; surfaced in the child's brief. #[serde(default, skip_serializing_if = "Option::is_none")] pub purpose: Option, + /// Whether this role's children may modify the workspace. + /// + /// `None`/`false` = read-only, and that default is load-bearing: the + /// daemon gives a read-only role a leaf identity with no write scope at + /// all, so a reviewer provably cannot write rather than being asked not + /// to. Write authority is opt-in per role. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub write: Option, +} + +/// Set on a role-CHILD of a collaborative session: which collaboration it +/// belongs to and which role it plays. +/// +/// The mirror of [`SessionInfo::collaboration`] (set on the orchestrating +/// parent), so a client can group a fleet without inferring it from names. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CollaborationRoleRef { + /// Session id of the orchestrating parent. + pub parent_session_id: String, + /// Role name from the parent's config (already lowercased by the daemon). + pub role_name: String, + /// 1-based index within this role's fan-out (`review` ×3 → 1, 2, 3). + pub ordinal: u32, + /// Whether this child's identity carries write authority. + pub write: bool, } /// Collaborative-session config: one goal worked by several role-children on diff --git a/crates/codeoid-protocol/tests/wire_format.rs b/crates/codeoid-protocol/tests/wire_format.rs index 65055cd..e698fd6 100644 --- a/crates/codeoid-protocol/tests/wire_format.rs +++ b/crates/codeoid-protocol/tests/wire_format.rs @@ -127,6 +127,7 @@ fn sample_session_info() -> SessionInfo { model: None, count: None, purpose: None, + write: None, }, codeoid_protocol::CollaborationRole { name: "review".into(), @@ -134,9 +135,18 @@ fn sample_session_info() -> SessionInfo { model: Some("gemini-2.5-pro".into()), count: Some(3), purpose: Some("independent critique".into()), + write: Some(false), }, ], }), + // A real session is either a parent or a child, never both; populated + // here anyway so the camelCase walker visits this struct's keys too. + collaboration_role: Some(codeoid_protocol::CollaborationRoleRef { + parent_session_id: "parent-1".into(), + role_name: "review".into(), + ordinal: 2, + write: false, + }), } } diff --git a/crates/codeoid-tui/src/app.rs b/crates/codeoid-tui/src/app.rs index 3510607..2d941d1 100644 --- a/crates/codeoid-tui/src/app.rs +++ b/crates/codeoid-tui/src/app.rs @@ -2653,6 +2653,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, }); state } diff --git a/crates/codeoid-tui/src/state/mod.rs b/crates/codeoid-tui/src/state/mod.rs index 684e181..f94df8d 100644 --- a/crates/codeoid-tui/src/state/mod.rs +++ b/crates/codeoid-tui/src/state/mod.rs @@ -1156,6 +1156,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, } } diff --git a/crates/codeoid-tui/src/state/sessions.rs b/crates/codeoid-tui/src/state/sessions.rs index 50b8f6c..1ac3458 100644 --- a/crates/codeoid-tui/src/state/sessions.rs +++ b/crates/codeoid-tui/src/state/sessions.rs @@ -131,6 +131,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, } } diff --git a/crates/codeoid-tui/src/ui/approval.rs b/crates/codeoid-tui/src/ui/approval.rs index 9619d59..51c0758 100644 --- a/crates/codeoid-tui/src/ui/approval.rs +++ b/crates/codeoid-tui/src/ui/approval.rs @@ -293,6 +293,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, }); let mut m = msg( MessageRole::ToolCall, diff --git a/crates/codeoid-tui/src/ui/scrollback.rs b/crates/codeoid-tui/src/ui/scrollback.rs index 9368ed3..fbca6dd 100644 --- a/crates/codeoid-tui/src/ui/scrollback.rs +++ b/crates/codeoid-tui/src/ui/scrollback.rs @@ -644,6 +644,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, } } diff --git a/crates/codeoid-tui/src/ui/worker.rs b/crates/codeoid-tui/src/ui/worker.rs index c0a3c4d..51c539e 100644 --- a/crates/codeoid-tui/src/ui/worker.rs +++ b/crates/codeoid-tui/src/ui/worker.rs @@ -441,6 +441,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, }); state.provider_commands.insert( "s1".into(),