Skip to main content

acyclic_fs/
operation_window.rs

1//! Durable operation windows for stable mounted-tool execution.
2//!
3//! A window is a short-lived coordination scope, not a filesystem lock. Any
4//! number of tools may read and write the same mounted workspace while leases
5//! are active. Parent advances are coalesced and reconciled exactly once after
6//! the last lease closes. Durable stores make crash recovery and writer fencing
7//! independent of the process that opened the window.
8
9use crate::{
10    AsyncAuthorityStore, AsyncObjectStore, GenerationId, IdempotencyKey, OperationId, Workspace,
11    WorkspaceError, WorkspaceId, WorkspaceRebase,
12};
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15use std::future::Future;
16use std::sync::{Arc, Mutex};
17use thiserror::Error;
18
19use futures::StreamExt as _;
20
21const STATE_VERSION: u32 = 1;
22const MAXIMUM_CAS_ATTEMPTS: u8 = 32;
23
24/// Stable identity of one tool lease.
25#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
26#[serde(transparent)]
27pub struct OperationLeaseId(OperationId);
28
29impl OperationLeaseId {
30    /// Creates a fresh time-ordered lease identity.
31    #[must_use]
32    pub fn new() -> Self {
33        Self(OperationId::new())
34    }
35
36    /// Restores a lease identity from its canonical bytes.
37    #[must_use]
38    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
39        Self(OperationId::from_bytes(bytes))
40    }
41
42    /// Returns the canonical bytes.
43    #[must_use]
44    pub const fn into_bytes(self) -> [u8; 16] {
45        self.0.into_bytes()
46    }
47}
48
49impl Default for OperationLeaseId {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55/// One active durable tool lease.
56#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
57pub struct OperationLease {
58    /// Stable lease identity.
59    pub id: OperationLeaseId,
60    /// Adapter-defined owner identity, normally a Codex agent and tool call.
61    pub owner: String,
62    /// Absolute Unix epoch expiry in milliseconds.
63    pub expires_at_millis: u64,
64}
65
66/// Durable window phase.
67#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
68pub enum OperationWindowPhase {
69    /// No tool currently pins the mounted view.
70    Idle,
71    /// One or more tools share a pinned view.
72    Active {
73        /// Parent generation visible when the first lease opened.
74        pinned_parent: GenerationId,
75        /// Active leases keyed by stable identity.
76        leases: BTreeMap<OperationLeaseId, OperationLease>,
77        /// Newest parent generation observed while the view was pinned.
78        pending_parent: Option<GenerationId>,
79    },
80    /// The final lease closed and one owner must complete reconciliation.
81    Reconciling {
82        /// Stable retry identity for reconciliation.
83        ticket: OperationId,
84        /// Parent generation at window entry.
85        pinned_parent: GenerationId,
86        /// Newest coalesced parent, if the parent advanced.
87        pending_parent: Option<GenerationId>,
88        /// Parent observed after this reconciliation ticket was claimed.
89        #[serde(default)]
90        subsequent_parent: Option<GenerationId>,
91    },
92}
93
94/// Complete versioned durable state for one workspace.
95#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
96pub struct OperationWindowSnapshot {
97    /// Serialization contract version.
98    pub version: u32,
99    /// Monotonic optimistic-concurrency revision.
100    pub revision: u64,
101    /// Workspace governed by this state.
102    pub workspace_id: WorkspaceId,
103    /// Current coordination phase.
104    pub phase: OperationWindowPhase,
105}
106
107impl OperationWindowSnapshot {
108    fn idle(workspace_id: WorkspaceId) -> Self {
109        Self {
110            version: STATE_VERSION,
111            revision: 0,
112            workspace_id,
113            phase: OperationWindowPhase::Idle,
114        }
115    }
116}
117
118fn snapshot_shape_is_valid(snapshot: &OperationWindowSnapshot) -> bool {
119    match &snapshot.phase {
120        OperationWindowPhase::Idle => true,
121        OperationWindowPhase::Reconciling { ticket, .. } => ticket.into_bytes() != [0; 16],
122        OperationWindowPhase::Active { leases, .. } => {
123            !leases.is_empty()
124                && leases.iter().all(|(lease_id, lease)| {
125                    *lease_id == lease.id
126                        && lease_id.into_bytes() != [0; 16]
127                        && !lease.owner.trim().is_empty()
128                        && lease.expires_at_millis > 0
129                })
130        }
131    }
132}
133
134/// Lease returned to one filesystem-touching tool.
135#[derive(Clone, Debug, Eq, PartialEq)]
136pub struct OperationWindowLease {
137    /// Workspace containing the shared mount.
138    pub workspace_id: WorkspaceId,
139    /// Stable lease identity.
140    pub lease_id: OperationLeaseId,
141    /// Parent generation pinned by the shared window.
142    pub pinned_parent: GenerationId,
143    /// Lease expiry copied from durable state.
144    pub expires_at_millis: u64,
145}
146
147impl OperationWindowLease {
148    /// Returns the authority permit that atomically admits publications made
149    /// by this exact still-active lease.
150    #[must_use]
151    pub const fn publication_permit(&self) -> crate::PublicationPermit {
152        crate::PublicationPermit::Lease {
153            authority_id: crate::kernel::volume_authority_id(self.workspace_id.volume_id())
154                .into_bytes(),
155            workspace_id: self.workspace_id.into_bytes(),
156            lease_id: self.lease_id.into_bytes(),
157            expires_at_millis: self.expires_at_millis,
158        }
159    }
160}
161
162/// Reconciliation work claimed after a final close or expiry sweep.
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub struct OperationWindowReconcile {
165    /// Stable retry identity.
166    pub ticket: OperationId,
167    /// Parent generation visible when the window opened.
168    pub pinned_parent: GenerationId,
169    /// Newest coalesced parent generation.
170    pub pending_parent: Option<GenerationId>,
171}
172
173/// Result of closing or recovering a lease.
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
175pub enum OperationWindowFinish {
176    /// Other live leases still pin the view.
177    StillActive {
178        /// Number of unexpired leases that remain.
179        remaining: u32,
180    },
181    /// The caller owns the one reconciliation attempt.
182    Reconcile(OperationWindowReconcile),
183    /// The lease was already closed or expired.
184    AlreadyClosed,
185}
186
187/// Bounds applied when the last tool lease reconciles a fork with its parent.
188#[derive(Clone, Copy, Debug, Eq, PartialEq)]
189pub struct OperationReconcileLimits {
190    /// Maximum immutable generations inspected while finding ancestry.
191    pub maximum_generations: u32,
192    /// Maximum semantic changes retained during planning.
193    pub maximum_changes: u32,
194    /// Maximum conflicts returned to the caller.
195    pub maximum_conflicts: u32,
196}
197
198impl Default for OperationReconcileLimits {
199    fn default() -> Self {
200        Self {
201            maximum_generations: 4_096,
202            maximum_changes: 65_536,
203            maximum_conflicts: 1_024,
204        }
205    }
206}
207
208/// Workspace-aware result of closing one tool lease.
209pub enum WorkspaceOperationFinish<A, O> {
210    /// Other live leases still pin the shared mount.
211    StillActive {
212        /// Number of unexpired leases that remain.
213        remaining: u32,
214    },
215    /// The lease was already closed or expired.
216    AlreadyClosed,
217    /// The final close ran the core live-rebase operation.
218    Reconciled(WorkspaceRebase<A, O>),
219}
220
221/// Durable optimistic-concurrency adapter for operation-window state.
222pub trait OperationWindowStore: Send + Sync {
223    /// Adapter error.
224    type Error: std::error::Error + Send + Sync + 'static;
225
226    /// Loads current state, returning `None` before first use.
227    fn load(
228        &self,
229        workspace_id: WorkspaceId,
230    ) -> impl Future<Output = Result<Option<OperationWindowSnapshot>, Self::Error>> + Send;
231
232    /// Replaces `expected_revision` atomically. Revision zero creates state.
233    fn compare_and_swap(
234        &self,
235        workspace_id: WorkspaceId,
236        expected_revision: u64,
237        replacement: OperationWindowSnapshot,
238    ) -> impl Future<Output = Result<bool, Self::Error>> + Send;
239}
240
241/// Operation-window state stored beside generation authority in one Stream
242/// provider. Each lease owns an immutable gate stream: opening creates tail 1,
243/// while close/expiry atomically advances it to tail 2 with the window CAS.
244/// A generation publication conditioned on tail 1 therefore linearizes
245/// exactly against fencing.
246#[derive(Clone)]
247pub struct StreamOperationWindowStore<P> {
248    provider: Arc<P>,
249}
250
251impl<P> StreamOperationWindowStore<P> {
252    /// Binds operation windows to the exact provider used by filesystem
253    /// authority publication.
254    #[must_use]
255    pub fn new(provider: Arc<P>) -> Self {
256        Self { provider }
257    }
258}
259
260/// Stream-backed operation-window failure.
261#[derive(Debug, Error)]
262pub enum StreamOperationWindowStoreError {
263    /// The shared Stream provider rejected or could not durably resolve state.
264    #[error("operation-window stream failed: {0}")]
265    Stream(#[from] acyclic_stream::StreamError),
266    /// Stored state is corrupt or incompatible.
267    #[error("operation-window stream state is corrupt: {0}")]
268    Corrupt(String),
269}
270
271impl<P: acyclic_stream::StreamProvider> OperationWindowStore for StreamOperationWindowStore<P> {
272    type Error = StreamOperationWindowStoreError;
273
274    async fn load(
275        &self,
276        workspace_id: WorkspaceId,
277    ) -> Result<Option<OperationWindowSnapshot>, Self::Error> {
278        let path = stream_window_path(workspace_id)?;
279        let tail = match self.provider.tail(path.clone()).await {
280            Ok(tail) => tail,
281            Err(acyclic_stream::StreamError::NotFound) => return Ok(None),
282            Err(error) => return Err(error.into()),
283        };
284        if tail == 0 {
285            return Err(StreamOperationWindowStoreError::Corrupt(
286                "window stream is empty".to_owned(),
287            ));
288        }
289        let mut records = self
290            .provider
291            .read(acyclic_stream::ReadRequest {
292                path,
293                from: tail - 1,
294                limit: 1,
295            })
296            .await?;
297        let record = records.next().await.transpose()?.ok_or_else(|| {
298            StreamOperationWindowStoreError::Corrupt("window stream tail has no record".to_owned())
299        })?;
300        let snapshot: OperationWindowSnapshot = serde_json::from_slice(&record.value)
301            .map_err(|error| StreamOperationWindowStoreError::Corrupt(error.to_string()))?;
302        if snapshot.revision != tail {
303            return Err(StreamOperationWindowStoreError::Corrupt(
304                "window revision does not match its stream tail".to_owned(),
305            ));
306        }
307        if !snapshot_shape_is_valid(&snapshot) {
308            return Err(StreamOperationWindowStoreError::Corrupt(
309                "window phase shape is invalid".to_owned(),
310            ));
311        }
312        Ok(Some(snapshot))
313    }
314
315    async fn compare_and_swap(
316        &self,
317        workspace_id: WorkspaceId,
318        expected_revision: u64,
319        replacement: OperationWindowSnapshot,
320    ) -> Result<bool, Self::Error> {
321        if replacement.workspace_id != workspace_id
322            || replacement.revision != expected_revision.saturating_add(1)
323            || !snapshot_shape_is_valid(&replacement)
324        {
325            return Err(StreamOperationWindowStoreError::Corrupt(
326                "replacement identity or revision is invalid".to_owned(),
327            ));
328        }
329        let current = self.load(workspace_id).await?;
330        if current.as_ref().map_or(0, |snapshot| snapshot.revision) != expected_revision {
331            return Ok(false);
332        }
333        let before = current
334            .as_ref()
335            .map_or_else(BTreeMap::new, |snapshot| phase_leases(&snapshot.phase));
336        let after = phase_leases(&replacement.phase);
337        let state_path = stream_window_path(workspace_id)?;
338        let mut conditions = vec![if expected_revision == 0 {
339            acyclic_stream::CommitCondition::Absent {
340                path: state_path.clone(),
341            }
342        } else {
343            acyclic_stream::CommitCondition::Tail {
344                path: state_path.clone(),
345                expected: expected_revision,
346            }
347        }];
348        let encoded = serde_json::to_vec(&replacement)
349            .map_err(|error| StreamOperationWindowStoreError::Corrupt(error.to_string()))?;
350        let mut mutations = vec![acyclic_stream::CommitMutation::Append {
351            path: state_path,
352            records: vec![encoded.into()],
353        }];
354        for lease_id in after
355            .keys()
356            .filter(|lease_id| !before.contains_key(lease_id))
357        {
358            let path = stream_lease_path(workspace_id, *lease_id)?;
359            conditions.push(acyclic_stream::CommitCondition::Absent { path: path.clone() });
360            mutations.push(acyclic_stream::CommitMutation::Append {
361                path,
362                records: vec![bytes::Bytes::from_static(b"active")],
363            });
364        }
365        for lease_id in before
366            .keys()
367            .filter(|lease_id| !after.contains_key(lease_id))
368        {
369            let path = stream_lease_path(workspace_id, *lease_id)?;
370            conditions.push(acyclic_stream::CommitCondition::Tail {
371                path: path.clone(),
372                expected: 1,
373            });
374            mutations.push(acyclic_stream::CommitMutation::Append {
375                path,
376                records: vec![bytes::Bytes::from_static(b"fenced")],
377            });
378        }
379        let mut hasher = blake3::Hasher::new();
380        hasher.update(b"acyclic-operation-window-cas-v2\0");
381        hasher.update(&workspace_id.into_bytes());
382        hasher.update(&expected_revision.to_le_bytes());
383        hasher.update(
384            &serde_json::to_vec(&replacement)
385                .map_err(|error| StreamOperationWindowStoreError::Corrupt(error.to_string()))?,
386        );
387        let key = acyclic_stream::IdempotencyKey::new(hasher.finalize().as_bytes().to_vec())?;
388        match self
389            .provider
390            .commit(acyclic_stream::CommitRequest {
391                conditions,
392                mutations,
393                idempotency_key: key,
394            })
395            .await?
396        {
397            acyclic_stream::CommitOutcome::Committed(_) => Ok(true),
398            acyclic_stream::CommitOutcome::Conflict(_) => Ok(false),
399        }
400    }
401}
402
403fn phase_leases(phase: &OperationWindowPhase) -> BTreeMap<OperationLeaseId, OperationLease> {
404    match phase {
405        OperationWindowPhase::Active { leases, .. } => leases.clone(),
406        OperationWindowPhase::Idle | OperationWindowPhase::Reconciling { .. } => BTreeMap::new(),
407    }
408}
409
410fn stream_window_path(
411    workspace_id: WorkspaceId,
412) -> Result<acyclic_stream::StreamPath, acyclic_stream::StreamError> {
413    acyclic_stream::StreamPath::new(format!(
414        "fs/operation-windows-v2/{}/state",
415        hex::encode(workspace_id.into_bytes())
416    ))
417}
418
419pub(crate) fn stream_lease_path(
420    workspace_id: WorkspaceId,
421    lease_id: OperationLeaseId,
422) -> Result<acyclic_stream::StreamPath, acyclic_stream::StreamError> {
423    acyclic_stream::StreamPath::new(format!(
424        "fs/operation-windows-v2/{}/leases/{}",
425        hex::encode(workspace_id.into_bytes()),
426        hex::encode(lease_id.into_bytes())
427    ))
428}
429
430/// Operation-window failure.
431#[derive(Debug, Error)]
432pub enum OperationWindowError<E: std::error::Error + 'static> {
433    /// Durable adapter failed.
434    #[error("operation-window store failed: {0}")]
435    Store(E),
436    /// Persisted state uses an unsupported version or identity.
437    #[error("operation-window state is incompatible")]
438    IncompatibleState,
439    /// A reconciliation owner already fences new windows.
440    #[error("workspace operation window is reconciling")]
441    Reconciling,
442    /// The supplied lease lifetime is empty or already expired.
443    #[error("operation lease must expire after the current time")]
444    InvalidExpiry,
445    /// A deterministic lease identity was already bound to different inputs.
446    #[error("operation lease identity is already bound to another owner or expiry")]
447    LeaseIdentityConflict,
448    /// The supplied lease was closed, expired, or superseded by a renewal.
449    #[error("operation lease is no longer active")]
450    StaleLease,
451    /// Optimistic-concurrency contention exceeded the bounded retry policy.
452    #[error("operation-window state remained contended")]
453    Contended,
454    /// Reconciliation completion used a stale ticket.
455    #[error("operation-window reconciliation ticket is stale")]
456    StaleTicket,
457    /// The core workspace reconciliation failed.
458    #[error("workspace reconciliation failed: {0}")]
459    Workspace(#[source] WorkspaceError),
460}
461
462/// Core coordinator for overlapping tool leases and deferred parent advances.
463pub struct OperationWindowCoordinator<S> {
464    store: S,
465}
466
467impl<S> OperationWindowCoordinator<S> {
468    /// Creates a coordinator over a durable state adapter.
469    #[must_use]
470    pub const fn new(store: S) -> Self {
471        Self { store }
472    }
473
474    /// Borrows the underlying adapter.
475    #[must_use]
476    pub const fn store(&self) -> &S {
477        &self.store
478    }
479}
480
481impl<S: OperationWindowStore> OperationWindowCoordinator<S> {
482    /// Opens an overlapping lease. The first lease pins `parent`.
483    pub async fn begin(
484        &self,
485        workspace_id: WorkspaceId,
486        parent: GenerationId,
487        owner: impl Into<String>,
488        now_millis: u64,
489        expires_at_millis: u64,
490    ) -> Result<OperationWindowLease, OperationWindowError<S::Error>> {
491        self.begin_with_lease_id(
492            workspace_id,
493            parent,
494            owner,
495            now_millis,
496            expires_at_millis,
497            OperationLeaseId::new(),
498        )
499        .await
500    }
501
502    /// Opens a lease with a caller-supplied deterministic identity.
503    pub async fn begin_with_lease_id(
504        &self,
505        workspace_id: WorkspaceId,
506        parent: GenerationId,
507        owner: impl Into<String>,
508        now_millis: u64,
509        expires_at_millis: u64,
510        lease_id: OperationLeaseId,
511    ) -> Result<OperationWindowLease, OperationWindowError<S::Error>> {
512        if expires_at_millis <= now_millis {
513            return Err(OperationWindowError::InvalidExpiry);
514        }
515        let owner = owner.into();
516        if owner.trim().is_empty() || lease_id.into_bytes() == [0; 16] {
517            return Err(OperationWindowError::LeaseIdentityConflict);
518        }
519        for _ in 0..MAXIMUM_CAS_ATTEMPTS {
520            let mut current = self.snapshot(workspace_id).await?;
521            if expire_to_reconcile(
522                &mut current.phase,
523                now_millis,
524                OperationId::from_bytes(lease_id.into_bytes()),
525            )
526            .is_some()
527            {
528                let expected = current.revision;
529                current.revision = expected.saturating_add(1);
530                if self.cas(workspace_id, expected, current).await? {
531                    return Err(OperationWindowError::Reconciling);
532                }
533                continue;
534            }
535            let pinned_parent = match &mut current.phase {
536                OperationWindowPhase::Idle => {
537                    let mut leases = BTreeMap::new();
538                    leases.insert(
539                        lease_id,
540                        OperationLease {
541                            id: lease_id,
542                            owner: owner.clone(),
543                            expires_at_millis,
544                        },
545                    );
546                    current.phase = OperationWindowPhase::Active {
547                        pinned_parent: parent,
548                        leases,
549                        pending_parent: None,
550                    };
551                    parent
552                }
553                OperationWindowPhase::Active {
554                    pinned_parent,
555                    leases,
556                    ..
557                } => {
558                    if let Some(existing) = leases.get(&lease_id) {
559                        if existing.owner == owner
560                            && existing.expires_at_millis == expires_at_millis
561                        {
562                            return Ok(OperationWindowLease {
563                                workspace_id,
564                                lease_id,
565                                pinned_parent: *pinned_parent,
566                                expires_at_millis,
567                            });
568                        }
569                        return Err(OperationWindowError::LeaseIdentityConflict);
570                    }
571                    leases.insert(
572                        lease_id,
573                        OperationLease {
574                            id: lease_id,
575                            owner: owner.clone(),
576                            expires_at_millis,
577                        },
578                    );
579                    *pinned_parent
580                }
581                OperationWindowPhase::Reconciling { .. } => {
582                    return Err(OperationWindowError::Reconciling);
583                }
584            };
585            let expected = current.revision;
586            current.revision = expected.saturating_add(1);
587            if self.cas(workspace_id, expected, current).await? {
588                return Ok(OperationWindowLease {
589                    workspace_id,
590                    lease_id,
591                    pinned_parent,
592                    expires_at_millis,
593                });
594            }
595        }
596        Err(OperationWindowError::Contended)
597    }
598
599    /// Coalesces a newly authenticated parent generation without changing the mount.
600    pub async fn observe_parent(
601        &self,
602        workspace_id: WorkspaceId,
603        parent: GenerationId,
604    ) -> Result<bool, OperationWindowError<S::Error>> {
605        for _ in 0..MAXIMUM_CAS_ATTEMPTS {
606            let mut current = self.snapshot(workspace_id).await?;
607            match &mut current.phase {
608                OperationWindowPhase::Idle => return Ok(false),
609                OperationWindowPhase::Active { pending_parent, .. } => {
610                    *pending_parent = Some(parent);
611                }
612                OperationWindowPhase::Reconciling {
613                    subsequent_parent, ..
614                } => *subsequent_parent = Some(parent),
615            }
616            let expected = current.revision;
617            current.revision = expected.saturating_add(1);
618            if self.cas(workspace_id, expected, current).await? {
619                return Ok(true);
620            }
621        }
622        Err(OperationWindowError::Contended)
623    }
624
625    /// Reconciles an idle child after its parent advances, or coalesces the
626    /// advance into an active/reconciling operation window. An idle claim is
627    /// durable and fences a new tool window until reconciliation completes.
628    ///
629    /// This also covers a parent tool that finishes after the child's last
630    /// overlapping tool: the child need not open another tool to catch up.
631    pub async fn parent_advanced_workspace<A: AsyncAuthorityStore, O: AsyncObjectStore>(
632        &self,
633        workspace: &Workspace<A, O>,
634        parent: GenerationId,
635        limits: OperationReconcileLimits,
636    ) -> Result<Option<WorkspaceRebase<A, O>>, OperationWindowError<S::Error>> {
637        let workspace_id = workspace.id();
638        for _ in 0..MAXIMUM_CAS_ATTEMPTS {
639            let mut current = self.snapshot(workspace_id).await?;
640            let reconcile = match &mut current.phase {
641                OperationWindowPhase::Idle => {
642                    let ticket = OperationId::new();
643                    current.phase = OperationWindowPhase::Reconciling {
644                        ticket,
645                        pinned_parent: parent,
646                        pending_parent: Some(parent),
647                        subsequent_parent: None,
648                    };
649                    Some(OperationWindowReconcile {
650                        ticket,
651                        pinned_parent: parent,
652                        pending_parent: Some(parent),
653                    })
654                }
655                OperationWindowPhase::Active { pending_parent, .. } => {
656                    *pending_parent = Some(parent);
657                    None
658                }
659                OperationWindowPhase::Reconciling {
660                    subsequent_parent, ..
661                } => {
662                    *subsequent_parent = Some(parent);
663                    None
664                }
665            };
666            let expected = current.revision;
667            current.revision = expected.saturating_add(1);
668            if self.cas(workspace_id, expected, current).await? {
669                return match reconcile {
670                    Some(reconcile) => self
671                        .reconcile_workspace(workspace, reconcile, limits)
672                        .await
673                        .map(Some),
674                    None => Ok(None),
675                };
676            }
677        }
678        Err(OperationWindowError::Contended)
679    }
680
681    /// Extends one still-active exact lease without changing the pinned mount.
682    /// The returned lease replaces the caller's old publication permit; using
683    /// the old expiry after renewal is fenced by durable state.
684    pub async fn renew(
685        &self,
686        lease: &OperationWindowLease,
687        now_millis: u64,
688        expires_at_millis: u64,
689    ) -> Result<OperationWindowLease, OperationWindowError<S::Error>> {
690        if expires_at_millis <= now_millis || expires_at_millis < lease.expires_at_millis {
691            return Err(OperationWindowError::InvalidExpiry);
692        }
693        for _ in 0..MAXIMUM_CAS_ATTEMPTS {
694            let mut current = self.snapshot(lease.workspace_id).await?;
695            let OperationWindowPhase::Active {
696                pinned_parent,
697                leases,
698                ..
699            } = &mut current.phase
700            else {
701                return Err(OperationWindowError::StaleLease);
702            };
703            let Some(existing) = leases.get(&lease.lease_id) else {
704                return Err(OperationWindowError::StaleLease);
705            };
706            if existing.expires_at_millis != lease.expires_at_millis
707                || existing.expires_at_millis <= now_millis
708            {
709                return Err(OperationWindowError::StaleLease);
710            }
711            if existing.expires_at_millis == expires_at_millis {
712                return Ok(lease.clone());
713            }
714            let pinned_parent = *pinned_parent;
715            leases
716                .get_mut(&lease.lease_id)
717                .ok_or(OperationWindowError::StaleLease)?
718                .expires_at_millis = expires_at_millis;
719            let expected = current.revision;
720            current.revision = expected.saturating_add(1);
721            if self.cas(lease.workspace_id, expected, current).await? {
722                return Ok(OperationWindowLease {
723                    workspace_id: lease.workspace_id,
724                    lease_id: lease.lease_id,
725                    pinned_parent,
726                    expires_at_millis,
727                });
728            }
729        }
730        Err(OperationWindowError::Contended)
731    }
732
733    /// Closes one lease and atomically claims reconciliation after the last close.
734    pub async fn finish(
735        &self,
736        lease: &OperationWindowLease,
737        now_millis: u64,
738    ) -> Result<OperationWindowFinish, OperationWindowError<S::Error>> {
739        self.finish_with_ticket(lease, now_millis, OperationId::new())
740            .await
741    }
742
743    /// Closes a lease with a caller-supplied deterministic reconciliation ticket.
744    pub async fn finish_with_ticket(
745        &self,
746        lease: &OperationWindowLease,
747        now_millis: u64,
748        reconciliation_ticket: OperationId,
749    ) -> Result<OperationWindowFinish, OperationWindowError<S::Error>> {
750        for _ in 0..MAXIMUM_CAS_ATTEMPTS {
751            let mut current = self.snapshot(lease.workspace_id).await?;
752            let before = current.phase.clone();
753            let result = close_lease(
754                &mut current.phase,
755                lease.lease_id,
756                lease.expires_at_millis,
757                now_millis,
758                reconciliation_ticket,
759            );
760            if result.is_none() && current.phase == before {
761                return Ok(OperationWindowFinish::AlreadyClosed);
762            }
763            let expected = current.revision;
764            current.revision = expected.saturating_add(1);
765            if self.cas(lease.workspace_id, expected, current).await? {
766                return Ok(result.unwrap_or(OperationWindowFinish::AlreadyClosed));
767            }
768        }
769        Err(OperationWindowError::Contended)
770    }
771
772    /// Closes a lease and runs exactly one core live rebase after the last close.
773    ///
774    /// Successful, current, and conflicted outcomes are terminal and release
775    /// the window. Stale, fenced, and idempotency-conflicted outcomes retain the
776    /// durable reconciliation ticket so recovery can retry or inspect it.
777    pub async fn finish_workspace<A: AsyncAuthorityStore, O: AsyncObjectStore>(
778        &self,
779        workspace: &Workspace<A, O>,
780        lease: &OperationWindowLease,
781        now_millis: u64,
782        limits: OperationReconcileLimits,
783    ) -> Result<WorkspaceOperationFinish<A, O>, OperationWindowError<S::Error>> {
784        if workspace.id() != lease.workspace_id {
785            return Err(OperationWindowError::IncompatibleState);
786        }
787        let reconcile = match self.finish(lease, now_millis).await? {
788            OperationWindowFinish::StillActive { remaining } => {
789                return Ok(WorkspaceOperationFinish::StillActive { remaining });
790            }
791            OperationWindowFinish::AlreadyClosed => {
792                return Ok(WorkspaceOperationFinish::AlreadyClosed);
793            }
794            OperationWindowFinish::Reconcile(reconcile) => reconcile,
795        };
796        self.reconcile_workspace(workspace, reconcile, limits)
797            .await
798            .map(WorkspaceOperationFinish::Reconciled)
799    }
800
801    /// Runs one previously claimed reconciliation ticket.
802    ///
803    /// This split form lets adapters fence a lease before capturing its mount,
804    /// then publish the capture and finish the core rebase without reopening a
805    /// race for another tool window.
806    pub async fn reconcile_workspace<A: AsyncAuthorityStore, O: AsyncObjectStore>(
807        &self,
808        workspace: &Workspace<A, O>,
809        reconcile: OperationWindowReconcile,
810        limits: OperationReconcileLimits,
811    ) -> Result<WorkspaceRebase<A, O>, OperationWindowError<S::Error>> {
812        let snapshot = self.snapshot(workspace.id()).await?;
813        if !matches!(
814            snapshot.phase,
815            OperationWindowPhase::Reconciling { ticket, .. } if ticket == reconcile.ticket
816        ) {
817            return Err(OperationWindowError::StaleTicket);
818        }
819        let outcome = if reconcile.pending_parent.is_some() {
820            workspace
821                .live_rebase(
822                    IdempotencyKey::from_bytes(reconcile.ticket.into_bytes()),
823                    limits.maximum_generations,
824                    limits.maximum_changes,
825                    limits.maximum_conflicts,
826                )
827                .await
828                .map_err(OperationWindowError::Workspace)?
829        } else {
830            let generation = workspace
831                .head()
832                .await
833                .map_err(OperationWindowError::Workspace)?;
834            WorkspaceRebase::Current(generation)
835        };
836        if matches!(
837            outcome,
838            WorkspaceRebase::Rebased(_)
839                | WorkspaceRebase::AlreadyRebased(_)
840                | WorkspaceRebase::Current(_)
841                | WorkspaceRebase::Conflicted { .. }
842        ) {
843            self.complete_reconcile(workspace.id(), reconcile.ticket)
844                .await?;
845        }
846        Ok(outcome)
847    }
848
849    /// Claims and completes reconciliation left behind by expired leases or a
850    /// crashed adapter. Returns `None` when no recovery work exists.
851    pub async fn recover_workspace<A: AsyncAuthorityStore, O: AsyncObjectStore>(
852        &self,
853        workspace: &Workspace<A, O>,
854        now_millis: u64,
855        limits: OperationReconcileLimits,
856    ) -> Result<Option<WorkspaceRebase<A, O>>, OperationWindowError<S::Error>> {
857        let reconcile = self.claim_reconcile(workspace.id(), now_millis).await?;
858        match reconcile {
859            Some(reconcile) => self
860                .reconcile_workspace(workspace, reconcile, limits)
861                .await
862                .map(Some),
863            None => Ok(None),
864        }
865    }
866
867    /// Claims an expired active window or returns the durable retry ticket for
868    /// an interrupted reconciliation.
869    pub async fn claim_reconcile(
870        &self,
871        workspace_id: WorkspaceId,
872        now_millis: u64,
873    ) -> Result<Option<OperationWindowReconcile>, OperationWindowError<S::Error>> {
874        self.claim_reconcile_with_ticket(workspace_id, now_millis, OperationId::new())
875            .await
876    }
877
878    /// Claims recovery with a caller-supplied deterministic ticket.
879    pub async fn claim_reconcile_with_ticket(
880        &self,
881        workspace_id: WorkspaceId,
882        now_millis: u64,
883        ticket: OperationId,
884    ) -> Result<Option<OperationWindowReconcile>, OperationWindowError<S::Error>> {
885        for _ in 0..MAXIMUM_CAS_ATTEMPTS {
886            let mut current = self.snapshot(workspace_id).await?;
887            match current.phase {
888                OperationWindowPhase::Idle => return Ok(None),
889                OperationWindowPhase::Reconciling {
890                    ticket,
891                    pinned_parent,
892                    pending_parent,
893                    ..
894                } => {
895                    return Ok(Some(OperationWindowReconcile {
896                        ticket,
897                        pinned_parent,
898                        pending_parent,
899                    }));
900                }
901                OperationWindowPhase::Active { .. } => {}
902            }
903            let Some(reconcile) = expire_to_reconcile(&mut current.phase, now_millis, ticket)
904            else {
905                return Ok(None);
906            };
907            let expected = current.revision;
908            current.revision = expected.saturating_add(1);
909            if self.cas(workspace_id, expected, current).await? {
910                return Ok(Some(reconcile));
911            }
912        }
913        Err(OperationWindowError::Contended)
914    }
915
916    /// Completes the exact reconciliation ticket and permits the next window.
917    pub async fn complete_reconcile(
918        &self,
919        workspace_id: WorkspaceId,
920        ticket: OperationId,
921    ) -> Result<(), OperationWindowError<S::Error>> {
922        self.complete_reconcile_with_next_ticket(workspace_id, ticket, OperationId::new())
923            .await
924    }
925
926    /// Completes reconciliation using a caller-supplied ticket if a newer
927    /// parent was observed while reconciliation was active.
928    pub async fn complete_reconcile_with_next_ticket(
929        &self,
930        workspace_id: WorkspaceId,
931        ticket: OperationId,
932        next_ticket: OperationId,
933    ) -> Result<(), OperationWindowError<S::Error>> {
934        for _ in 0..MAXIMUM_CAS_ATTEMPTS {
935            let mut current = self.snapshot(workspace_id).await?;
936            match current.phase {
937                OperationWindowPhase::Reconciling {
938                    ticket: current_ticket,
939                    pinned_parent,
940                    subsequent_parent,
941                    ..
942                } if current_ticket == ticket => {
943                    current.phase = match subsequent_parent {
944                        Some(parent) => OperationWindowPhase::Reconciling {
945                            ticket: next_ticket,
946                            pinned_parent,
947                            pending_parent: Some(parent),
948                            subsequent_parent: None,
949                        },
950                        None => OperationWindowPhase::Idle,
951                    };
952                }
953                _ => return Err(OperationWindowError::StaleTicket),
954            }
955            let expected = current.revision;
956            current.revision = expected.saturating_add(1);
957            if self.cas(workspace_id, expected, current).await? {
958                return Ok(());
959            }
960        }
961        Err(OperationWindowError::Contended)
962    }
963
964    /// Returns validated durable state for diagnostics and recovery.
965    pub async fn inspect(
966        &self,
967        workspace_id: WorkspaceId,
968    ) -> Result<OperationWindowSnapshot, OperationWindowError<S::Error>> {
969        self.snapshot(workspace_id).await
970    }
971
972    /// Loads and validates the current durable window state for inspection.
973    pub async fn snapshot(
974        &self,
975        workspace_id: WorkspaceId,
976    ) -> Result<OperationWindowSnapshot, OperationWindowError<S::Error>> {
977        let stored = self
978            .store
979            .load(workspace_id)
980            .await
981            .map_err(OperationWindowError::Store)?;
982        let state = stored
983            .clone()
984            .unwrap_or_else(|| OperationWindowSnapshot::idle(workspace_id));
985        if state.version != STATE_VERSION
986            || state.workspace_id != workspace_id
987            || stored.is_some() && state.revision == 0
988            || !snapshot_shape_is_valid(&state)
989        {
990            return Err(OperationWindowError::IncompatibleState);
991        }
992        Ok(state)
993    }
994
995    async fn cas(
996        &self,
997        workspace_id: WorkspaceId,
998        expected_revision: u64,
999        replacement: OperationWindowSnapshot,
1000    ) -> Result<bool, OperationWindowError<S::Error>> {
1001        if replacement.version != STATE_VERSION
1002            || replacement.workspace_id != workspace_id
1003            || replacement.revision != expected_revision.saturating_add(1)
1004            || !snapshot_shape_is_valid(&replacement)
1005        {
1006            return Err(OperationWindowError::IncompatibleState);
1007        }
1008        self.store
1009            .compare_and_swap(workspace_id, expected_revision, replacement)
1010            .await
1011            .map_err(OperationWindowError::Store)
1012    }
1013}
1014
1015fn expire_to_reconcile(
1016    phase: &mut OperationWindowPhase,
1017    now_millis: u64,
1018    ticket: OperationId,
1019) -> Option<OperationWindowReconcile> {
1020    let OperationWindowPhase::Active {
1021        pinned_parent,
1022        leases,
1023        pending_parent,
1024    } = phase
1025    else {
1026        return None;
1027    };
1028    leases.retain(|_, lease| lease.expires_at_millis > now_millis);
1029    if !leases.is_empty() {
1030        return None;
1031    }
1032    let reconcile = OperationWindowReconcile {
1033        ticket,
1034        pinned_parent: *pinned_parent,
1035        pending_parent: *pending_parent,
1036    };
1037    *phase = OperationWindowPhase::Reconciling {
1038        ticket: reconcile.ticket,
1039        pinned_parent: reconcile.pinned_parent,
1040        pending_parent: reconcile.pending_parent,
1041        subsequent_parent: None,
1042    };
1043    Some(reconcile)
1044}
1045
1046fn close_lease(
1047    phase: &mut OperationWindowPhase,
1048    lease_id: OperationLeaseId,
1049    expected_expiry_millis: u64,
1050    now_millis: u64,
1051    reconciliation_ticket: OperationId,
1052) -> Option<OperationWindowFinish> {
1053    let OperationWindowPhase::Active {
1054        pinned_parent,
1055        leases,
1056        pending_parent,
1057    } = phase
1058    else {
1059        return None;
1060    };
1061    let requested_live = leases.get(&lease_id).is_some_and(|lease| {
1062        lease.expires_at_millis == expected_expiry_millis && lease.expires_at_millis > now_millis
1063    });
1064    if leases
1065        .get(&lease_id)
1066        .is_some_and(|lease| lease.expires_at_millis != expected_expiry_millis)
1067    {
1068        return None;
1069    }
1070    leases.remove(&lease_id);
1071    leases.retain(|_, lease| lease.expires_at_millis > now_millis);
1072    if !leases.is_empty() {
1073        if !requested_live {
1074            return None;
1075        }
1076        return Some(OperationWindowFinish::StillActive {
1077            remaining: u32::try_from(leases.len()).unwrap_or(u32::MAX),
1078        });
1079    }
1080    let reconcile = OperationWindowReconcile {
1081        ticket: reconciliation_ticket,
1082        pinned_parent: *pinned_parent,
1083        pending_parent: *pending_parent,
1084    };
1085    *phase = OperationWindowPhase::Reconciling {
1086        ticket: reconcile.ticket,
1087        pinned_parent: reconcile.pinned_parent,
1088        pending_parent: reconcile.pending_parent,
1089        subsequent_parent: None,
1090    };
1091    requested_live.then_some(OperationWindowFinish::Reconcile(reconcile))
1092}
1093
1094/// Process-local adapter used by tests and embedded single-process callers.
1095#[derive(Clone, Default)]
1096pub struct MemoryOperationWindowStore {
1097    states: Arc<Mutex<BTreeMap<WorkspaceId, OperationWindowSnapshot>>>,
1098}
1099
1100impl MemoryOperationWindowStore {
1101    /// Creates an empty state adapter.
1102    #[must_use]
1103    pub fn new() -> Self {
1104        Self::default()
1105    }
1106}
1107
1108/// Process-local store synchronization failure.
1109#[derive(Debug, Error)]
1110#[error("operation-window memory store is unavailable")]
1111pub struct MemoryOperationWindowStoreError;
1112
1113impl OperationWindowStore for MemoryOperationWindowStore {
1114    type Error = MemoryOperationWindowStoreError;
1115
1116    async fn load(
1117        &self,
1118        workspace_id: WorkspaceId,
1119    ) -> Result<Option<OperationWindowSnapshot>, Self::Error> {
1120        self.states
1121            .lock()
1122            .map_err(|_| MemoryOperationWindowStoreError)
1123            .map(|states| states.get(&workspace_id).cloned())
1124    }
1125
1126    async fn compare_and_swap(
1127        &self,
1128        workspace_id: WorkspaceId,
1129        expected_revision: u64,
1130        replacement: OperationWindowSnapshot,
1131    ) -> Result<bool, Self::Error> {
1132        let mut states = self
1133            .states
1134            .lock()
1135            .map_err(|_| MemoryOperationWindowStoreError)?;
1136        let revision = states.get(&workspace_id).map_or(0, |state| state.revision);
1137        if revision != expected_revision {
1138            return Ok(false);
1139        }
1140        states.insert(workspace_id, replacement);
1141        Ok(true)
1142    }
1143}
1144
1145#[cfg(test)]
1146#[allow(clippy::expect_used, clippy::panic)]
1147mod tests {
1148    use super::*;
1149    use crate::{
1150        AsyncAuthorityStore, AuthorityId, CancellationToken, Digest, Epoch, ForkOptions, Fs,
1151        IdempotencyKey, OperationId, ProposedCommit, StreamAuthorityStore, WorkBudget,
1152    };
1153    use bytes::Bytes;
1154    use std::sync::atomic::{AtomicU64, Ordering};
1155
1156    #[derive(Default)]
1157    struct TestClock(AtomicU64);
1158
1159    impl acyclic_stream::UnixMillisClock for TestClock {
1160        fn now_unix_millis(&self) -> u64 {
1161            self.0.load(Ordering::SeqCst)
1162        }
1163    }
1164
1165    fn generation(byte: u8) -> GenerationId {
1166        GenerationId::new(Digest::from_bytes([byte; 32]))
1167    }
1168
1169    #[tokio::test]
1170    async fn persisted_active_windows_require_a_nonempty_identity_keyed_lease_set() {
1171        let workspace = WorkspaceId::from_bytes([6; 16]);
1172        let store = MemoryOperationWindowStore::new();
1173        store.states.lock().expect("state lock").insert(
1174            workspace,
1175            OperationWindowSnapshot {
1176                version: STATE_VERSION,
1177                revision: 1,
1178                workspace_id: workspace,
1179                phase: OperationWindowPhase::Active {
1180                    pinned_parent: generation(1),
1181                    leases: BTreeMap::new(),
1182                    pending_parent: None,
1183                },
1184            },
1185        );
1186        let coordinator = OperationWindowCoordinator::new(store);
1187
1188        assert!(matches!(
1189            coordinator.inspect(workspace).await,
1190            Err(OperationWindowError::IncompatibleState)
1191        ));
1192    }
1193
1194    #[tokio::test]
1195    async fn overlapping_leases_coalesce_parent_and_reconcile_once() {
1196        let workspace = WorkspaceId::derive(
1197            [7; 16],
1198            &crate::WorkspaceName::new("agent").expect("valid test workspace"),
1199        );
1200        let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1201        let first = coordinator
1202            .begin(workspace, generation(1), "tool-1", 10, 100)
1203            .await
1204            .expect("first lease");
1205        let second = coordinator
1206            .begin(workspace, generation(1), "tool-2", 11, 100)
1207            .await
1208            .expect("second lease");
1209        assert!(
1210            coordinator
1211                .observe_parent(workspace, generation(2))
1212                .await
1213                .expect("observe")
1214        );
1215        assert_eq!(
1216            coordinator.finish(&first, 20).await.expect("first close"),
1217            OperationWindowFinish::StillActive { remaining: 1 }
1218        );
1219        let OperationWindowFinish::Reconcile(reconcile) =
1220            coordinator.finish(&second, 21).await.expect("final close")
1221        else {
1222            panic!("final close must claim reconciliation");
1223        };
1224        assert_eq!(reconcile.pinned_parent, generation(1));
1225        assert_eq!(reconcile.pending_parent, Some(generation(2)));
1226        assert!(matches!(
1227            coordinator
1228                .begin(workspace, generation(2), "tool-3", 22, 100)
1229                .await,
1230            Err(OperationWindowError::Reconciling)
1231        ));
1232        coordinator
1233            .complete_reconcile(workspace, reconcile.ticket)
1234            .await
1235            .expect("complete reconciliation");
1236        coordinator
1237            .begin(workspace, generation(2), "tool-3", 23, 100)
1238            .await
1239            .expect("next window");
1240    }
1241
1242    #[tokio::test]
1243    async fn parent_advance_after_child_close_rebases_without_another_tool() {
1244        let fs = Fs::memory();
1245        let parent = fs.create_workspace("late-parent").await.expect("parent");
1246        let child = parent
1247            .fork(
1248                "late-child",
1249                ForkOptions::from_generation(
1250                    parent.head().await.expect("base"),
1251                    IdempotencyKey::new(),
1252                ),
1253            )
1254            .await
1255            .expect("child");
1256        child
1257            .write_text("/child", "local")
1258            .await
1259            .expect("child write");
1260        let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1261        let lease = coordinator
1262            .begin(
1263                child.id(),
1264                parent.head().await.expect("parent head").id(),
1265                "tool",
1266                1,
1267                100,
1268            )
1269            .await
1270            .expect("lease");
1271        assert!(matches!(
1272            coordinator
1273                .finish_workspace(&child, &lease, 2, OperationReconcileLimits::default())
1274                .await
1275                .expect("child close"),
1276            WorkspaceOperationFinish::Reconciled(WorkspaceRebase::Current(_))
1277        ));
1278        parent
1279            .write_text("/parent", "upstream")
1280            .await
1281            .expect("parent write");
1282        assert!(matches!(
1283            coordinator
1284                .parent_advanced_workspace(
1285                    &child,
1286                    parent.head().await.expect("new parent head").id(),
1287                    OperationReconcileLimits::default(),
1288                )
1289                .await
1290                .expect("parent advance"),
1291            Some(WorkspaceRebase::Rebased(_))
1292        ));
1293        assert_eq!(
1294            child.read("/parent", 16).await.expect("upstream"),
1295            Bytes::from_static(b"upstream")
1296        );
1297        assert_eq!(
1298            child.read("/child", 16).await.expect("local"),
1299            Bytes::from_static(b"local")
1300        );
1301        assert!(matches!(
1302            coordinator.inspect(child.id()).await.expect("window").phase,
1303            OperationWindowPhase::Idle
1304        ));
1305    }
1306
1307    #[tokio::test]
1308    async fn parent_advance_during_child_tool_rebases_at_final_close() {
1309        let fs = Fs::memory();
1310        let parent = fs.create_workspace("active-parent").await.expect("parent");
1311        let child = parent
1312            .fork(
1313                "active-child",
1314                ForkOptions::from_generation(
1315                    parent.head().await.expect("base"),
1316                    IdempotencyKey::new(),
1317                ),
1318            )
1319            .await
1320            .expect("child");
1321        let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1322        let lease = coordinator
1323            .begin(
1324                child.id(),
1325                parent.head().await.expect("parent head").id(),
1326                "tool",
1327                1,
1328                100,
1329            )
1330            .await
1331            .expect("lease");
1332        parent
1333            .write_text("/parent", "new")
1334            .await
1335            .expect("parent write");
1336        assert!(
1337            coordinator
1338                .parent_advanced_workspace(
1339                    &child,
1340                    parent.head().await.expect("new parent head").id(),
1341                    OperationReconcileLimits::default(),
1342                )
1343                .await
1344                .expect("coalesce")
1345                .is_none()
1346        );
1347        assert!(matches!(
1348            coordinator
1349                .finish_workspace(&child, &lease, 2, OperationReconcileLimits::default())
1350                .await
1351                .expect("final close"),
1352            WorkspaceOperationFinish::Reconciled(WorkspaceRebase::Rebased(_))
1353        ));
1354        assert_eq!(
1355            child.read("/parent", 16).await.expect("upstream").as_ref(),
1356            b"new"
1357        );
1358    }
1359
1360    #[tokio::test]
1361    async fn deterministic_lease_identity_is_idempotent_but_cannot_be_rebound() {
1362        let workspace = WorkspaceId::derive(
1363            [12; 16],
1364            &crate::WorkspaceName::new("lease-identity").expect("valid test workspace"),
1365        );
1366        let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1367        let lease_id = OperationLeaseId::from_bytes([42; 16]);
1368        let first = coordinator
1369            .begin_with_lease_id(workspace, generation(1), "owner", 1, 10, lease_id)
1370            .await
1371            .expect("first lease");
1372        assert_eq!(
1373            coordinator
1374                .begin_with_lease_id(workspace, generation(1), "owner", 2, 10, lease_id)
1375                .await
1376                .expect("exact retry"),
1377            first
1378        );
1379        assert!(matches!(
1380            coordinator
1381                .begin_with_lease_id(workspace, generation(1), "other", 2, 10, lease_id)
1382                .await,
1383            Err(OperationWindowError::LeaseIdentityConflict)
1384        ));
1385        assert!(matches!(
1386            coordinator
1387                .begin_with_lease_id(workspace, generation(1), "owner", 2, 11, lease_id)
1388                .await,
1389            Err(OperationWindowError::LeaseIdentityConflict)
1390        ));
1391    }
1392
1393    #[tokio::test]
1394    async fn renewal_replaces_the_exact_live_lease_and_fences_stale_handles() {
1395        let workspace = WorkspaceId::derive(
1396            [13; 16],
1397            &crate::WorkspaceName::new("lease-renewal").expect("valid test workspace"),
1398        );
1399        let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1400        let lease = coordinator
1401            .begin(workspace, generation(1), "owner", 1, 10)
1402            .await
1403            .expect("lease");
1404        let renewed = coordinator
1405            .renew(&lease, 2, 20)
1406            .await
1407            .expect("renew live lease");
1408        assert_eq!(renewed.expires_at_millis, 20);
1409        assert_eq!(renewed.pinned_parent, lease.pinned_parent);
1410        assert!(matches!(
1411            coordinator.renew(&lease, 3, 30).await,
1412            Err(OperationWindowError::StaleLease)
1413        ));
1414        assert!(matches!(
1415            coordinator.finish(&lease, 4).await,
1416            Ok(OperationWindowFinish::AlreadyClosed)
1417        ));
1418        assert!(matches!(
1419            coordinator.finish(&renewed, 4).await,
1420            Ok(OperationWindowFinish::Reconcile(_))
1421        ));
1422    }
1423
1424    #[tokio::test]
1425    async fn parent_observed_during_reconcile_is_not_lost() {
1426        let workspace = WorkspaceId::derive(
1427            [8; 16],
1428            &crate::WorkspaceName::new("agent").expect("valid test workspace"),
1429        );
1430        let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1431        let lease = coordinator
1432            .begin(workspace, generation(1), "tool", 10, 100)
1433            .await
1434            .expect("lease");
1435        let OperationWindowFinish::Reconcile(reconcile) =
1436            coordinator.finish(&lease, 20).await.expect("finish")
1437        else {
1438            panic!("final close must claim reconciliation");
1439        };
1440        coordinator
1441            .observe_parent(workspace, generation(2))
1442            .await
1443            .expect("observe during reconcile");
1444        coordinator
1445            .complete_reconcile(workspace, reconcile.ticket)
1446            .await
1447            .expect("complete first reconcile");
1448        assert!(matches!(
1449            coordinator.inspect(workspace).await.expect("inspect").phase,
1450            OperationWindowPhase::Reconciling {
1451                pending_parent: Some(parent),
1452                ..
1453            } if parent == generation(2)
1454        ));
1455    }
1456
1457    #[tokio::test]
1458    async fn expired_lease_is_fenced_and_recovery_ticket_is_claimable() {
1459        let workspace = WorkspaceId::derive(
1460            [9; 16],
1461            &crate::WorkspaceName::new("expired").expect("valid test workspace"),
1462        );
1463        let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1464        let lease = coordinator
1465            .begin(workspace, generation(1), "tool", 10, 20)
1466            .await
1467            .expect("lease");
1468        assert_eq!(
1469            coordinator.finish(&lease, 20).await.expect("expired close"),
1470            OperationWindowFinish::AlreadyClosed
1471        );
1472        let claimed = coordinator
1473            .claim_reconcile(workspace, 20)
1474            .await
1475            .expect("claim")
1476            .expect("recovery ticket");
1477        assert_eq!(claimed.pinned_parent, generation(1));
1478        assert!(matches!(
1479            coordinator
1480                .begin(workspace, generation(1), "late", 21, 30)
1481                .await,
1482            Err(OperationWindowError::Reconciling)
1483        ));
1484    }
1485
1486    #[tokio::test]
1487    async fn expired_close_is_persistently_fenced_while_another_lease_remains() {
1488        let workspace = WorkspaceId::derive(
1489            [10; 16],
1490            &crate::WorkspaceName::new("overlap-expiry").expect("valid test workspace"),
1491        );
1492        let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1493        let expired = coordinator
1494            .begin(workspace, generation(1), "expired", 10, 20)
1495            .await
1496            .expect("expired lease");
1497        let active = coordinator
1498            .begin(workspace, generation(1), "active", 11, 40)
1499            .await
1500            .expect("active lease");
1501
1502        assert_eq!(
1503            coordinator
1504                .finish(&expired, 20)
1505                .await
1506                .expect("expired close"),
1507            OperationWindowFinish::AlreadyClosed
1508        );
1509        let snapshot = coordinator.inspect(workspace).await.expect("inspect");
1510        assert!(matches!(
1511            snapshot.phase,
1512            OperationWindowPhase::Active { ref leases, .. }
1513                if leases.len() == 1 && leases.contains_key(&active.lease_id)
1514        ));
1515        assert!(matches!(
1516            coordinator.finish(&active, 22).await.expect("active close"),
1517            OperationWindowFinish::Reconcile(_)
1518        ));
1519    }
1520
1521    #[tokio::test]
1522    async fn duplicate_close_persists_expiry_of_the_remaining_lease() {
1523        let workspace = WorkspaceId::derive(
1524            [11; 16],
1525            &crate::WorkspaceName::new("duplicate-expiry").expect("valid test workspace"),
1526        );
1527        let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1528        let first = coordinator
1529            .begin(workspace, generation(1), "first", 1, 10)
1530            .await
1531            .expect("first lease");
1532        coordinator
1533            .begin(workspace, generation(1), "second", 2, 40)
1534            .await
1535            .expect("second lease");
1536        assert_eq!(
1537            coordinator.finish(&first, 5).await.expect("first close"),
1538            OperationWindowFinish::StillActive { remaining: 1 }
1539        );
1540        assert_eq!(
1541            coordinator
1542                .finish(&first, 50)
1543                .await
1544                .expect("duplicate close"),
1545            OperationWindowFinish::AlreadyClosed
1546        );
1547        assert!(matches!(
1548            coordinator.inspect(workspace).await.expect("inspect").phase,
1549            OperationWindowPhase::Reconciling { .. }
1550        ));
1551    }
1552
1553    #[tokio::test]
1554    async fn stream_lease_gate_fences_one_writer_without_fencing_its_peer() {
1555        let clock = Arc::new(TestClock::default());
1556        clock.0.store(10, Ordering::SeqCst);
1557        let stream = Arc::new(acyclic_stream::MemoryStream::new_with_clock(
1558            acyclic_stream::MemoryLimits::default(),
1559            clock,
1560        ));
1561        let authority = StreamAuthorityStore::new(Arc::clone(&stream));
1562        let windows =
1563            OperationWindowCoordinator::new(StreamOperationWindowStore::new(Arc::clone(&stream)));
1564        let workspace = WorkspaceId::from_bytes([61; 16]);
1565        let authority_id = crate::kernel::volume_authority_id(workspace.volume_id());
1566        let unrelated_authority = AuthorityId::from_bytes([62; 16]);
1567        let cancellation = CancellationToken::new();
1568        AsyncAuthorityStore::create_authority(
1569            &authority,
1570            unrelated_authority,
1571            Epoch::GENESIS,
1572            WorkBudget::UNBOUNDED,
1573            &cancellation,
1574        )
1575        .await
1576        .expect("create unrelated authority");
1577        AsyncAuthorityStore::create_authority(
1578            &authority,
1579            authority_id,
1580            Epoch::GENESIS,
1581            WorkBudget::UNBOUNDED,
1582            &cancellation,
1583        )
1584        .await
1585        .expect("create authority");
1586        let first = windows
1587            .begin(workspace, generation(1), "first", 1, 100)
1588            .await
1589            .expect("first lease");
1590        let second = windows
1591            .begin(workspace, generation(1), "second", 2, 100)
1592            .await
1593            .expect("second lease");
1594        assert_eq!(
1595            windows.finish(&first, 3).await.expect("close first"),
1596            OperationWindowFinish::StillActive { remaining: 1 }
1597        );
1598
1599        let head = AsyncAuthorityStore::head(
1600            &authority,
1601            authority_id,
1602            WorkBudget::UNBOUNDED,
1603            &cancellation,
1604        )
1605        .await
1606        .expect("authority head")
1607        .value;
1608        let rejected = AsyncAuthorityStore::compare_and_append_guarded(
1609            &authority,
1610            crate::GuardedAppend {
1611                authority_id,
1612                epoch: head.epoch,
1613                expected: head,
1614                commit: ProposedCommit {
1615                    operation_id: OperationId::from_bytes([71; 16]),
1616                    fingerprint: Digest::from_bytes([72; 32]),
1617                    payload: Bytes::from_static(b"first"),
1618                },
1619                permit: first.publication_permit(),
1620            },
1621            WorkBudget::UNBOUNDED,
1622            &cancellation,
1623        )
1624        .await
1625        .expect("guarded rejection")
1626        .value;
1627        assert!(matches!(rejected, crate::AppendOutcome::Fenced { .. }));
1628
1629        let committed = AsyncAuthorityStore::compare_and_append_guarded(
1630            &authority,
1631            crate::GuardedAppend {
1632                authority_id,
1633                epoch: head.epoch,
1634                expected: head,
1635                commit: ProposedCommit {
1636                    operation_id: OperationId::from_bytes([73; 16]),
1637                    fingerprint: Digest::from_bytes([74; 32]),
1638                    payload: Bytes::from_static(b"second"),
1639                },
1640                permit: second.publication_permit(),
1641            },
1642            WorkBudget::UNBOUNDED,
1643            &cancellation,
1644        )
1645        .await
1646        .expect("guarded append")
1647        .value;
1648        assert!(matches!(committed, crate::AppendOutcome::Committed(_)));
1649
1650        let unrelated_head = AsyncAuthorityStore::head(
1651            &authority,
1652            unrelated_authority,
1653            WorkBudget::UNBOUNDED,
1654            &cancellation,
1655        )
1656        .await
1657        .expect("unrelated authority head")
1658        .value;
1659        let rejected = AsyncAuthorityStore::compare_and_append_guarded(
1660            &authority,
1661            crate::GuardedAppend {
1662                authority_id: unrelated_authority,
1663                epoch: unrelated_head.epoch,
1664                expected: unrelated_head,
1665                commit: ProposedCommit {
1666                    operation_id: OperationId::from_bytes([75; 16]),
1667                    fingerprint: Digest::from_bytes([76; 32]),
1668                    payload: Bytes::from_static(b"cross-authority"),
1669                },
1670                permit: second.publication_permit(),
1671            },
1672            WorkBudget::UNBOUNDED,
1673            &cancellation,
1674        )
1675        .await
1676        .expect("cross-authority lease is a semantic fence")
1677        .value;
1678        assert!(matches!(rejected, crate::AppendOutcome::Fenced { .. }));
1679        assert_eq!(
1680            AsyncAuthorityStore::head(
1681                &authority,
1682                unrelated_authority,
1683                WorkBudget::UNBOUNDED,
1684                &cancellation,
1685            )
1686            .await
1687            .expect("unrelated authority remains unchanged")
1688            .value,
1689            unrelated_head
1690        );
1691    }
1692
1693    #[tokio::test]
1694    async fn provider_deadline_fences_without_a_separate_expiry_sweep() {
1695        let clock = Arc::new(TestClock::default());
1696        clock.0.store(10, Ordering::SeqCst);
1697        let stream = Arc::new(acyclic_stream::MemoryStream::new_with_clock(
1698            acyclic_stream::MemoryLimits::default(),
1699            clock.clone(),
1700        ));
1701        let authority = StreamAuthorityStore::new(Arc::clone(&stream));
1702        let windows =
1703            OperationWindowCoordinator::new(StreamOperationWindowStore::new(Arc::clone(&stream)));
1704        let workspace = WorkspaceId::from_bytes([81; 16]);
1705        let authority_id = crate::kernel::volume_authority_id(workspace.volume_id());
1706        let cancellation = CancellationToken::new();
1707        AsyncAuthorityStore::create_authority(
1708            &authority,
1709            authority_id,
1710            Epoch::GENESIS,
1711            WorkBudget::UNBOUNDED,
1712            &cancellation,
1713        )
1714        .await
1715        .expect("create authority");
1716        let lease = windows
1717            .begin(workspace, generation(1), "tool", 10, 20)
1718            .await
1719            .expect("lease");
1720        let head = AsyncAuthorityStore::head(
1721            &authority,
1722            authority_id,
1723            WorkBudget::UNBOUNDED,
1724            &cancellation,
1725        )
1726        .await
1727        .expect("authority head")
1728        .value;
1729        clock.0.store(20, Ordering::SeqCst);
1730        let outcome = AsyncAuthorityStore::compare_and_append_guarded(
1731            &authority,
1732            crate::GuardedAppend {
1733                authority_id,
1734                epoch: head.epoch,
1735                expected: head,
1736                commit: ProposedCommit {
1737                    operation_id: OperationId::from_bytes([83; 16]),
1738                    fingerprint: Digest::from_bytes([84; 32]),
1739                    payload: Bytes::from_static(b"expired"),
1740                },
1741                permit: lease.publication_permit(),
1742            },
1743            WorkBudget::UNBOUNDED,
1744            &cancellation,
1745        )
1746        .await
1747        .expect("deadline is a semantic fence")
1748        .value;
1749        assert!(matches!(outcome, crate::AppendOutcome::Fenced { .. }));
1750        assert_eq!(
1751            AsyncAuthorityStore::head(
1752                &authority,
1753                authority_id,
1754                WorkBudget::UNBOUNDED,
1755                &cancellation,
1756            )
1757            .await
1758            .expect("unchanged authority")
1759            .value,
1760            head
1761        );
1762    }
1763}