1use crate::git_worktree::{paths_equal, validate_managed_allocation_root};
2use crate::protocol::{
3 GitObjectId, ManagedWorktreeCleanupFailure, ManagedWorktreeGitScope,
4 ManagedWorktreeLeaseId, ManagedWorktreeLeaseSnapshot, ManagedWorktreeLeaseState,
5 ManagedWorktreeRetention, NodeIncarnationId, SessionRecordId, WorktreeProfileId,
6 WorktreeProfileRevision, WorkspaceId, MAX_MANAGED_WORKTREE_LEASES,
7};
8use gate4agent_node_wire::random_nonce;
9use gate4agent_types::{AgentInstanceId, SessionGeneration};
10use serde::{Deserialize, Serialize};
11use std::collections::{BTreeMap, BTreeSet};
12use std::path::{Path, PathBuf};
13
14const MAX_MANAGED_WORKTREE_TOMBSTONES: usize = MAX_MANAGED_WORKTREE_LEASES;
15const MAX_BRANCH_PREFIX_BYTES: usize = 256;
16const MAX_BASE_REVISION_BYTES: usize = 1_024;
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct ManagedWorktreeProfile {
20 profile_id: WorktreeProfileId,
21 revision: WorktreeProfileRevision,
22 allocation_root: String,
23 branch_prefix: String,
24 base: String,
25 retention: ManagedWorktreeRetention,
26}
27
28impl ManagedWorktreeProfile {
29 pub fn new(
30 profile_id: WorktreeProfileId,
31 revision: WorktreeProfileRevision,
32 allocation_root: impl AsRef<Path>,
33 branch_prefix: impl Into<String>,
34 base: impl Into<String>,
35 retention: ManagedWorktreeRetention,
36 ) -> Result<Self, String> {
37 let allocation_root = allocation_root.as_ref();
38 if !allocation_root.is_absolute() {
39 return Err("managed worktree allocation root must be absolute".to_owned());
40 }
41 let canonical = std::fs::canonicalize(allocation_root)
42 .map_err(|error| format!("managed worktree allocation root is unavailable: {error}"))?;
43 if !canonical.is_dir() {
44 return Err("managed worktree allocation root must be a directory".to_owned());
45 }
46 let allocation_root = canonical.into_os_string().into_string().map_err(|path| {
47 format!(
48 "managed worktree allocation root is not valid Unicode: {}",
49 path.to_string_lossy(),
50 )
51 })?;
52 let allocation_root = crate::platform::normalize_canonical_root(allocation_root);
53 let branch_prefix = branch_prefix.into();
54 if branch_prefix.is_empty()
55 || branch_prefix.len() > MAX_BRANCH_PREFIX_BYTES
56 || branch_prefix.chars().any(char::is_control)
57 || branch_prefix.ends_with('/')
58 {
59 return Err("managed worktree branch prefix is invalid".to_owned());
60 }
61 let base = base.into();
62 if base.is_empty()
63 || base.len() > MAX_BASE_REVISION_BYTES
64 || base.chars().any(char::is_control)
65 {
66 return Err("managed worktree base revision is invalid".to_owned());
67 }
68 Ok(Self {
69 profile_id,
70 revision,
71 allocation_root,
72 branch_prefix,
73 base,
74 retention,
75 })
76 }
77
78 pub fn profile_id(&self) -> &WorktreeProfileId {
79 &self.profile_id
80 }
81
82 pub fn revision(&self) -> &WorktreeProfileRevision {
83 &self.revision
84 }
85
86 pub fn allocation_root(&self) -> &str {
87 &self.allocation_root
88 }
89
90 pub fn branch_prefix(&self) -> &str {
91 &self.branch_prefix
92 }
93
94 pub fn base(&self) -> &str {
95 &self.base
96 }
97
98 pub fn retention(&self) -> ManagedWorktreeRetention {
99 self.retention
100 }
101
102 pub(crate) fn validate_for_workspace(&self, source_root: &str) -> Result<(), String> {
103 validate_managed_allocation_root(source_root, &self.allocation_root)
104 .map_err(|error| error.message)
105 }
106
107 pub(crate) fn validate_target_authority(
108 &self,
109 lease_id: &ManagedWorktreeLeaseId,
110 target_root: &str,
111 ) -> Result<(), String> {
112 let current_root = std::fs::canonicalize(&self.allocation_root)
113 .map_err(|error| format!("managed worktree allocation root is unavailable: {error}"))?;
114 let current_root = crate::platform::normalize_canonical_root(
115 current_root.into_os_string().into_string()
116 .map_err(|_| "managed worktree allocation root is not valid Unicode".to_owned())?,
117 );
118 if !paths_equal(¤t_root, &self.allocation_root) {
119 return Err("managed worktree allocation root identity changed".to_owned());
120 }
121 let target = Path::new(target_root);
122 let expected = PathBuf::from(&self.allocation_root).join(lease_id.as_str());
123 if target != expected
124 || target.file_name().and_then(|name| name.to_str()) != Some(lease_id.as_str())
125 || target.components().any(|component| matches!(
126 component,
127 std::path::Component::CurDir | std::path::Component::ParentDir,
128 ))
129 {
130 return Err("managed worktree target is not the exact allocated lease child".to_owned());
131 }
132 let parent = target.parent()
133 .ok_or_else(|| "managed worktree target has no parent".to_owned())?;
134 let canonical_parent = std::fs::canonicalize(parent)
135 .map_err(|error| format!("managed worktree target parent is unavailable: {error}"))?;
136 let canonical_parent = crate::platform::normalize_canonical_root(
137 canonical_parent.into_os_string().into_string()
138 .map_err(|_| "managed worktree target parent is not valid Unicode".to_owned())?,
139 );
140 if !paths_equal(&canonical_parent, &self.allocation_root)
141 {
142 return Err("managed worktree target is not an exact child of its allocation root".to_owned());
143 }
144 Ok(())
145 }
146}
147
148#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
149pub(crate) struct ManagedWorktreeSessionHolder {
150 pub incarnation_id: NodeIncarnationId,
151 pub instance_id: AgentInstanceId,
152 pub generation: SessionGeneration,
153}
154
155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156pub(crate) struct ManagedWorktreeLeaseRecord {
157 pub lease_id: ManagedWorktreeLeaseId,
158 pub source_workspace_id: WorkspaceId,
159 pub workspace_id: WorkspaceId,
160 pub profile_id: WorktreeProfileId,
161 pub profile_revision: WorktreeProfileRevision,
162 pub target_root: String,
163 pub branch: String,
164 pub base_commit: String,
165 pub expected_head: Option<String>,
166 pub retention: ManagedWorktreeRetention,
167 pub state: ManagedWorktreeLeaseState,
168 pub session_holders: Vec<ManagedWorktreeSessionHolder>,
169 pub record_holders: Vec<SessionRecordId>,
170 pub cleanup_failure: Option<ManagedWorktreeCleanupFailure>,
171 pub created_at_unix_ms: u64,
172 pub updated_at_unix_ms: u64,
173}
174
175impl ManagedWorktreeLeaseRecord {
176 pub(crate) fn snapshot(&self) -> ManagedWorktreeLeaseSnapshot {
177 ManagedWorktreeLeaseSnapshot {
178 lease_id: self.lease_id.clone(),
179 source_workspace_id: self.source_workspace_id.clone(),
180 workspace_id: self.workspace_id.clone(),
181 profile_id: self.profile_id.clone(),
182 profile_revision: self.profile_revision.clone(),
183 retention: self.retention,
184 state: self.state,
185 active_session_count: u16::try_from(self.session_holders.len()).unwrap_or(u16::MAX),
186 managed_record_count: u16::try_from(self.record_holders.len()).unwrap_or(u16::MAX),
187 cleanup_failure: self.cleanup_failure,
188 created_at_unix_ms: self.created_at_unix_ms,
189 updated_at_unix_ms: self.updated_at_unix_ms,
190 }
191 }
192
193 pub(crate) fn has_holders(&self) -> bool {
194 !self.session_holders.is_empty() || !self.record_holders.is_empty()
195 }
196}
197
198#[derive(Clone, Debug, Default, Eq, PartialEq)]
199pub(crate) struct ManagedWorktreeRegistry {
200 leases: BTreeMap<ManagedWorktreeLeaseId, ManagedWorktreeLeaseRecord>,
201 tombstones: BTreeMap<ManagedWorktreeLeaseId, ManagedWorktreeLeaseRecord>,
202}
203
204impl ManagedWorktreeRegistry {
205 pub(crate) fn from_records(
206 records: Vec<ManagedWorktreeLeaseRecord>,
207 tombstones: Vec<ManagedWorktreeLeaseRecord>,
208 ) -> Result<Self, String> {
209 if records.len() > MAX_MANAGED_WORKTREE_LEASES
210 || tombstones.len() > MAX_MANAGED_WORKTREE_TOMBSTONES
211 {
212 return Err("managed worktree durable registry exceeds its bounded capacity".to_owned());
213 }
214 let mut registry = Self::default();
215 let mut workspace_ids = BTreeSet::new();
216 let mut target_roots = BTreeSet::new();
217 let mut branches = BTreeSet::new();
218 for record in records {
219 if record.state == ManagedWorktreeLeaseState::Removed
220 || !workspace_ids.insert(record.workspace_id.clone())
221 || !target_roots.insert(crate::platform::root_identity(&record.target_root))
222 || !branches.insert(record.branch.clone())
223 || registry.leases.insert(record.lease_id.clone(), record).is_some()
224 {
225 return Err("managed worktree durable registry is inconsistent".to_owned());
226 }
227 }
228 for record in tombstones {
229 if record.state != ManagedWorktreeLeaseState::Removed
230 || registry.leases.contains_key(&record.lease_id)
231 || registry.tombstones.insert(record.lease_id.clone(), record).is_some()
232 {
233 return Err("managed worktree tombstone registry is inconsistent".to_owned());
234 }
235 }
236 Ok(registry)
237 }
238
239 pub(crate) fn records(&self) -> Vec<ManagedWorktreeLeaseRecord> {
240 self.leases.values().cloned().collect()
241 }
242
243 pub(crate) fn tombstones(&self) -> Vec<ManagedWorktreeLeaseRecord> {
244 self.tombstones.values().cloned().collect()
245 }
246
247 pub(crate) fn snapshots(&self) -> Vec<ManagedWorktreeLeaseSnapshot> {
248 self.leases.values().map(ManagedWorktreeLeaseRecord::snapshot).collect()
249 }
250
251 pub(crate) fn get(&self, lease_id: &ManagedWorktreeLeaseId) -> Option<&ManagedWorktreeLeaseRecord> {
252 self.leases.get(lease_id).or_else(|| self.tombstones.get(lease_id))
253 }
254
255 pub(crate) fn get_mut(
256 &mut self,
257 lease_id: &ManagedWorktreeLeaseId,
258 ) -> Option<&mut ManagedWorktreeLeaseRecord> {
259 self.leases.get_mut(lease_id)
260 }
261
262 pub(crate) fn active_records(&self) -> impl Iterator<Item = &ManagedWorktreeLeaseRecord> {
263 self.leases.values()
264 }
265
266 pub(crate) fn git_scope(
267 &self,
268 workspace_id: &WorkspaceId,
269 branch: Option<&str>,
270 ) -> Option<ManagedWorktreeGitScope> {
271 let lease = self.leases.values().find(|lease| {
272 &lease.workspace_id == workspace_id && branch == Some(lease.branch.as_str())
273 })?;
274 if lease.source_workspace_id == *workspace_id || !lease.has_holders() {
275 return None;
276 }
277 Some(ManagedWorktreeGitScope {
278 lease_id: lease.lease_id.clone(),
279 source_workspace_id: lease.source_workspace_id.clone(),
280 branch: lease.branch.clone(),
281 base_commit: GitObjectId::new(lease.base_commit.clone()).ok()?,
282 active_session_count: u16::try_from(lease.session_holders.len()).unwrap_or(u16::MAX),
283 managed_record_count: u16::try_from(lease.record_holders.len()).unwrap_or(u16::MAX),
284 })
285 }
286
287 pub(crate) fn allocate(
288 &mut self,
289 source_workspace_id: WorkspaceId,
290 profile: &ManagedWorktreeProfile,
291 base_commit: String,
292 now: u64,
293 ) -> Result<ManagedWorktreeLeaseRecord, String> {
294 if self.leases.len() >= MAX_MANAGED_WORKTREE_LEASES {
295 return Err("managed worktree lease capacity is full".to_owned());
296 }
297 for _ in 0..8 {
298 let nonce = random_nonce()
299 .map_err(|error| format!("managed worktree identity generation failed: {error}"))?;
300 let suffix = hex_suffix(&nonce[..12]);
301 let lease_id = ManagedWorktreeLeaseId::new(format!("mw-{suffix}"))
302 .map_err(|error| error.to_string())?;
303 if self.leases.contains_key(&lease_id) || self.tombstones.contains_key(&lease_id) {
304 continue;
305 }
306 let workspace_id = WorkspaceId::new(format!("managed-{suffix}"))
307 .map_err(|error| error.to_string())?;
308 let target = PathBuf::from(profile.allocation_root()).join(lease_id.as_str());
309 let target_root = target.into_os_string().into_string().map_err(|path| {
310 format!("managed worktree target is not valid Unicode: {}", path.to_string_lossy())
311 })?;
312 let branch = format!("{}/{}", profile.branch_prefix(), lease_id.as_str());
313 let record = ManagedWorktreeLeaseRecord {
314 lease_id: lease_id.clone(),
315 source_workspace_id,
316 workspace_id,
317 profile_id: profile.profile_id().clone(),
318 profile_revision: profile.revision().clone(),
319 target_root,
320 branch,
321 base_commit,
322 expected_head: None,
323 retention: profile.retention(),
324 state: ManagedWorktreeLeaseState::Allocating,
325 session_holders: Vec::new(),
326 record_holders: Vec::new(),
327 cleanup_failure: None,
328 created_at_unix_ms: now,
329 updated_at_unix_ms: now,
330 };
331 self.leases.insert(lease_id, record.clone());
332 return Ok(record);
333 }
334 Err("could not allocate a unique managed worktree identity".to_owned())
335 }
336
337 pub(crate) fn remove_unmutated(&mut self, lease_id: &ManagedWorktreeLeaseId) {
338 self.leases.remove(lease_id);
339 }
340
341 pub(crate) fn tombstone(
342 &mut self,
343 lease_id: &ManagedWorktreeLeaseId,
344 now: u64,
345 ) -> Option<ManagedWorktreeLeaseRecord> {
346 let mut record = self.leases.remove(lease_id)?;
347 record.state = ManagedWorktreeLeaseState::Removed;
348 record.session_holders.clear();
349 record.record_holders.clear();
350 record.cleanup_failure = None;
351 record.updated_at_unix_ms = now;
352 while self.tombstones.len() >= MAX_MANAGED_WORKTREE_TOMBSTONES {
353 let oldest = self
354 .tombstones
355 .values()
356 .min_by_key(|item| (item.updated_at_unix_ms, item.lease_id.as_str()))
357 .map(|item| item.lease_id.clone());
358 if let Some(oldest) = oldest {
359 self.tombstones.remove(&oldest);
360 } else {
361 break;
362 }
363 }
364 self.tombstones.insert(record.lease_id.clone(), record.clone());
365 Some(record)
366 }
367
368 pub(crate) fn clear_stale_session_holders(&mut self, incarnation: NodeIncarnationId, now: u64) {
369 for lease in self.leases.values_mut() {
370 let previous = lease.session_holders.len();
371 lease.session_holders.retain(|holder| holder.incarnation_id == incarnation);
372 if previous != lease.session_holders.len() {
373 lease.updated_at_unix_ms = now;
374 }
375 }
376 }
377
378 pub(crate) fn bind_session(
379 &mut self,
380 lease_id: &ManagedWorktreeLeaseId,
381 holder: ManagedWorktreeSessionHolder,
382 record_id: Option<SessionRecordId>,
383 now: u64,
384 ) -> Result<ManagedWorktreeLeaseSnapshot, String> {
385 let lease = self.leases.get_mut(lease_id)
386 .ok_or_else(|| "managed worktree lease does not exist".to_owned())?;
387 if !lease.session_holders.contains(&holder) {
388 lease.session_holders.push(holder);
389 }
390 if let Some(record_id) = record_id {
391 if !lease.record_holders.contains(&record_id) {
392 lease.record_holders.push(record_id);
393 }
394 }
395 lease.state = ManagedWorktreeLeaseState::InUse;
396 lease.cleanup_failure = None;
397 lease.updated_at_unix_ms = now;
398 Ok(lease.snapshot())
399 }
400
401 pub(crate) fn release_session(
402 &mut self,
403 lease_id: &ManagedWorktreeLeaseId,
404 incarnation_id: NodeIncarnationId,
405 instance_id: AgentInstanceId,
406 generation: SessionGeneration,
407 now: u64,
408 ) -> Option<ManagedWorktreeLeaseSnapshot> {
409 let lease = self.leases.get_mut(lease_id)?;
410 lease.session_holders.retain(|holder| {
411 holder.incarnation_id != incarnation_id
412 || holder.instance_id != instance_id
413 || holder.generation != generation
414 });
415 if !lease.has_holders() && lease.state == ManagedWorktreeLeaseState::InUse {
416 lease.state = if lease.retention == ManagedWorktreeRetention::Retain {
417 ManagedWorktreeLeaseState::Retained
418 } else {
419 ManagedWorktreeLeaseState::Ready
420 };
421 }
422 lease.updated_at_unix_ms = now;
423 Some(lease.snapshot())
424 }
425
426 pub(crate) fn lease_for_workspace(&self, workspace_id: &WorkspaceId) -> Option<ManagedWorktreeLeaseId> {
427 self.leases.values().find(|lease| &lease.workspace_id == workspace_id)
428 .map(|lease| lease.lease_id.clone())
429 }
430
431 pub(crate) fn reattach_record_holders(&mut self, record_ids: &[(SessionRecordId, WorkspaceId)], now: u64) {
432 for lease in self.leases.values_mut() {
433 let next_holders = record_ids.iter()
434 .filter(|(_, workspace_id)| workspace_id == &lease.workspace_id)
435 .map(|(record_id, _)| record_id.clone())
436 .collect::<Vec<_>>();
437 let previous_holders = lease.record_holders.clone();
438 let previous_state = lease.state;
439 lease.record_holders = next_holders;
440 if !matches!(
441 lease.state,
442 ManagedWorktreeLeaseState::RecoveryRequired
443 | ManagedWorktreeLeaseState::CleanupBlocked
444 | ManagedWorktreeLeaseState::Removed
445 | ManagedWorktreeLeaseState::Allocating
446 ) {
447 if lease.has_holders() {
448 lease.state = ManagedWorktreeLeaseState::InUse;
449 } else if previous_state == ManagedWorktreeLeaseState::InUse {
450 lease.state = if lease.retention == ManagedWorktreeRetention::Retain {
451 ManagedWorktreeLeaseState::Retained
452 } else {
453 ManagedWorktreeLeaseState::Ready
454 };
455 }
456 }
457 if lease.state != previous_state || lease.record_holders != previous_holders {
458 lease.updated_at_unix_ms = now;
459 }
460 }
461 }
462}
463
464pub(crate) fn exact_owned_worktree(
465 lease: &ManagedWorktreeLeaseRecord,
466 path: &str,
467 branch: Option<&str>,
468 _head: &str,
469) -> bool {
470 paths_equal(&lease.target_root, path)
471 && branch == Some(lease.branch.as_str())
472}
473
474pub(crate) fn exact_created_worktree(
475 lease: &ManagedWorktreeLeaseRecord,
476 path: &str,
477 branch: Option<&str>,
478 head: &str,
479) -> bool {
480 exact_owned_worktree(lease, path, branch, head)
481 && lease.expected_head.as_deref().unwrap_or(lease.base_commit.as_str()) == head
482}
483
484fn hex_suffix(bytes: &[u8]) -> String {
485 let mut value = String::with_capacity(bytes.len() * 2);
486 for byte in bytes {
487 use std::fmt::Write as _;
488 let _ = write!(&mut value, "{byte:02x}");
489 }
490 value
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use std::sync::atomic::{AtomicU64, Ordering};
497
498 static NEXT_TEMP: AtomicU64 = AtomicU64::new(1);
499
500 fn temporary_root(label: &str) -> PathBuf {
501 let sequence = NEXT_TEMP.fetch_add(1, Ordering::Relaxed);
502 std::env::temp_dir().join(format!(
503 "gate4agent-managed-worktree-{label}-{}-{sequence}",
504 std::process::id(),
505 ))
506 }
507
508 fn lease(
509 id: &str,
510 workspace: &str,
511 target: &str,
512 branch: &str,
513 ) -> ManagedWorktreeLeaseRecord {
514 ManagedWorktreeLeaseRecord {
515 lease_id: ManagedWorktreeLeaseId::new(id).unwrap(),
516 source_workspace_id: WorkspaceId::new("source").unwrap(),
517 workspace_id: WorkspaceId::new(workspace).unwrap(),
518 profile_id: WorktreeProfileId::new("default").unwrap(),
519 profile_revision: WorktreeProfileRevision::new("v1").unwrap(),
520 target_root: target.to_owned(),
521 branch: branch.to_owned(),
522 base_commit: "0123456789abcdef0123456789abcdef01234567".to_owned(),
523 expected_head: Some("0123456789abcdef0123456789abcdef01234567".to_owned()),
524 retention: ManagedWorktreeRetention::RemoveWhenReleased,
525 state: ManagedWorktreeLeaseState::Ready,
526 session_holders: Vec::new(),
527 record_holders: Vec::new(),
528 cleanup_failure: None,
529 created_at_unix_ms: 1,
530 updated_at_unix_ms: 1,
531 }
532 }
533
534 #[test]
535 fn managed_profile_recanonicalizes_root_and_requires_exact_lease_child() {
536 let parent = temporary_root("profile");
537 let source = parent.join("source");
538 let allocation = parent.join("allocation");
539 std::fs::create_dir_all(&source).unwrap();
540 std::fs::create_dir_all(&allocation).unwrap();
541 let profile = ManagedWorktreeProfile::new(
542 WorktreeProfileId::new("default").unwrap(),
543 WorktreeProfileRevision::new("v1").unwrap(),
544 &allocation,
545 "gate4agent",
546 "HEAD",
547 ManagedWorktreeRetention::RemoveWhenReleased,
548 ).unwrap();
549 profile.validate_for_workspace(source.to_str().unwrap()).unwrap();
550 let lease_id = ManagedWorktreeLeaseId::new("mw-abc").unwrap();
551 let exact = PathBuf::from(profile.allocation_root()).join(lease_id.as_str());
552 profile.validate_target_authority(&lease_id, exact.to_str().unwrap()).unwrap();
553 let traversing = PathBuf::from(profile.allocation_root())
554 .join("ignored").join("..").join(lease_id.as_str());
555 assert!(profile.validate_target_authority(&lease_id, traversing.to_str().unwrap()).is_err());
556 std::fs::remove_dir_all(parent).unwrap();
557 }
558
559 #[test]
560 fn registry_rejects_duplicate_workspace_root_and_branch_authority() {
561 let first = lease("mw-a", "managed-a", "C:/trees/a", "gate4agent/a");
562 let mut duplicate_workspace = lease("mw-b", "managed-a", "C:/trees/b", "gate4agent/b");
563 assert!(ManagedWorktreeRegistry::from_records(
564 vec![first.clone(), duplicate_workspace.clone()], Vec::new(),
565 ).is_err());
566 duplicate_workspace.workspace_id = WorkspaceId::new("managed-b").unwrap();
567 duplicate_workspace.target_root = first.target_root.clone();
568 assert!(ManagedWorktreeRegistry::from_records(
569 vec![first.clone(), duplicate_workspace.clone()], Vec::new(),
570 ).is_err());
571 duplicate_workspace.target_root = "C:/trees/b".to_owned();
572 duplicate_workspace.branch = first.branch.clone();
573 assert!(ManagedWorktreeRegistry::from_records(
574 vec![first, duplicate_workspace], Vec::new(),
575 ).is_err());
576 }
577
578 #[test]
579 fn exact_current_incarnation_holder_release_transitions_without_head_fence() {
580 let mut registry = ManagedWorktreeRegistry::from_records(
581 vec![lease("mw-a", "managed-a", "C:/trees/a", "gate4agent/a")],
582 Vec::new(),
583 ).unwrap();
584 let lease_id = ManagedWorktreeLeaseId::new("mw-a").unwrap();
585 let current = NodeIncarnationId::from_bytes([1; crate::protocol::NODE_INCARNATION_ID_BYTES]);
586 registry.bind_session(
587 &lease_id,
588 ManagedWorktreeSessionHolder {
589 incarnation_id: current,
590 instance_id: AgentInstanceId(7),
591 generation: SessionGeneration(2),
592 },
593 None,
594 2,
595 ).unwrap();
596 registry.release_session(
597 &lease_id,
598 NodeIncarnationId::from_bytes([2; crate::protocol::NODE_INCARNATION_ID_BYTES]),
599 AgentInstanceId(7),
600 SessionGeneration(2),
601 3,
602 );
603 assert_eq!(registry.get(&lease_id).unwrap().snapshot().active_session_count, 1);
604 registry.release_session(&lease_id, current, AgentInstanceId(7), SessionGeneration(2), 4);
605 assert_eq!(registry.get(&lease_id).unwrap().state, ManagedWorktreeLeaseState::Ready);
606 let record_id = SessionRecordId::new("sr-a").unwrap();
607 registry.reattach_record_holders(
608 &[(record_id, WorkspaceId::new("managed-a").unwrap())],
609 5,
610 );
611 assert_eq!(registry.get(&lease_id).unwrap().state, ManagedWorktreeLeaseState::InUse);
612 assert_eq!(registry.get(&lease_id).unwrap().updated_at_unix_ms, 5);
613 registry.reattach_record_holders(
614 &[(SessionRecordId::new("sr-a").unwrap(), WorkspaceId::new("managed-a").unwrap())],
615 9,
616 );
617 assert_eq!(registry.get(&lease_id).unwrap().updated_at_unix_ms, 5);
618 let record = registry.get(&lease_id).unwrap();
619 assert!(exact_owned_worktree(record, "C:/trees/a", Some("gate4agent/a"), "new-head"));
620 assert!(!exact_created_worktree(record, "C:/trees/a", Some("gate4agent/a"), "new-head"));
621 }
622
623 #[test]
624 fn managed_git_scope_requires_exact_active_workspace_and_branch() {
625 let mut record = lease("mw-a", "managed-a", "C:/trees/a", "gate4agent/a");
626 record.session_holders.push(ManagedWorktreeSessionHolder {
627 incarnation_id: NodeIncarnationId::from_bytes([
628 1;
629 crate::protocol::NODE_INCARNATION_ID_BYTES
630 ]),
631 instance_id: AgentInstanceId(7),
632 generation: SessionGeneration(2),
633 });
634 record.record_holders.push(SessionRecordId::new("sr-a").unwrap());
635 let registry = ManagedWorktreeRegistry::from_records(vec![record], Vec::new()).unwrap();
636 let workspace_id = WorkspaceId::new("managed-a").unwrap();
637 let scope = registry
638 .git_scope(&workspace_id, Some("gate4agent/a"))
639 .unwrap();
640 assert_eq!(scope.lease_id.as_str(), "mw-a");
641 assert_eq!(scope.source_workspace_id.as_str(), "source");
642 assert_eq!(scope.branch, "gate4agent/a");
643 assert_eq!(scope.active_session_count, 1);
644 assert_eq!(scope.managed_record_count, 1);
645 assert!(registry.git_scope(&workspace_id, Some("gate4agent/b")).is_none());
646 assert!(registry
647 .git_scope(&WorkspaceId::new("managed-b").unwrap(), Some("gate4agent/a"))
648 .is_none());
649 }
650}