Skip to content

Commit 5d65ea1

Browse files
authored
feat: carry collaboration config in the protocol crate (lockstep) (#35)
Lockstep half of the daemon's "collaboration config on session.create" (collaborative sessions P1a). CONTRIBUTING requires the Rust protocol crate to move in the same change set as `packages/protocol/`. - session.rs: CollaborationConfig + CollaborationRole, camelCase on the wire. `name` and `provider_id` are plain strings, not enums — the role taxonomy is data on the daemon side, so a new role like "security-reviewer" must not need a release of this crate to parse. - client.rs: optional `collaboration` on ClientMessage::SessionCreate. - session.rs: optional `collaboration` on SessionInfo. - app.rs: the TUI's create path passes None — the collaborative create dialog is a later phase; this only keeps the frame builder compiling. PROTOCOL_VERSION deliberately NOT bumped. It stays at 1 on both sides. The change is purely additive (new optional fields), which CONTRIBUTING calls additive-safe and lib.rs scopes the bump to "any breaking change". Bumping only this crate would manufacture a version-mismatch warning on every connect against a daemon that is otherwise fully compatible. Verified: the existing wire_format camelCase test walks nested objects AND arrays, so populating the SessionInfo fixture with a two-role collaboration genuinely asserts `collaboration.roles[N].providerId` serializes camelCase — it is not a vacuous fixture change. cargo build + clippy + test green across the workspace (314 tests).
1 parent 64c2cfc commit 5d65ea1

10 files changed

Lines changed: 85 additions & 3 deletions

File tree

crates/codeoid-protocol/src/client.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use serde::{Deserialize, Serialize};
1616
use serde_json;
1717

18-
use crate::session::SessionMode;
18+
use crate::session::{CollaborationConfig, SessionMode};
1919

2020
/// Tagged union of every message a client can send the daemon.
2121
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -30,6 +30,13 @@ pub enum ClientMessage {
3030
/// daemon fail-closes on unknown ids. Absent = daemon default.
3131
#[serde(default, skip_serializing_if = "Option::is_none")]
3232
provider_id: Option<String>,
33+
/// Make this a collaborative session: one goal worked by several
34+
/// role-children on their own backends. Absent = a normal session.
35+
/// The daemon validates the semantics (provider registered, exactly
36+
/// one orchestrator, claude-only orchestrator in v1) and answers
37+
/// with a specific `invalid_request` error.
38+
#[serde(default, skip_serializing_if = "Option::is_none")]
39+
collaboration: Option<CollaborationConfig>,
3340
},
3441

3542
#[serde(rename = "session.list", rename_all = "camelCase")]

crates/codeoid-protocol/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ pub use message::{
5252
ContentPart, IdentityType, MessageIdentity, MessageRole, SessionMessage, SessionMessageDelta,
5353
};
5454
pub use session::{
55-
ForkedFrom, SessionInfo, SessionMode, SessionStatus, SessionUsage, SessionWorktree, Subagent,
56-
TurnUsage,
55+
CollaborationConfig, CollaborationRole, ForkedFrom, SessionInfo, SessionMode, SessionStatus,
56+
SessionUsage, SessionWorktree, Subagent, TurnUsage,
5757
};
5858
pub use tool::{CancelReason, ConfirmedBy, ToolInfo, ToolPhase, ToolState};
5959

crates/codeoid-protocol/src/session.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ pub struct SessionInfo {
9494
/// "⎇ <branch>" tag in the session title.
9595
#[serde(default, skip_serializing_if = "Option::is_none")]
9696
pub worktree: Option<SessionWorktree>,
97+
98+
/// Collaboration this session orchestrates — goal + role→backend
99+
/// bindings — when it was created with the Collaborative toggle.
100+
/// Absent = a normal session. Persisted daemon-side, so it survives a
101+
/// restart the way `role`/`provider_id` already do.
102+
#[serde(default, skip_serializing_if = "Option::is_none")]
103+
pub collaboration: Option<CollaborationConfig>,
97104
}
98105

99106
/// Where a forked session came from — the parent id, the parent's name at
@@ -118,6 +125,45 @@ pub struct SessionWorktree {
118125
pub created_by_codeoid: bool,
119126
}
120127

128+
/// One role in a collaborative session — a `{backend, model}` binding chosen
129+
/// per purpose.
130+
///
131+
/// `name` is a free-form string, not an enum: the role taxonomy is data, so
132+
/// the daemon can add "security-reviewer" as a config change and this crate
133+
/// keeps parsing it without a release.
134+
#[derive(Debug, Clone, Serialize, Deserialize)]
135+
#[serde(rename_all = "camelCase")]
136+
pub struct CollaborationRole {
137+
pub name: String,
138+
/// Backend this role's children run on. The daemon fail-closes on an id
139+
/// it does not have registered.
140+
pub provider_id: String,
141+
/// Model within that backend. `None` = the backend's own default.
142+
#[serde(default, skip_serializing_if = "Option::is_none")]
143+
pub model: Option<String>,
144+
/// How many children to fan out for this role. `None` = 1; >1 is what
145+
/// makes a review panel a panel.
146+
#[serde(default, skip_serializing_if = "Option::is_none")]
147+
pub count: Option<u32>,
148+
/// What this role is for; surfaced in the child's brief.
149+
#[serde(default, skip_serializing_if = "Option::is_none")]
150+
pub purpose: Option<String>,
151+
}
152+
153+
/// Collaborative-session config: one goal worked by several role-children on
154+
/// possibly different backends. Sent on `session.create` and echoed back on
155+
/// [`SessionInfo`].
156+
///
157+
/// Exactly one role must be named `orchestrator`, and in v1 it must sit on
158+
/// the claude backend. The daemon enforces both and answers with a specific
159+
/// error, so this crate carries only the shape.
160+
#[derive(Debug, Clone, Serialize, Deserialize)]
161+
#[serde(rename_all = "camelCase")]
162+
pub struct CollaborationConfig {
163+
pub goal: String,
164+
pub roles: Vec<CollaborationRole>,
165+
}
166+
121167
/// Rotation telemetry — how many times the backing Claude Code session has
122168
/// been rolled over to avoid context compaction.
123169
#[derive(Debug, Clone, Serialize, Deserialize)]

crates/codeoid-protocol/tests/wire_format.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,25 @@ fn sample_session_info() -> SessionInfo {
118118
branch: "codeoid/fix-a1b2".into(),
119119
created_by_codeoid: true,
120120
}),
121+
collaboration: Some(codeoid_protocol::CollaborationConfig {
122+
goal: "Add rate limiting to the public API".into(),
123+
roles: vec![
124+
codeoid_protocol::CollaborationRole {
125+
name: "orchestrator".into(),
126+
provider_id: "claude".into(),
127+
model: None,
128+
count: None,
129+
purpose: None,
130+
},
131+
codeoid_protocol::CollaborationRole {
132+
name: "review".into(),
133+
provider_id: "gemini".into(),
134+
model: Some("gemini-2.5-pro".into()),
135+
count: Some(3),
136+
purpose: Some("independent critique".into()),
137+
},
138+
],
139+
}),
121140
}
122141
}
123142

@@ -179,6 +198,7 @@ fn client_messages_are_camel_case_on_wire() {
179198
name: "n".into(),
180199
workdir: "/".into(),
181200
provider_id: Some("pi".into()),
201+
collaboration: None,
182202
},
183203
),
184204
("SessionList", ClientMessage::SessionList { id: "1".into() }),

crates/codeoid-tui/src/app.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2515,6 +2515,9 @@ fn session_create_message(
25152515
name,
25162516
workdir,
25172517
provider_id,
2518+
// The TUI has no collaborative-create surface yet (that lands with
2519+
// the extended create dialog); omitted means a normal session.
2520+
collaboration: None,
25182521
}
25192522
}
25202523

@@ -2649,6 +2652,7 @@ mod tests {
26492652
provider_id: None,
26502653
forked_from: None,
26512654
worktree: None,
2655+
collaboration: None,
26522656
});
26532657
state
26542658
}

crates/codeoid-tui/src/state/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,7 @@ mod tests {
11551155
provider_id: None,
11561156
forked_from: None,
11571157
worktree: None,
1158+
collaboration: None,
11581159
}
11591160
}
11601161

crates/codeoid-tui/src/state/sessions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ mod tests {
130130
provider_id: None,
131131
forked_from: None,
132132
worktree: None,
133+
collaboration: None,
133134
}
134135
}
135136

crates/codeoid-tui/src/ui/approval.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ mod tests {
292292
provider_id: None,
293293
forked_from: None,
294294
worktree: None,
295+
collaboration: None,
295296
});
296297
let mut m = msg(
297298
MessageRole::ToolCall,

crates/codeoid-tui/src/ui/scrollback.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ mod tests {
643643
provider_id: None,
644644
forked_from: None,
645645
worktree: None,
646+
collaboration: None,
646647
}
647648
}
648649

crates/codeoid-tui/src/ui/worker.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ mod tests {
440440
provider_id: None,
441441
forked_from: None,
442442
worktree: None,
443+
collaboration: None,
443444
});
444445
state.provider_commands.insert(
445446
"s1".into(),

0 commit comments

Comments
 (0)