Skip to main content

cookie_agent_engine/session/
mod.rs

1//! Session directories, projections, and rebuildable metadata caches.
2
3mod cache;
4mod fold;
5mod tree_load;
6mod workdir;
7
8pub(crate) use cache::meta_path;
9#[cfg(windows)]
10pub(crate) use cache::replace_windows_path_with_retry;
11use cache::*;
12pub(crate) use fold::projection;
13pub(crate) use fold::restart_stable_grant;
14use fold::*;
15pub(crate) use tree_load::LogFingerprint;
16pub(crate) use tree_load::TreeLoadObserver;
17pub(crate) use tree_load::TreeLoadProducts;
18pub(crate) use tree_load::TreeState;
19use tree_load::*;
20#[cfg(unix)]
21pub(crate) use workdir::create_unix_session_directory_all;
22#[cfg(windows)]
23pub(crate) use workdir::create_windows_session_directory;
24use workdir::*;
25
26use std::{
27    collections::{BTreeMap, HashMap, HashSet},
28    fs,
29    hash::{Hash, Hasher},
30    io::Write,
31    path::{Path, PathBuf},
32    sync::{
33        Arc, Condvar, Mutex,
34        atomic::{AtomicBool, Ordering},
35    },
36};
37
38#[cfg(unix)]
39use std::{
40    fs::OpenOptions,
41    os::unix::{
42        ffi::OsStrExt,
43        fs::{OpenOptionsExt, PermissionsExt},
44    },
45};
46
47use cookie_agent_protocol::{
48    AgentId, AgentSnapshot, ChildSummary, ClientRenameId, ClientRunId, EventPayload,
49    EventSubscriptionMessage, EventsSubscribeResult, RunId, RunSelection, SessionId, SessionMeta,
50    SessionOrigin, SessionPermissionOverlay, SessionRenameRecord, SessionStatus, SessionTitle,
51    SessionTitleChange, SessionTree, StoredEvent, ToolCallId, Usage, UsageRollup,
52};
53use thiserror::Error;
54use tokio::sync::mpsc;
55use uuid::Uuid;
56
57use crate::events::{EventLog, EventLogError, fsync_directory};
58use crate::ownership::{
59    HeldLock, OWNER_LOCK_FILE, OWNER_LOCK_SUFFIX, SessionOwnership, WriteAuthority,
60    WriteCapability, owner_lock_path, try_acquire,
61};
62
63pub(crate) const WORKDIR_CWD_FILE: &str = "cwd";
64/// v2 session-store root under the data root (`~/.cookie-agent/sessions`).
65pub(crate) const SESSIONS_ROOT_DIR: &str = "sessions";
66/// Marker file recording the on-disk layout version of a work-dir store.
67pub(crate) const LAYOUT_MARKER_FILE: &str = "layout.json";
68/// Session metadata cache file name.
69pub(crate) const SESSION_META_FILE: &str = "metadata";
70/// Per-root directory holding delegated child sessions.
71pub(crate) const SUBAGENTS_DIR: &str = "subagents";
72/// Persisted child-summary cache inside [`SUBAGENTS_DIR`].
73pub(crate) const SUBAGENT_INDEX_FILE: &str = "index.json";
74/// Current `subagents/index.json` schema version.
75const SUBAGENT_INDEX_VERSION: u32 = 1;
76const PERSISTED_SUBSCRIBER_QUEUE_CAPACITY: usize = 256;
77/// Event log file name.
78pub(crate) const EVENTS_FILE: &str = "events.jsonl";
79/// Layout version written by this build.
80pub(crate) const LAYOUT_VERSION: u32 = 2;
81
82/// Where a session lives relative to its work-dir store.
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub(crate) enum SessionLocation {
85    /// `sessions/<workdirkey>/<id>/`
86    Root,
87    /// `sessions/<workdirkey>/<root>/subagents/<id>/`
88    Child { root: SessionId },
89}
90
91impl SessionLocation {
92    fn root_of(self) -> Option<SessionId> {
93        match self {
94            Self::Root => None,
95            Self::Child { root } => Some(root),
96        }
97    }
98}
99
100#[derive(Debug, Error)]
101pub enum SessionError {
102    #[error(transparent)]
103    Event(#[from] EventLogError),
104    #[error("session IO failure at {path}: {source}")]
105    Io {
106        path: PathBuf,
107        #[source]
108        source: std::io::Error,
109    },
110    #[error("invalid session metadata at {path}: {source}")]
111    Json {
112        path: PathBuf,
113        #[source]
114        source: serde_json::Error,
115    },
116    #[error("session {0} not found")]
117    Missing(SessionId),
118    #[error("session {0} is owned by another cookie process")]
119    SessionLocked(SessionId),
120    #[error("session store is closed")]
121    StoreClosed,
122    #[error("sequence {through_seq} is not a valid event in session {session_id}")]
123    InvalidSequence {
124        session_id: SessionId,
125        through_seq: u64,
126    },
127    #[error("invalid fork title: {0}")]
128    InvalidForkTitle(String),
129    /// A lazy tree load completed but the engine rejected its products (a child
130    /// log referencing a manifest this runtime cannot serve, for example).
131    /// `impl From<SessionError> for EngineError` unwraps it back to its type.
132    #[error("tree load rejected: {0}")]
133    TreeRejected(Box<crate::runtime::EngineError>),
134    /// The tree of a root kept losing its read/install window to concurrent
135    /// writers, or the load was retaken more times than the driver budget allows.
136    #[error("tree load for session {0} keeps racing concurrent writers")]
137    TreeContended(SessionId),
138}
139
140#[derive(Clone, Debug, PartialEq)]
141pub struct RunProjection {
142    pub id: RunId,
143    pub client_run_id: ClientRunId,
144    pub input: String,
145    pub selection: RunSelection,
146    pub agent: AgentSnapshot,
147    pub status: SessionStatus,
148    pub final_text: Option<String>,
149    pub pending_calls: HashMap<ToolCallId, String>,
150}
151
152#[derive(Clone, Debug)]
153pub struct SessionProjection {
154    pub meta: SessionMeta,
155    pub creation_agent: AgentSnapshot,
156    pub status: SessionStatus,
157    pub usage: Option<Usage>,
158    pub usage_rollup: UsageRollup,
159    pub agent_usage: BTreeMap<AgentId, UsageRollup>,
160    pub runs: HashMap<RunId, RunProjection>,
161    pub rename_records: HashMap<cookie_agent_protocol::ClientRenameId, SessionRenameRecord>,
162    pub permission_overlay: SessionPermissionOverlay,
163    pub log: Arc<EventLog>,
164}
165
166#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
167pub struct SessionSummary {
168    pub meta: SessionMeta,
169    pub usage: Option<Usage>,
170    pub usage_rollup: UsageRollup,
171    pub agent_usage: BTreeMap<AgentId, UsageRollup>,
172}
173
174/// One entry of a root's `subagents/index.json` child-summary cache (§3.4).
175#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
176struct IndexedChild {
177    summary: SessionSummary,
178    /// Run ids that reached a terminal status, for the delegation registry.
179    terminal_runs: BTreeMap<String, SessionStatus>,
180}
181
182/// Persisted child-summary cache (`subagents/index.json`, §3.4). A missing or
183/// corrupt file only means "children unknown until tree load".
184#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
185struct SubagentIndex {
186    version: u32,
187    children: Vec<IndexedChild>,
188}
189
190impl SessionProjection {
191    #[must_use]
192    pub fn metadata(&self) -> SessionMeta {
193        self.meta.clone()
194    }
195}
196
197#[derive(Debug, Default)]
198struct SessionResidency {
199    resident: HashMap<SessionId, SessionProjection>,
200    evicted: HashMap<SessionId, SessionSummary>,
201}
202
203impl SessionResidency {
204    fn known_ids(&self) -> Vec<SessionId> {
205        self.resident
206            .keys()
207            .chain(self.evicted.keys())
208            .copied()
209            .collect::<HashSet<_>>()
210            .into_iter()
211            .collect()
212    }
213}
214
215/// Ownership of one root session tree. Exactly one lock and one
216/// [`WriteAuthority`] exist per tree; every log in the tree writes under that
217/// authority, so dropping the tree's entry invalidates all of them at once.
218#[derive(Debug)]
219enum TreeOwnership {
220    /// The root was created in this process and has not been published yet, so
221    /// there is no directory to lock. Children may already be writing.
222    PendingPublish { authority: WriteAuthority },
223    /// The tree lock is held for an adoption that has not been committed yet.
224    Adopting {
225        _lock: HeldLock,
226        authority: WriteAuthority,
227    },
228    Owned {
229        _lock: HeldLock,
230        authority: WriteAuthority,
231    },
232    /// Another process holds this tree's lock (or it could not be classified).
233    Foreign,
234}
235
236impl TreeOwnership {
237    /// The authority every log in the tree writes under, when this process
238    /// holds the tree at all.
239    fn authority(&self) -> Option<&WriteAuthority> {
240        match self {
241            Self::PendingPublish { authority }
242            | Self::Adopting { authority, .. }
243            | Self::Owned { authority, .. } => Some(authority),
244            Self::Foreign => None,
245        }
246    }
247
248    /// Whether the tree is held *and settled*: an uncommitted adoption is not
249    /// yet a writable tree.
250    fn is_settled(&self) -> bool {
251        matches!(self, Self::PendingPublish { .. } | Self::Owned { .. })
252    }
253}
254
255/// The store's ownership bookkeeping: one entry per root tree, plus the
256/// per-session record of which sessions this process may write. A session is
257/// writable only when its tree is held here *and* the session was created in
258/// this process or adopted (reconciled and committed) in it.
259#[derive(Debug, Default)]
260struct OwnershipState {
261    /// root session id -> the tree's ownership state.
262    trees: HashMap<SessionId, TreeOwnership>,
263    /// session id -> its root, for sessions created or adopted in this process.
264    writable: HashMap<SessionId, SessionId>,
265    /// session id -> its root, for adoptions that have not been committed.
266    adopting: HashMap<SessionId, SessionId>,
267}
268
269impl OwnershipState {
270    /// Capability for a write to `id`. `allow_adopting` admits a session whose
271    /// adoption is still in flight — the reconciliation window, which is the
272    /// only time an uncommitted session may append.
273    fn capability(
274        &self,
275        id: SessionId,
276        allow_adopting: bool,
277    ) -> Result<WriteCapability, SessionError> {
278        let root = match self.writable.get(&id) {
279            Some(root) => *root,
280            None if allow_adopting => *self
281                .adopting
282                .get(&id)
283                .ok_or(SessionError::SessionLocked(id))?,
284            None => return Err(SessionError::SessionLocked(id)),
285        };
286        let tree = self
287            .trees
288            .get(&root)
289            .ok_or(SessionError::SessionLocked(id))?;
290        if !allow_adopting && !tree.is_settled() {
291            return Err(SessionError::SessionLocked(id));
292        }
293        tree.authority()
294            .map(WriteAuthority::capability)
295            .ok_or(SessionError::SessionLocked(id))
296    }
297
298    /// Whether any session still depends on the tree's lock. Used to decide
299    /// whether a rolled-back adoption releases it.
300    fn tree_is_referenced(&self, root: SessionId) -> bool {
301        self.writable.values().any(|owner| *owner == root)
302            || self.adopting.values().any(|owner| *owner == root)
303    }
304}
305
306/// How a fork relates to the tree it publishes into.
307#[derive(Debug)]
308enum ForkTree {
309    /// A forked root opens a tree of its own, locked at publication.
310    NewRoot(WriteAuthority),
311    /// A forked child joins its root's tree, carrying the lock when this fork
312    /// is what took it.
313    Joined(Option<(HeldLock, WriteAuthority)>),
314}
315
316#[derive(Clone, Copy, Debug, Eq, PartialEq)]
317pub(crate) enum WriteOpen {
318    AlreadyOwned,
319    Adopting,
320}
321
322#[derive(Debug)]
323pub struct SessionStore {
324    data_root: PathBuf,
325    /// The work-dir store: `sessions/<workdirkey>/`. Root session dirs live
326    /// directly inside it, alongside the work-dir files (`cwd`, the layout
327    /// marker, the grant journal, runtime revisions and artifacts).
328    workdir_dir: PathBuf,
329    cwd: PathBuf,
330    /// id -> where it lives on disk. Roots are inserted at discovery; children
331    /// lazily (tree load, index cache, or direct-address locate).
332    locations: Mutex<HashMap<SessionId, SessionLocation>>,
333    /// root id -> state of the lazy tree load.
334    trees: Mutex<HashMap<SessionId, TreeState>>,
335    /// Per-root load gates: who is driving the pass, and what waiters block on.
336    tree_locks: Mutex<HashMap<SessionId, Arc<TreeGate>>>,
337    /// Completed tree loads waiting for the engine hook that consumes them (D5).
338    pending_loads: Mutex<PendingLoads>,
339    residency: Mutex<SessionResidency>,
340    /// Tree-scoped ownership: one lock per root, plus per-session writability.
341    ownership: Mutex<OwnershipState>,
342    /// Per-*tree* adoption gates, keyed by root: adopting two sessions of one
343    /// tree concurrently would race on the single tree lock.
344    adoption_locks: Mutex<HashMap<SessionId, Arc<Mutex<()>>>>,
345    /// Serializes publication of one session directory, so the scaffold check in
346    /// [`Self::publish_prepared_dir`] stays true until its entries have moved.
347    publish_locks: Mutex<HashMap<SessionId, Arc<Mutex<()>>>>,
348    mutation: Mutex<()>,
349    subscribers: Mutex<HashMap<SessionId, Vec<mpsc::Sender<EventSubscriptionMessage>>>>,
350    closed: AtomicBool,
351    #[cfg(test)]
352    eviction_transition_hook: Mutex<Option<EvictionTransitionHook>>,
353    #[cfg(test)]
354    publish_hook: Mutex<Option<PublishHook>>,
355    /// Test-only: how many times each session's `events.jsonl` was opened and
356    /// folded by this store (§8.2 #5). Counts log opens, not load attempts.
357    #[cfg(test)]
358    log_opens: Mutex<HashMap<SessionId, usize>>,
359    /// Test-only: fires once at the end of a load read phase, before the fold is
360    /// verified and installed, so a test can race a write into that window (L1).
361    #[cfg(test)]
362    tree_load_read_hook: TreeLoadReadHook,
363}
364
365impl SessionStore {
366    /// The `<16-hex-hash>` component of the v2 work-dir key.
367    fn project_hash(cwd: &Path) -> String {
368        let canonical = cwd.canonicalize().unwrap_or_else(|_| cwd.to_owned());
369        let mut hash = std::collections::hash_map::DefaultHasher::new();
370        canonical.to_string_lossy().hash(&mut hash);
371        format!("{:016x}", hash.finish())
372    }
373
374    /// v2 work-dir key: `<16-hex-hash>-<sanitized-basename>` (§1.1). The hash
375    /// prefix is the only component ever matched against; the suffix is
376    /// cosmetic and computed once at directory creation.
377    pub(crate) fn workdir_key(cwd: &Path) -> String {
378        let hash = Self::project_hash(cwd);
379        let suffix = workdir_key_suffix(cwd);
380        if suffix.is_empty() {
381            hash
382        } else {
383            format!("{hash}-{suffix}")
384        }
385    }
386
387    /// Locate the v2 work-dir for `cwd`, matching an existing directory by hash
388    /// prefix (a stale suffix after a cwd rename is harmless), else reserve the
389    /// freshly derived name.
390    pub(crate) fn resolve_workdir_dir(data_root: &Path, cwd: &Path) -> PathBuf {
391        let hash = Self::project_hash(cwd);
392        let sessions_root = data_root.join(SESSIONS_ROOT_DIR);
393        if let Ok(entries) = fs::read_dir(&sessions_root) {
394            for entry in entries.flatten() {
395                let name = entry.file_name().to_string_lossy().to_string();
396                if !entry.path().is_dir() {
397                    continue;
398                }
399                if name == hash || name.starts_with(&format!("{hash}-")) {
400                    return entry.path();
401                }
402            }
403        }
404        sessions_root.join(Self::workdir_key(cwd))
405    }
406
407    pub fn open(data_root: &Path, cwd: &Path) -> Result<Arc<Self>, SessionError> {
408        let v2_dir = Self::resolve_workdir_dir(data_root, cwd);
409        #[cfg(unix)]
410        create_unix_session_directory_all(&v2_dir)?;
411        #[cfg(windows)]
412        for path in [data_root.join(SESSIONS_ROOT_DIR), v2_dir.clone()] {
413            create_windows_session_directory(&path)?;
414        }
415        write_layout_marker_if_absent(&v2_dir)?;
416        write_workdir_cwd(&v2_dir, cwd)?;
417        let store = Arc::new(Self {
418            data_root: data_root.to_owned(),
419            workdir_dir: v2_dir,
420            cwd: cwd.canonicalize().unwrap_or_else(|_| cwd.to_owned()),
421            locations: Mutex::new(HashMap::new()),
422            trees: Mutex::new(HashMap::new()),
423            tree_locks: Mutex::new(HashMap::new()),
424            pending_loads: Mutex::new(PendingLoads::default()),
425            residency: Mutex::new(SessionResidency::default()),
426            ownership: Mutex::new(OwnershipState::default()),
427            adoption_locks: Mutex::new(HashMap::new()),
428            publish_locks: Mutex::new(HashMap::new()),
429            mutation: Mutex::new(()),
430            subscribers: Mutex::new(HashMap::new()),
431            closed: AtomicBool::new(false),
432            #[cfg(test)]
433            eviction_transition_hook: Mutex::new(None),
434            #[cfg(test)]
435            publish_hook: Mutex::new(None),
436            #[cfg(test)]
437            log_opens: Mutex::new(HashMap::new()),
438            #[cfg(test)]
439            tree_load_read_hook: TreeLoadReadHook::default(),
440        });
441        store.refresh_discovered();
442        Ok(store)
443    }
444
445    /// The data root this store was opened against.
446    #[must_use]
447    pub fn data_root(&self) -> &Path {
448        &self.data_root
449    }
450
451    /// Path of `id`'s session directory for a known placement.
452    fn path_for(&self, location: SessionLocation, id: SessionId) -> PathBuf {
453        match location {
454            SessionLocation::Root => self.workdir_dir.join(id.to_string()),
455            SessionLocation::Child { root } => self
456                .workdir_dir
457                .join(root.to_string())
458                .join(SUBAGENTS_DIR)
459                .join(id.to_string()),
460        }
461    }
462
463    #[must_use]
464    fn cached_location(&self, id: SessionId) -> Option<SessionLocation> {
465        self.locations
466            .lock()
467            .unwrap_or_else(|poisoned| poisoned.into_inner())
468            .get(&id)
469            .copied()
470    }
471
472    fn record_location(&self, id: SessionId, location: SessionLocation) {
473        self.locations
474            .lock()
475            .unwrap_or_else(|poisoned| poisoned.into_inner())
476            .insert(id, location);
477    }
478
479    /// Root of the tree `id` belongs to (`id` itself for a root session).
480    /// Unknown children are located on disk first, so the answer is
481    /// disk-accurate — it decides which `owner.lock` guards the session.
482    pub(crate) fn root_of(&self, id: SessionId) -> Result<SessionId, SessionError> {
483        self.resolve_dir(id)?;
484        Ok(self
485            .cached_location(id)
486            .and_then(SessionLocation::root_of)
487            .unwrap_or(id))
488    }
489
490    /// Directory whose `owner.lock` guards the tree rooted at `root`.
491    fn tree_root_dir(&self, root: SessionId) -> PathBuf {
492        self.path_for(SessionLocation::Root, root)
493    }
494
495    /// Resolves the on-disk directory of `id` by placement (§2.2).
496    pub(crate) fn resolve_dir(&self, id: SessionId) -> Result<PathBuf, SessionError> {
497        if let Some(location) = self.cached_location(id) {
498            return Ok(self.path_for(location, id));
499        }
500        let root_dir = self.workdir_dir.join(id.to_string());
501        if root_dir.is_dir() {
502            self.record_location(id, SessionLocation::Root);
503            return Ok(root_dir);
504        }
505        if let Some(root) = self.locate_child(id) {
506            self.record_location(id, SessionLocation::Child { root });
507            return Ok(self.path_for(SessionLocation::Child { root }, id));
508        }
509        Err(SessionError::Missing(id))
510    }
511
512    /// Direct-address locate for an unknown child: stat each root's
513    /// `subagents/<id>/metadata`. O(#roots) stats, paid once per unknown child
514    /// before `locations` caches the answer.
515    fn locate_child(&self, id: SessionId) -> Option<SessionId> {
516        for root in self.root_dir_ids() {
517            let dir = self
518                .workdir_dir
519                .join(root.to_string())
520                .join(SUBAGENTS_DIR)
521                .join(id.to_string());
522            if dir.join(SESSION_META_FILE).exists() {
523                return Some(root);
524            }
525        }
526        None
527    }
528
529    /// Session-id-named directories directly inside the work dir. These are
530    /// roots *by construction*.
531    fn root_dir_ids(&self) -> Vec<SessionId> {
532        let mut ids = Vec::new();
533        let Ok(entries) = fs::read_dir(&self.workdir_dir) else {
534            return ids;
535        };
536        for entry in entries.flatten() {
537            if !entry.path().is_dir() {
538                continue;
539            }
540            let Ok(id) = entry.file_name().to_string_lossy().parse::<SessionId>() else {
541                continue;
542            };
543            ids.push(id);
544        }
545        ids
546    }
547
548    /// Single source of truth for where a *new* session is created (§2.2).
549    fn placement_for(&self, origin: &SessionOrigin) -> SessionLocation {
550        match origin {
551            SessionOrigin::Root => SessionLocation::Root,
552            SessionOrigin::Delegated {
553                root_session_id, ..
554            } => SessionLocation::Child {
555                root: *root_session_id,
556            },
557        }
558    }
559
560    /// Origin carried by a creation payload, which decides placement.
561    fn creation_origin(creation: &EventPayload) -> Option<SessionOrigin> {
562        match creation {
563            EventPayload::SessionCreated { origin, .. } => Some(origin.clone()),
564            _ => None,
565        }
566    }
567
568    /// Directory a session's metadata cache lives in, plus its placement.
569    fn dir_for_placement(&self, location: SessionLocation, id: SessionId) -> PathBuf {
570        self.path_for(location, id)
571    }
572
573    /// Moves a fully prepared session directory into its published location.
574    ///
575    /// A root's directory can already exist as a placement scaffold: a child
576    /// created while the root was still buffered is filed under `<root>/subagents/`
577    /// which brings `<root>` into being before the root itself publishes. Such a
578    /// scaffold holds *nothing but* that directory, so the prepared files are
579    /// merged into it. Anything else present — another file, a foreign directory,
580    /// or an empty directory that is not a scaffold at all — means the location is
581    /// genuinely taken and the publish fails closed instead of replacing bytes
582    /// another writer owns (D6, review L11).
583    ///
584    /// The whole decision runs under this session's publish gate and is
585    /// revalidated there, so a second publisher of the same id cannot interleave
586    /// between the check and the moves; a name that appeared in the destination in
587    /// between is rejected rather than renamed over.
588    fn publish_prepared_dir(
589        &self,
590        temporary: &Path,
591        final_dir: &Path,
592        session_id: SessionId,
593    ) -> Result<(), SessionError> {
594        let gate = self.publish_gate(session_id);
595        let _publishing = gate.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
596        if !final_dir.exists() {
597            return fs::rename(temporary, final_dir).map_err(|source| SessionError::Io {
598                path: final_dir.to_owned(),
599                source,
600            });
601        }
602        let existing = scaffold_listing(final_dir)?.unwrap_or_default();
603        let merged_scaffold = match existing.as_slice() {
604            [entry] => entry.file_name() == SUBAGENTS_DIR && entry.path().is_dir(),
605            _ => false,
606        };
607        if !final_dir.is_dir() || !merged_scaffold {
608            return Err(SessionError::SessionLocked(session_id));
609        }
610        let prepared = scaffold_listing(temporary)?.ok_or(SessionError::Io {
611            path: temporary.to_owned(),
612            source: std::io::Error::new(
613                std::io::ErrorKind::NotFound,
614                "prepared session directory vanished",
615            ),
616        })?;
617        for entry in &prepared {
618            let destination = final_dir.join(entry.file_name());
619            // Never rename over a name another writer created in the meantime.
620            if destination.symlink_metadata().is_ok() {
621                return Err(SessionError::SessionLocked(session_id));
622            }
623            fs::rename(entry.path(), &destination).map_err(|source| SessionError::Io {
624                path: destination,
625                source,
626            })?;
627        }
628        fs::remove_dir(temporary).map_err(|source| SessionError::Io {
629            path: temporary.to_owned(),
630            source,
631        })?;
632        // The entries that changed live in `final_dir` itself, so that is the
633        // directory to sync before the session becomes visible: syncing the
634        // parent again would prove nothing about the merged files.
635        fsync_directory(final_dir)?;
636        Ok(())
637    }
638
639    /// Process-serial gate for one session's publication.
640    fn publish_gate(&self, session_id: SessionId) -> Arc<Mutex<()>> {
641        let mut gates = self
642            .publish_locks
643            .lock()
644            .unwrap_or_else(|poisoned| poisoned.into_inner());
645        Arc::clone(
646            gates
647                .entry(session_id)
648                .or_insert_with(|| Arc::new(Mutex::new(()))),
649        )
650    }
651
652    /// Directory a new session directory is renamed into (must share the
653    /// filesystem with its temporary sibling).
654    fn publish_parent_for(&self, location: SessionLocation) -> Result<PathBuf, SessionError> {
655        match location {
656            SessionLocation::Root => Ok(self.workdir_dir.clone()),
657            SessionLocation::Child { root } => self.ensure_subagents_dir(root),
658        }
659    }
660
661    /// Ensure a root's `subagents/` directory exists, returning it.
662    fn ensure_subagents_dir(&self, root: SessionId) -> Result<PathBuf, SessionError> {
663        let dir = self.workdir_dir.join(root.to_string()).join(SUBAGENTS_DIR);
664        if !dir.exists() {
665            #[cfg(unix)]
666            create_unix_session_directory_all(&dir)?;
667            #[cfg(windows)]
668            create_windows_session_directory(&dir)?;
669        }
670        Ok(dir)
671    }
672
673    pub fn create(
674        &self,
675        session_id: SessionId,
676        origin: cookie_agent_protocol::EventOrigin,
677        creation: EventPayload,
678    ) -> Result<Arc<EventLog>, SessionError> {
679        self.create_with_status(session_id, origin, creation)
680            .map(|(log, _)| log)
681    }
682
683    /// Creates a session atomically and reports whether this caller won creation.
684    pub fn create_with_status(
685        &self,
686        session_id: SessionId,
687        origin: cookie_agent_protocol::EventOrigin,
688        creation: EventPayload,
689    ) -> Result<(Arc<EventLog>, bool), SessionError> {
690        let _mutation = self.lock_mutation();
691        self.ensure_open()?;
692        if let Some(existing) = self
693            .residency
694            .lock()
695            .unwrap_or_else(|poisoned| poisoned.into_inner())
696            .resident
697            .get(&session_id)
698            .cloned()
699        {
700            return Ok((existing.log, false));
701        }
702        let creation_origin = Self::creation_origin(&creation).unwrap_or(SessionOrigin::Root);
703        let location = self.placement_for(&creation_origin);
704        self.record_location(session_id, location);
705        let final_dir = self.dir_for_placement(location, session_id);
706        if final_dir.exists() {
707            return Err(SessionError::SessionLocked(session_id));
708        }
709        // A new root opens its own tree, unlocked until it publishes. A child
710        // joins its root's tree and writes under that tree's authority, so the
711        // tree has to be held here before the child's log exists at all.
712        let root = location.root_of().unwrap_or(session_id);
713        let (capability, opened_tree) = match location {
714            SessionLocation::Root => {
715                let authority = WriteAuthority::new();
716                (authority.capability(), Some(authority))
717            }
718            SessionLocation::Child { .. } => (self.tree_capability(root, session_id)?, None),
719        };
720        let log = EventLog::create_buffered_owned(
721            final_dir.join(EVENTS_FILE),
722            session_id,
723            origin,
724            creation,
725            capability,
726        )?;
727        let result = projection(log.clone())?;
728        {
729            let mut ownership = self
730                .ownership
731                .lock()
732                .unwrap_or_else(|poisoned| poisoned.into_inner());
733            if let Some(authority) = opened_tree {
734                ownership
735                    .trees
736                    .insert(root, TreeOwnership::PendingPublish { authority });
737            }
738            ownership.writable.insert(session_id, root);
739        }
740        {
741            let mut residency = self
742                .residency
743                .lock()
744                .unwrap_or_else(|poisoned| poisoned.into_inner());
745            residency.resident.insert(session_id, result);
746            residency.evicted.remove(&session_id);
747        }
748        self.note_placed_child(&location, &creation_origin, session_id);
749        Ok((log, true))
750    }
751
752    pub fn get(&self, id: SessionId) -> Result<SessionProjection, SessionError> {
753        // Completing the tree comes first, *before* the resident fast path: a
754        // session created or adopted in this process is resident from a moment
755        // when its root's products did not exist yet, and serving it from the
756        // cache would hide the load that has to run (review L5).
757        self.ensure_tree_for(id)?;
758        if let Some(session) = self.get_resident(id) {
759            return Ok(session);
760        }
761        if self.is_owned(id) {
762            return self.reopen_owned(id);
763        }
764        self.open_snapshot(id, true)
765    }
766
767    /// Read a session without completing its tree first.
768    ///
769    /// Startup passes need a session's own log for bookkeeping; going through
770    /// [`Self::get`] would put child reads back on the startup path (§4.1.1).
771    pub(crate) fn get_log_only(&self, id: SessionId) -> Result<SessionProjection, SessionError> {
772        if let Some(session) = self.get_resident(id) {
773            return Ok(session);
774        }
775        if self.is_owned(id) {
776            return self.reopen_owned(id);
777        }
778        self.open_snapshot(id, true)
779    }
780
781    #[must_use]
782    pub fn get_resident(&self, id: SessionId) -> Option<SessionProjection> {
783        self.residency
784            .lock()
785            .unwrap_or_else(|poisoned| poisoned.into_inner())
786            .resident
787            .get(&id)
788            .cloned()
789    }
790
791    /// Inspects recovery controls without paging an owned dormant session back in.
792    pub(crate) fn recovery_event_snapshot(
793        &self,
794        id: SessionId,
795    ) -> Result<Arc<[cookie_agent_protocol::StoredEvent]>, SessionError> {
796        if let Some(session) = self.get_resident(id) {
797            return Ok(session.log.event_snapshot());
798        }
799        Ok(EventLog::open_read_only(self.resolve_dir(id)?.join(EVENTS_FILE), id)?.event_snapshot())
800    }
801
802    /// Arc clone of the resident log plus its persistence flag, without
803    /// cloning the whole projection. Used by the append hot path, which only
804    /// needs the log and the `first_user_message` gate.
805    fn resident_log(&self, id: SessionId) -> Result<(Arc<EventLog>, bool), SessionError> {
806        let log = self
807            .residency
808            .lock()
809            .unwrap_or_else(|poisoned| poisoned.into_inner())
810            .resident
811            .get(&id)
812            .map(|session| session.log.clone());
813        match log {
814            Some(log) => {
815                let persisted = log.is_persisted();
816                Ok((log, persisted))
817            }
818            None => {
819                let session = self.get(id)?;
820                let persisted = session.log.is_persisted();
821                Ok((session.log, persisted))
822            }
823        }
824    }
825
826    fn reopen_owned(&self, id: SessionId) -> Result<SessionProjection, SessionError> {
827        let _mutation = self.lock_mutation();
828        if let Some(session) = self
829            .residency
830            .lock()
831            .unwrap_or_else(|poisoned| poisoned.into_inner())
832            .resident
833            .get(&id)
834            .cloned()
835        {
836            return Ok(session);
837        }
838        let session_dir = self.resolve_dir(id)?;
839        if !session_dir.is_dir() {
840            return Err(SessionError::Missing(id));
841        }
842        let capability = self.write_capability(id, false)?;
843        self.note_log_open(id);
844        let log = EventLog::open_owned(session_dir.join(EVENTS_FILE), id, capability)?;
845        let reopened = projection(log)?;
846        let mut residency = self
847            .residency
848            .lock()
849            .unwrap_or_else(|poisoned| poisoned.into_inner());
850        residency.resident.insert(id, reopened.clone());
851        residency.evicted.remove(&id);
852        Ok(reopened)
853    }
854
855    /// Reads and folds one session log without making it resident.
856    ///
857    /// `direct` marks a read the caller asked for by session id (a `get` or a
858    /// direct-address child access). Child logs are otherwise only legal inside
859    /// [`Self::load_tree`], and a debug build asserts that invariant so hidden
860    /// child loads show up in CI instead of in startup profiles (§3.2.3, §3.3).
861    fn open_snapshot(
862        &self,
863        id: SessionId,
864        direct: bool,
865    ) -> Result<SessionProjection, SessionError> {
866        debug_assert!(
867            direct || TreeLoadReads::active() || !self.is_filed_child(id),
868            "child log {id} opened outside a tree load"
869        );
870        self.note_log_open(id);
871        let session_dir = self.resolve_dir(id)?;
872        if !session_dir.is_dir() {
873            return Err(SessionError::Missing(id));
874        }
875        let snapshot = projection(EventLog::open_read_only(session_dir.join(EVENTS_FILE), id)?)?;
876        self.residency
877            .lock()
878            .unwrap_or_else(|poisoned| poisoned.into_inner())
879            .evicted
880            .insert(id, summary_from_projection(&snapshot));
881        Ok(snapshot)
882    }
883
884    /// Takes the store's durable-mutation lock, noting the holder on this
885    /// thread. Paths that already hold it must not start a lazy tree load: the
886    /// load's install phase needs the same lock, and waiting for another thread's
887    /// tree load while holding it inverts the two (append, create, fork and
888    /// `begin_write_locked` all reach `get` from inside a mutation).
889    fn lock_mutation(&self) -> MutationGuard<'_> {
890        let store = self.store_key();
891        if MUTATION_DEPTH.with(|depths| depths.borrow().contains_key(&store)) {
892            return MutationGuard {
893                locked: None,
894                store: None,
895            };
896        }
897        let guard = self
898            .mutation
899            .lock()
900            .unwrap_or_else(|poisoned| poisoned.into_inner());
901        MUTATION_DEPTH.with(|depths| depths.borrow_mut().insert(store, ()));
902        MutationGuard {
903            locked: Some(guard),
904            store: Some(store),
905        }
906    }
907
908    /// Identity of this store for the per-store reentrancy counter. Unique while
909    /// the store is alive, which is exactly the window any of its guards span.
910    #[must_use]
911    fn store_key(&self) -> usize {
912        std::ptr::from_ref(self).cast::<()>() as usize
913    }
914
915    #[must_use]
916    fn mutation_held(&self) -> bool {
917        let store = self.store_key();
918        MUTATION_DEPTH.with(|depths| depths.borrow().contains_key(&store))
919    }
920
921    pub(crate) fn begin_write(&self, id: SessionId) -> Result<WriteOpen, SessionError> {
922        // The tree has to be complete before a session in it starts writing, and
923        // the load cannot run under `mutation` (it installs its own).
924        self.ensure_tree_for(id)?;
925        let _mutation = self.lock_mutation();
926        self.ensure_open()?;
927        self.begin_write_locked(id)
928    }
929
930    #[cfg(test)]
931    pub(crate) fn open_for_write(&self, id: SessionId) -> Result<SessionProjection, SessionError> {
932        let adoption_lock = self.adoption_lock(id);
933        let _adoption = adoption_lock
934            .lock()
935            .unwrap_or_else(|poisoned| poisoned.into_inner());
936        if self.begin_write(id)? == WriteOpen::Adopting {
937            self.commit_adoption(id)?;
938        }
939        self.get(id)
940    }
941
942    /// Capability of a tree this process already holds and has settled, for a
943    /// session that is about to write inside it. This is how a child joins its
944    /// root's authority instead of minting one of its own.
945    fn tree_capability(
946        &self,
947        root: SessionId,
948        id: SessionId,
949    ) -> Result<WriteCapability, SessionError> {
950        let ownership = self
951            .ownership
952            .lock()
953            .unwrap_or_else(|poisoned| poisoned.into_inner());
954        match ownership.trees.get(&root) {
955            Some(tree) if tree.is_settled() => Ok(tree
956                .authority()
957                .expect("a settled tree has an authority")
958                .capability()),
959            _ => Err(SessionError::SessionLocked(id)),
960        }
961    }
962
963    /// Makes sure this process holds the tree rooted at `root`, taking its lock
964    /// when nobody holds it yet.
965    ///
966    /// Returns the capability every log in the tree writes under, plus the
967    /// freshly taken lock when this call is the one that took it. The caller
968    /// installs that lock only once the write it was taken for has actually
969    /// succeeded, so a failure drops it and leaves the tree free.
970    ///
971    /// A cached `Foreign` classification is re-checked rather than believed: the
972    /// owner may have exited since, exactly as the per-session classification
973    /// was retried before.
974    #[allow(clippy::type_complexity)]
975    fn hold_tree(
976        &self,
977        root: SessionId,
978        id: SessionId,
979    ) -> Result<(WriteCapability, Option<(HeldLock, WriteAuthority)>), SessionError> {
980        {
981            let ownership = self
982                .ownership
983                .lock()
984                .unwrap_or_else(|poisoned| poisoned.into_inner());
985            match ownership.trees.get(&root) {
986                Some(TreeOwnership::Foreign) | None => {}
987                Some(tree) => {
988                    return Ok((
989                        tree.authority()
990                            .expect("a held tree has an authority")
991                            .capability(),
992                        None,
993                    ));
994                }
995            }
996        }
997        let root_dir = self.tree_root_dir(root);
998        let lock = match try_acquire(&root_dir) {
999            Ok(SessionOwnership::Owned(lock)) => lock,
1000            Ok(SessionOwnership::Foreign) => {
1001                self.mark_tree_foreign(root);
1002                return Err(SessionError::SessionLocked(id));
1003            }
1004            Err(error) => {
1005                eprintln!("session tree {root} ownership classification failed: {error}");
1006                self.mark_tree_foreign(root);
1007                return Err(SessionError::SessionLocked(id));
1008            }
1009        };
1010        let authority = WriteAuthority::new();
1011        Ok((authority.capability(), Some((lock, authority))))
1012    }
1013
1014    /// Records that another process owns this tree. The record is per root, so
1015    /// it answers for every session in the tree at once: none of them is
1016    /// writable, and each reports [`SessionError::SessionLocked`]. A later
1017    /// acquisition still restats the lock, because the owner may have exited.
1018    fn mark_tree_foreign(&self, root: SessionId) {
1019        self.ownership
1020            .lock()
1021            .unwrap_or_else(|poisoned| poisoned.into_inner())
1022            .trees
1023            .insert(root, TreeOwnership::Foreign);
1024    }
1025
1026    fn begin_write_locked(&self, id: SessionId) -> Result<WriteOpen, SessionError> {
1027        if self.is_owned(id) {
1028            if let Some(session) = self.get_resident(id) {
1029                drop(session);
1030                return Ok(WriteOpen::AlreadyOwned);
1031            }
1032            let session_dir = self.resolve_dir(id)?;
1033            let capability = self.write_capability(id, false)?;
1034            let reopened = projection(EventLog::open_owned(
1035                session_dir.join(EVENTS_FILE),
1036                id,
1037                capability,
1038            )?)?;
1039            let mut residency = self
1040                .residency
1041                .lock()
1042                .unwrap_or_else(|poisoned| poisoned.into_inner());
1043            residency.resident.insert(id, reopened.clone());
1044            residency.evicted.remove(&id);
1045            return Ok(WriteOpen::AlreadyOwned);
1046        }
1047        let session_dir = self.resolve_dir(id)?;
1048        if !session_dir.is_dir() {
1049            return Err(SessionError::Missing(id));
1050        }
1051        // Ownership is the tree's, so the lock that has to be free is the
1052        // root's — including when the session being adopted is a child. The
1053        // root itself is *not* reconciled here; it reconciles when it is first
1054        // written, through its own adoption.
1055        let root = self.root_of(id)?;
1056        let (capability, acquired) = self.hold_tree(root, id)?;
1057        let opened = EventLog::open_owned(session_dir.join(EVENTS_FILE), id, capability);
1058        let projection = match opened.and_then(|log| {
1059            projection(log).map_err(|error| match error {
1060                SessionError::Event(error) => error,
1061                _ => unreachable!("projection only returns event-log errors"),
1062            })
1063        }) {
1064            Ok(projection) => projection,
1065            Err(error) => {
1066                // `acquired` drops here: a tree this call locked is released
1067                // again, so the failed adoption leaves nothing behind.
1068                eprintln!("session {id} adoption failed closed: {error}");
1069                return Err(SessionError::SessionLocked(id));
1070            }
1071        };
1072        {
1073            let mut ownership = self
1074                .ownership
1075                .lock()
1076                .unwrap_or_else(|poisoned| poisoned.into_inner());
1077            if let Some((lock, authority)) = acquired {
1078                ownership.trees.insert(
1079                    root,
1080                    TreeOwnership::Adopting {
1081                        _lock: lock,
1082                        authority,
1083                    },
1084                );
1085            }
1086            ownership.adopting.insert(id, root);
1087        }
1088        let mut residency = self
1089            .residency
1090            .lock()
1091            .unwrap_or_else(|poisoned| poisoned.into_inner());
1092        residency.resident.insert(id, projection.clone());
1093        residency.evicted.remove(&id);
1094        Ok(WriteOpen::Adopting)
1095    }
1096
1097    pub(crate) fn commit_adoption(&self, id: SessionId) -> Result<(), SessionError> {
1098        self.ensure_open()?;
1099        let mut ownership = self
1100            .ownership
1101            .lock()
1102            .unwrap_or_else(|poisoned| poisoned.into_inner());
1103        let root = *ownership
1104            .adopting
1105            .get(&id)
1106            .ok_or(SessionError::SessionLocked(id))?;
1107        let tree = ownership
1108            .trees
1109            .remove(&root)
1110            .ok_or(SessionError::SessionLocked(id))?;
1111        match tree {
1112            // The first committed adoption settles the tree; later ones join a
1113            // tree that is already owned or still pending its root's publish.
1114            TreeOwnership::Adopting { _lock, authority } => {
1115                ownership
1116                    .trees
1117                    .insert(root, TreeOwnership::Owned { _lock, authority });
1118            }
1119            TreeOwnership::Foreign => {
1120                ownership.trees.insert(root, TreeOwnership::Foreign);
1121                return Err(SessionError::SessionLocked(id));
1122            }
1123            settled => {
1124                ownership.trees.insert(root, settled);
1125            }
1126        }
1127        ownership.adopting.remove(&id);
1128        ownership.writable.insert(id, root);
1129        Ok(())
1130    }
1131
1132    pub(crate) fn rollback_adoption(&self, id: SessionId) {
1133        let projection = self
1134            .residency
1135            .lock()
1136            .unwrap_or_else(|poisoned| poisoned.into_inner())
1137            .resident
1138            .remove(&id);
1139        if let Some(projection) = projection {
1140            let _ = projection.log.suspend_writer();
1141            self.residency
1142                .lock()
1143                .unwrap_or_else(|poisoned| poisoned.into_inner())
1144                .evicted
1145                .insert(id, summary_from_projection(&projection));
1146        }
1147        let mut ownership = self
1148            .ownership
1149            .lock()
1150            .unwrap_or_else(|poisoned| poisoned.into_inner());
1151        let removed = ownership.adopting.remove(&id);
1152        // A tree locked *for* this adoption and never committed by anything
1153        // else goes back to unowned, so another process can adopt it.
1154        if let Some(root) = removed
1155            && matches!(
1156                ownership.trees.get(&root),
1157                Some(TreeOwnership::Adopting { .. })
1158            )
1159            && !ownership.tree_is_referenced(root)
1160        {
1161            ownership.trees.remove(&root);
1162        }
1163        debug_assert!(removed.is_some() || self.closed.load(Ordering::Acquire));
1164    }
1165
1166    /// Adoption gate for `id`'s *tree*. One tree lock means one adoption at a
1167    /// time in a tree; an id that cannot be located keeps a gate of its own,
1168    /// because its `begin_write` fails before it reaches any lock.
1169    pub(crate) fn adoption_lock(&self, id: SessionId) -> Arc<Mutex<()>> {
1170        let root = self.root_of(id).unwrap_or(id);
1171        self.adoption_locks
1172            .lock()
1173            .unwrap_or_else(|poisoned| poisoned.into_inner())
1174            .entry(root)
1175            .or_insert_with(|| Arc::new(Mutex::new(())))
1176            .clone()
1177    }
1178
1179    fn write_capability(
1180        &self,
1181        id: SessionId,
1182        allow_adopting: bool,
1183    ) -> Result<WriteCapability, SessionError> {
1184        self.ownership
1185            .lock()
1186            .unwrap_or_else(|poisoned| poisoned.into_inner())
1187            .capability(id, allow_adopting)
1188    }
1189
1190    /// Whether this process may write `id`: its tree is held and settled here,
1191    /// and `id` itself was created or adopted in this process. A foreign root
1192    /// therefore answers `false` for every session in its tree.
1193    #[must_use]
1194    pub fn is_owned(&self, id: SessionId) -> bool {
1195        let ownership = self
1196            .ownership
1197            .lock()
1198            .unwrap_or_else(|poisoned| poisoned.into_inner());
1199        ownership.writable.get(&id).is_some_and(|root| {
1200            ownership
1201                .trees
1202                .get(root)
1203                .is_some_and(TreeOwnership::is_settled)
1204        })
1205    }
1206
1207    pub fn evict(&self, id: SessionId) -> Result<bool, SessionError> {
1208        let _mutation = self.lock_mutation();
1209        // Persisted child caches are refreshed after the residency guard drops.
1210        let parent_root = self.parent_root_of(id);
1211        let evicted = self.evict_locked(id)?;
1212        if evicted && let Some(root) = parent_root {
1213            self.persist_subagent_index(root)?;
1214        }
1215        Ok(evicted)
1216    }
1217
1218    /// The root a session is filed under, from the location cache only (never
1219    /// resolves paths, so it is safe to call while store locks are held).
1220    fn parent_root_of(&self, id: SessionId) -> Option<SessionId> {
1221        match self.cached_location(id) {
1222            Some(SessionLocation::Child { root }) => Some(root),
1223            Some(SessionLocation::Root) | None => match self.cached_origin(id) {
1224                Some(SessionOrigin::Delegated {
1225                    root_session_id, ..
1226                }) => Some(root_session_id),
1227                _ => None,
1228            },
1229        }
1230    }
1231
1232    fn evict_locked(&self, id: SessionId) -> Result<bool, SessionError> {
1233        let mut residency = self
1234            .residency
1235            .lock()
1236            .unwrap_or_else(|poisoned| poisoned.into_inner());
1237        let Some(session) = residency.resident.get(&id) else {
1238            return Ok(false);
1239        };
1240        if !session.log.is_persisted() {
1241            return Ok(false);
1242        }
1243        // Stream records may be published before their grouped sync. Flush before
1244        // removing the resident projection so eviction never outruns durability.
1245        session.log.flush()?;
1246        let summary = SessionSummary {
1247            meta: session.meta.clone(),
1248            usage: session.usage.clone(),
1249            usage_rollup: session.usage_rollup.clone(),
1250            agent_usage: session.agent_usage.clone(),
1251        };
1252        residency.evicted.insert(id, summary);
1253        #[cfg(test)]
1254        if let Some(hook) = self
1255            .eviction_transition_hook
1256            .lock()
1257            .expect("eviction transition hook lock poisoned")
1258            .take()
1259        {
1260            if let Some(reached) = hook
1261                .reached
1262                .lock()
1263                .expect("eviction transition reached lock poisoned")
1264                .take()
1265            {
1266                let _ = reached.send(id);
1267            }
1268            let _ = hook
1269                .release
1270                .lock()
1271                .expect("eviction transition release lock poisoned")
1272                .recv();
1273        }
1274        residency.resident.remove(&id);
1275        Ok(true)
1276    }
1277
1278    pub fn append(
1279        &self,
1280        id: SessionId,
1281        run: Option<RunId>,
1282        origin: cookie_agent_protocol::EventOrigin,
1283        event: EventPayload,
1284    ) -> Result<cookie_agent_protocol::StoredEvent, SessionError> {
1285        self.append_with_mode(id, run, origin, event, false)
1286    }
1287
1288    pub(crate) fn append_recovery(
1289        &self,
1290        id: SessionId,
1291        run: Option<RunId>,
1292        origin: cookie_agent_protocol::EventOrigin,
1293        event: EventPayload,
1294    ) -> Result<cookie_agent_protocol::StoredEvent, SessionError> {
1295        self.append_with_mode(id, run, origin, event, true)
1296    }
1297
1298    fn append_with_mode(
1299        &self,
1300        id: SessionId,
1301        run: Option<RunId>,
1302        origin: cookie_agent_protocol::EventOrigin,
1303        event: EventPayload,
1304        recovery: bool,
1305    ) -> Result<cookie_agent_protocol::StoredEvent, SessionError> {
1306        // §3.2: the tree must be complete before a session writes into it, and
1307        // the load cannot run under `mutation` (it installs its own), so it runs
1308        // here instead. Cheap once the tree is Loaded.
1309        self.ensure_tree_for(id)?;
1310        let _mutation = self.lock_mutation();
1311        self.ensure_open()?;
1312        let capability = self.write_capability(id, recovery)?;
1313        let (log, was_persisted) = self.resident_log(id)?;
1314        let first_user_message = !was_persisted
1315            && matches!(
1316                event,
1317                EventPayload::UserInputAdmitted { .. }
1318                    | EventPayload::GoalActivated { .. }
1319                    | EventPayload::ProducerMessageAccepted { .. }
1320                    | EventPayload::UserInputSubmitted { .. }
1321                    | EventPayload::DelegatedContextSeeded { .. }
1322                    | EventPayload::SessionPermissionOverlaySet { .. }
1323                    | EventPayload::SkillLoaded { .. }
1324                    | EventPayload::AgentMdLoaded { .. }
1325            );
1326        let envelope = log.append_owned(&capability, run, origin, event)?;
1327        // Fold-ignored payloads (the per-token TextDelta/ReasoningDelta and
1328        // per-chunk ToolCallProgress hot path) only advance the metadata tip;
1329        // update the resident projection in place instead of re-folding the
1330        // whole log. The resident is updated before write_cache — on a cache
1331        // write failure the resident stays consistent with the log while the
1332        // on-disk discovery cache lags, which is strictly better than the
1333        // previous resident-behind-log window.
1334        let incremental_meta = if fold_consumed(&envelope.payload) {
1335            None
1336        } else {
1337            let mut residency = self
1338                .residency
1339                .lock()
1340                .unwrap_or_else(|poisoned| poisoned.into_inner());
1341            if residency.resident.contains_key(&id) {
1342                residency.evicted.remove(&id);
1343                let resident = residency.resident.get_mut(&id).expect("checked above");
1344                resident.meta.last_event_seq = envelope.seq;
1345                resident.meta.last_activity = envelope.timestamp;
1346                Some(resident.meta.clone())
1347            } else {
1348                None
1349            }
1350        };
1351        if let Some(meta) = incremental_meta {
1352            if first_user_message {
1353                let projection = self.get(id)?;
1354                self.persist_buffered(id, &projection)?;
1355            } else if log.is_persisted() {
1356                write_cache(&self.meta_cache_path(id)?, &meta)?;
1357            }
1358        } else {
1359            let rebuilt = projection(log.clone())?;
1360            if first_user_message {
1361                self.persist_buffered(id, &rebuilt)?;
1362            } else if log.is_persisted() {
1363                write_cache(&self.meta_cache_path(id)?, &rebuilt.meta)?;
1364            }
1365            {
1366                let mut residency = self
1367                    .residency
1368                    .lock()
1369                    .unwrap_or_else(|poisoned| poisoned.into_inner());
1370                residency.resident.insert(id, rebuilt);
1371                residency.evicted.remove(&id);
1372            }
1373            // The publish that just happened changed the child's durable summary.
1374            if !was_persisted && let Some(root) = self.parent_root_of(id) {
1375                self.persist_subagent_index(root)?;
1376            }
1377        }
1378        // Run-terminal events are rare and are the only thing the delegation
1379        // registry needs from a child log, so cache them per tree (§4.1.3).
1380        if let Some(terminal) = terminal_run_of(run, &envelope.payload) {
1381            self.record_terminal_run(id, terminal.0, terminal.1)?;
1382        }
1383        #[cfg(test)]
1384        {
1385            let reference = projection_fold(log).expect("reference fold");
1386            let resident = self.get_resident(id).expect("resident after append");
1387            assert_projection_equivalent(&resident, &reference);
1388        }
1389        self.publish_stored_event(&envelope);
1390        Ok(envelope)
1391    }
1392
1393    pub(crate) fn subscribe_events(
1394        &self,
1395        session: SessionId,
1396        cursor: Option<u64>,
1397    ) -> Result<
1398        (
1399            EventsSubscribeResult,
1400            mpsc::Receiver<EventSubscriptionMessage>,
1401        ),
1402        SessionError,
1403    > {
1404        self.ensure_tree_for(session)?;
1405        // Snapshot and registration share the append lock with actor writes and
1406        // direct journal writes, closing the snapshot-to-live handoff gap.
1407        let _mutation = self.lock_mutation();
1408        let events = self
1409            .get(session)?
1410            .log
1411            .all_events()
1412            .into_iter()
1413            .filter(|event| cursor.is_none_or(|cursor| event.seq > cursor))
1414            .collect();
1415        let (sender, receiver) = mpsc::channel(PERSISTED_SUBSCRIBER_QUEUE_CAPACITY);
1416        self.subscribers
1417            .lock()
1418            .unwrap_or_else(|p| p.into_inner())
1419            .entry(session)
1420            .or_default()
1421            .push(sender);
1422        Ok((EventsSubscribeResult { events }, receiver))
1423    }
1424
1425    fn publish_stored_event(&self, envelope: &StoredEvent) {
1426        // Called under mutation after projection update, preserving append order.
1427        self.subscribers
1428            .lock()
1429            .unwrap_or_else(|p| p.into_inner())
1430            .entry(envelope.session_id)
1431            .or_default()
1432            .retain(|sender| {
1433                // Reserve the final slot for a gap so a slow reader can replay.
1434                let is_gap = sender.capacity() <= 1;
1435                let message = if is_gap {
1436                    EventSubscriptionMessage::Gap {
1437                        session_id: envelope.session_id,
1438                        last_delivered_seq: envelope.seq.saturating_sub(1),
1439                    }
1440                } else {
1441                    EventSubscriptionMessage::Event {
1442                        event: Box::new(envelope.clone()),
1443                    }
1444                };
1445                sender.try_send(message).is_ok() && !is_gap
1446            });
1447    }
1448
1449    pub(crate) fn notify_evicted_subscribers(&self, session_id: SessionId, last_event_seq: u64) {
1450        let subscribers = self
1451            .subscribers
1452            .lock()
1453            .unwrap_or_else(|p| p.into_inner())
1454            .remove(&session_id)
1455            .unwrap_or_default();
1456        for sender in subscribers {
1457            // publish_stored_event always leaves a slot for this final gap.
1458            let _ = sender.try_send(EventSubscriptionMessage::Gap {
1459                session_id,
1460                last_delivered_seq: last_event_seq,
1461            });
1462        }
1463    }
1464
1465    pub fn fork(
1466        &self,
1467        source_id: SessionId,
1468        through_seq: u64,
1469        origin: cookie_agent_protocol::EventOrigin,
1470    ) -> Result<SessionId, SessionError> {
1471        // A fork reads its source's tree and files the new session into it, so
1472        // the tree has to be complete *before* `mutation` is taken: a load
1473        // installs under that lock and must never start while it is held.
1474        // Forking a cold child otherwise files it into an unflattened tree.
1475        if self.tree_load_pending_for(source_id) {
1476            self.ensure_tree_for(source_id)?;
1477        }
1478        let _mutation = self.lock_mutation();
1479        self.ensure_open()?;
1480        let source = self.get(source_id)?;
1481        if !source.log.is_persisted() {
1482            return Err(SessionError::InvalidSequence {
1483                session_id: source_id,
1484                through_seq,
1485            });
1486        }
1487        source.log.suspend_writer()?;
1488        let source_events = source.log.all_events();
1489        let prefix = source_events
1490            .iter()
1491            .filter(|event| event.seq <= through_seq)
1492            .cloned()
1493            .collect::<Vec<_>>();
1494        if through_seq == 0
1495            || through_seq > source_events.last().map_or(0, |event| event.seq)
1496            || !cookie_agent_protocol::visible_events(&prefix)
1497                .iter()
1498                .any(|event| matches!(event.payload, EventPayload::UserInputSubmitted { .. }))
1499        {
1500            return Err(SessionError::InvalidSequence {
1501                session_id: source_id,
1502                through_seq,
1503            });
1504        }
1505
1506        let session_id = SessionId::new_v7();
1507        // A forked origin is copied verbatim from the source's `SessionCreated`
1508        // event, so a fork of a child stays inside the same tree.
1509        let location = self.placement_for(&source.meta.origin);
1510        let root = location.root_of().unwrap_or(session_id);
1511        // A forked root opens a tree of its own, locked when it publishes. A
1512        // forked child is prepared and published *inside* its root's directory,
1513        // so the root's lock has to be this process's before a byte is written
1514        // there — including the temporary directory below. Nobody holding it is
1515        // the cold-store fork: take it now, the way an adoption through a child
1516        // would.
1517        let (capability, fork_tree) = match location {
1518            SessionLocation::Root => {
1519                let authority = WriteAuthority::new();
1520                (authority.capability(), ForkTree::NewRoot(authority))
1521            }
1522            SessionLocation::Child { .. } => {
1523                let (capability, acquired) = self.hold_tree(root, session_id)?;
1524                (capability, ForkTree::Joined(acquired))
1525            }
1526        };
1527        self.record_location(session_id, location);
1528        let destination_parent = self.publish_parent_for(location)?;
1529        let final_dir = self.dir_for_placement(location, session_id);
1530        let temporary =
1531            destination_parent.join(format!(".{session_id}.{}.tmp", SessionId::new_v7()));
1532        #[cfg(unix)]
1533        fs::create_dir(&temporary).map_err(|source| SessionError::Io {
1534            path: temporary.clone(),
1535            source,
1536        })?;
1537        #[cfg(windows)]
1538        create_windows_session_directory(&temporary)?;
1539        #[cfg(unix)]
1540        fs::set_permissions(&temporary, fs::Permissions::from_mode(0o700)).map_err(|source| {
1541            SessionError::Io {
1542                path: temporary.clone(),
1543                source,
1544            }
1545        })?;
1546        let result = (|| {
1547            let log_path = temporary.join("events.jsonl");
1548            #[cfg(windows)]
1549            create_windows_session_file(&log_path)?;
1550            for event in source_events
1551                .iter()
1552                .filter(|event| event.seq <= through_seq)
1553            {
1554                let mut copied = event.clone();
1555                copied.session_id = session_id;
1556                crate::events::append_copied_event_jsonl(&log_path, &copied)?;
1557            }
1558            #[cfg(unix)]
1559            fs::set_permissions(&log_path, fs::Permissions::from_mode(0o600)).map_err(
1560                |source| SessionError::Io {
1561                    path: log_path.clone(),
1562                    source,
1563                },
1564            )?;
1565            let log = EventLog::open_owned(log_path, session_id, capability.clone())?;
1566            log.append_owned(
1567                &capability,
1568                None,
1569                origin,
1570                EventPayload::SessionReverted { through_seq },
1571            )?;
1572            let prefix_projection = projection(log.clone())?;
1573            let title = fork_title(prefix_projection.meta.title.as_ref())?;
1574            log.append_owned(
1575                &capability,
1576                None,
1577                cookie_agent_protocol::EventOrigin::new("user")
1578                    .expect("static event origin is valid"),
1579                EventPayload::SessionTitleCommitted {
1580                    change: SessionTitleChange::UserSet {
1581                        title,
1582                        client_rename_id: ClientRenameId::new(format!("fork-{session_id}"))
1583                            .expect("fork rename ID is bounded"),
1584                    },
1585                    input_through_seq: through_seq,
1586                },
1587            )?;
1588            log.suspend_writer()?;
1589            let fork_projection = projection(log)?;
1590            write_cache(&temporary.join(SESSION_META_FILE), &fork_projection.meta)?;
1591            // Only a root opens a tree, and only a root takes a lock: on unix
1592            // inside the temporary directory that is about to become the root,
1593            // on Windows from the sidecar path derived from the final one. A
1594            // forked child publishes into a tree this process already holds.
1595            #[cfg(unix)]
1596            let lock_root_dir = &temporary;
1597            #[cfg(windows)]
1598            let lock_root_dir = &final_dir;
1599            let published_tree = match fork_tree {
1600                ForkTree::NewRoot(authority) => {
1601                    let lock =
1602                        match try_acquire(lock_root_dir).map_err(|source| SessionError::Io {
1603                            path: owner_lock_path(lock_root_dir),
1604                            source,
1605                        })? {
1606                            SessionOwnership::Owned(lock) => lock,
1607                            SessionOwnership::Foreign => {
1608                                return Err(SessionError::SessionLocked(session_id));
1609                            }
1610                        };
1611                    Some(TreeOwnership::Owned {
1612                        _lock: lock,
1613                        authority,
1614                    })
1615                }
1616                ForkTree::Joined(Some((lock, authority))) => Some(TreeOwnership::Owned {
1617                    _lock: lock,
1618                    authority,
1619                }),
1620                ForkTree::Joined(None) => None,
1621            };
1622            #[cfg(test)]
1623            if let Some(hook) = self
1624                .publish_hook
1625                .lock()
1626                .expect("publish hook lock poisoned")
1627                .take()
1628            {
1629                let _ = hook.reached.send(session_id);
1630                let _ = hook.release.recv();
1631            }
1632            fsync_directory(&temporary)?;
1633            self.publish_prepared_dir(&temporary, &final_dir, session_id)?;
1634            fsync_directory(&destination_parent)?;
1635            {
1636                let mut ownership = self
1637                    .ownership
1638                    .lock()
1639                    .unwrap_or_else(|poisoned| poisoned.into_inner());
1640                match published_tree {
1641                    Some(tree) => {
1642                        ownership.trees.insert(root, tree);
1643                    }
1644                    None => match ownership.trees.get(&root) {
1645                        Some(tree) if tree.is_settled() => {}
1646                        _ => return Err(SessionError::SessionLocked(session_id)),
1647                    },
1648                }
1649                ownership.writable.insert(session_id, root);
1650            }
1651            let log = EventLog::open_owned(final_dir.join("events.jsonl"), session_id, capability)?;
1652            let fork_projection = projection(log)?;
1653            let fork_origin = fork_projection.meta.origin.clone();
1654            self.residency
1655                .lock()
1656                .unwrap_or_else(|poisoned| poisoned.into_inner())
1657                .resident
1658                .insert(session_id, fork_projection);
1659            self.note_placed_child(&location, &fork_origin, session_id);
1660            if let SessionLocation::Child { root } = location {
1661                self.persist_subagent_index(root)?;
1662            }
1663            Ok(session_id)
1664        })();
1665        if result.is_err() {
1666            let _ = fs::remove_dir_all(&temporary);
1667        }
1668        result
1669    }
1670
1671    fn persist_buffered(
1672        &self,
1673        session_id: SessionId,
1674        projection: &SessionProjection,
1675    ) -> Result<(), SessionError> {
1676        self.write_capability(session_id, false)?;
1677        let location = self.placement_for(&projection.meta.origin);
1678        // Only a root's publication takes a lock; a child publishes into a tree
1679        // this process already holds, which `write_capability` just proved.
1680        let root = location.root_of().unwrap_or(session_id);
1681        self.record_location(session_id, location);
1682        self.note_placed_child(&location, &projection.meta.origin, session_id);
1683        let destination_parent = self.publish_parent_for(location)?;
1684        let final_dir = self.dir_for_placement(location, session_id);
1685        let temporary =
1686            destination_parent.join(format!(".{session_id}.{}.tmp", SessionId::new_v7()));
1687        #[cfg(unix)]
1688        create_unix_session_directory_all(&temporary)?;
1689        #[cfg(windows)]
1690        create_windows_session_directory(&temporary)?;
1691        let result = (|| {
1692            let log_path = temporary.join("events.jsonl");
1693            #[cfg(windows)]
1694            create_windows_session_file(&log_path)?;
1695            for event in projection.log.all_events() {
1696                crate::events::append_jsonl(&log_path, &event)?;
1697            }
1698            write_cache(&temporary.join(SESSION_META_FILE), &projection.meta)?;
1699            #[cfg(unix)]
1700            let lock_root_dir = &temporary;
1701            #[cfg(windows)]
1702            let lock_root_dir = &final_dir;
1703            let lock = match location {
1704                SessionLocation::Root => {
1705                    match try_acquire(lock_root_dir).map_err(|source| SessionError::Io {
1706                        path: owner_lock_path(lock_root_dir),
1707                        source,
1708                    })? {
1709                        SessionOwnership::Owned(lock) => Some(lock),
1710                        SessionOwnership::Foreign => {
1711                            return Err(SessionError::SessionLocked(session_id));
1712                        }
1713                    }
1714                }
1715                SessionLocation::Child { .. } => None,
1716            };
1717            #[cfg(test)]
1718            if let Some(hook) = self
1719                .publish_hook
1720                .lock()
1721                .expect("publish hook lock poisoned")
1722                .take()
1723            {
1724                let _ = hook.reached.send(session_id);
1725                let _ = hook.release.recv();
1726            }
1727            fsync_directory(&temporary)?;
1728            self.publish_prepared_dir(&temporary, &final_dir, session_id)?;
1729            fsync_directory(&destination_parent)?;
1730            let mut ownership = self
1731                .ownership
1732                .lock()
1733                .unwrap_or_else(|poisoned| poisoned.into_inner());
1734            match lock {
1735                // The root's own publication settles its tree, carrying the
1736                // authority its children are already writing under.
1737                Some(lock) => {
1738                    let state = ownership
1739                        .trees
1740                        .remove(&root)
1741                        .ok_or(SessionError::SessionLocked(session_id))?;
1742                    match state {
1743                        TreeOwnership::PendingPublish { authority } => {
1744                            ownership.trees.insert(
1745                                root,
1746                                TreeOwnership::Owned {
1747                                    _lock: lock,
1748                                    authority,
1749                                },
1750                            );
1751                        }
1752                        state => {
1753                            ownership.trees.insert(root, state);
1754                            return Err(SessionError::SessionLocked(session_id));
1755                        }
1756                    }
1757                }
1758                None => match ownership.trees.get(&root) {
1759                    Some(tree) if tree.is_settled() => {}
1760                    _ => return Err(SessionError::SessionLocked(session_id)),
1761                },
1762            }
1763            Ok::<(), SessionError>(())
1764        })();
1765        if result.is_err() {
1766            let _ = fs::remove_dir_all(&temporary);
1767            return result;
1768        }
1769        projection.log.mark_persisted();
1770        // The child directory is durable now, so the cache may name it (§3.4).
1771        if let SessionLocation::Child { root } = location {
1772            self.persist_subagent_index(root)?;
1773        }
1774        Ok(())
1775    }
1776
1777    pub(crate) fn persist_buffered_session(&self, id: SessionId) -> Result<(), SessionError> {
1778        // Publishing a buffered session makes it a member of its tree, so the
1779        // tree has to be flat before `mutation` is taken — a cold root would
1780        // otherwise stay unflattened with a child filed into it, and a load
1781        // cannot run under the lock its install phase needs (§3.2).
1782        if self.tree_load_pending_for(id) {
1783            self.ensure_tree_for(id)?;
1784        }
1785        let _mutation = self.lock_mutation();
1786        self.ensure_open()?;
1787        let projection = self.get(id)?;
1788        if !projection.log.is_persisted() {
1789            self.persist_buffered(id, &projection)?;
1790        }
1791        Ok(())
1792    }
1793
1794    #[must_use]
1795    pub fn all(&self) -> Vec<SessionProjection> {
1796        self.residency
1797            .lock()
1798            .unwrap_or_else(|poisoned| poisoned.into_inner())
1799            .resident
1800            .values()
1801            .cloned()
1802            .collect()
1803    }
1804
1805    /// Snapshots of the sessions that are roots of their own tree. This is the
1806    /// startup pass shape: never touches delegated child logs and never triggers
1807    /// a lazy tree load (§3.2, §4).
1808    pub fn root_snapshots(&self) -> Vec<SessionProjection> {
1809        self.refresh_discovered();
1810        let ids = self
1811            .residency
1812            .lock()
1813            .unwrap_or_else(|poisoned| poisoned.into_inner())
1814            .known_ids()
1815            .into_iter()
1816            .filter(|id| self.is_root_id(*id))
1817            .collect::<Vec<_>>();
1818
1819        self.startup_snapshots_for(ids)
1820    }
1821
1822    /// Startup bookkeeping reads only: root summaries, the delegation registry,
1823    /// the approval rebuild and the producer install (§4). A child read here
1824    /// would land outside the coalesced bulk pass, which is why the use-path
1825    /// APIs go through [`Self::ensure_tree_for`] instead.
1826    fn startup_snapshots_for(&self, ids: Vec<SessionId>) -> Vec<SessionProjection> {
1827        ids.into_iter()
1828            .filter_map(|id| match self.startup_snapshot(id) {
1829                Ok(session) => Some(session),
1830                Err(error) => {
1831                    eprintln!("session {id} snapshot skipped: {error}");
1832                    None
1833                }
1834            })
1835            .collect()
1836    }
1837
1838    /// Reads one session *without* completing its tree.
1839    ///
1840    /// Legal for startup bookkeeping and for direct-address access the caller
1841    /// has already proven; anything else wants [`Self::get`], which refuses to
1842    /// serve a session whose tree is still incomplete.
1843    fn startup_snapshot(&self, id: SessionId) -> Result<SessionProjection, SessionError> {
1844        if let Some(session) = self.get_resident(id) {
1845            return Ok(session);
1846        }
1847        self.open_snapshot(id, false)
1848    }
1849
1850    /// Whether a session is filed inside another session's tree.
1851    fn is_filed_child(&self, id: SessionId) -> bool {
1852        matches!(
1853            self.cached_location(id),
1854            Some(SessionLocation::Child { .. })
1855        )
1856    }
1857
1858    /// Existence check that never opens an event log: the location cache and, for
1859    /// unknown ids, a directory probe.
1860    pub(crate) fn session_exists(&self, id: SessionId) -> bool {
1861        self.resolve_dir(id).is_ok()
1862    }
1863
1864    fn is_root_id(&self, id: SessionId) -> bool {
1865        !matches!(
1866            self.cached_location(id),
1867            Some(SessionLocation::Child { .. })
1868        )
1869    }
1870
1871    fn cached_origin(&self, id: SessionId) -> Option<SessionOrigin> {
1872        let residency = self
1873            .residency
1874            .lock()
1875            .unwrap_or_else(|poisoned| poisoned.into_inner());
1876        if let Some(session) = residency.resident.get(&id) {
1877            return Some(session.meta.origin.clone());
1878        }
1879        residency
1880            .evicted
1881            .get(&id)
1882            .map(|summary| summary.meta.origin.clone())
1883    }
1884
1885    #[must_use]
1886    pub fn all_summaries(&self) -> Vec<SessionSummary> {
1887        self.refresh_discovered();
1888        let residency = self
1889            .residency
1890            .lock()
1891            .unwrap_or_else(|poisoned| poisoned.into_inner());
1892        let mut summaries = residency.evicted.clone();
1893        summaries.extend(residency.resident.iter().map(|(session_id, session)| {
1894            (
1895                *session_id,
1896                SessionSummary {
1897                    meta: session.meta.clone(),
1898                    usage: session.usage.clone(),
1899                    usage_rollup: session.usage_rollup.clone(),
1900                    agent_usage: session.agent_usage.clone(),
1901                },
1902            )
1903        }));
1904        summaries.into_values().collect()
1905    }
1906
1907    /// One session's summary: a resident projection, a cached summary the store
1908    /// can prove is current, or — after completing the owning root's tree — the
1909    /// answer the bulk pass installed (§2.4, §3.2).
1910    ///
1911    /// A summary that cannot be proven current is *pre-load* data: the persisted
1912    /// `subagents/index.json` is a cache, and the one thing that proves a cached
1913    /// entry has fallen behind is a disagreement with the session's own
1914    /// authoritative `metadata` file. Serving it anyway is how a usage query ends
1915    /// up reporting an incomplete tree (`session_usage` and
1916    /// `Engine::session_tree_usage` both answer from this method), so the tree is
1917    /// completed first. The hot path stays lock-light: an already-loaded tree
1918    /// costs one placement probe and one tree-state probe, no filesystem access,
1919    /// and a caller holding `mutation` never starts a load here (§3.2, review F2).
1920    pub fn summary(&self, id: SessionId) -> Result<SessionSummary, SessionError> {
1921        if self.summary_cache_is_current(id)
1922            && let Some(summary) = self.cached_summary(id)
1923        {
1924            return Ok(summary);
1925        }
1926        self.ensure_tree_for(id)?;
1927        if self.summary_cache_is_current(id)
1928            && let Some(summary) = self.cached_summary(id)
1929        {
1930            return Ok(summary);
1931        }
1932        self.get(id)
1933            .map(|session| summary_from_projection(&session))
1934    }
1935
1936    /// Whether the in-memory summary of `id` may be served *without* completing
1937    /// its tree: either the pass has run, so the cache holds what it installed, or
1938    /// the seed it holds still agrees with the session's authoritative `metadata`
1939    /// cache (§2.3, §3.4).
1940    fn summary_cache_is_current(&self, id: SessionId) -> bool {
1941        let root = match self.cached_location(id) {
1942            Some(SessionLocation::Child { root }) => root,
1943            // A root — or a session whose placement this store has not resolved
1944            // yet, in which case the tree in question can only be its own.
1945            Some(SessionLocation::Root) | None => id,
1946        };
1947        let trees = self
1948            .trees
1949            .lock()
1950            .unwrap_or_else(|poisoned| poisoned.into_inner());
1951        match trees.get(&root) {
1952            // Nothing was ever recorded about this tree, so nothing cached in it
1953            // has been proven against a log.
1954            None => false,
1955            Some(state) => state.loaded || !state.stale_seeds.contains(&id),
1956        }
1957    }
1958
1959    /// Root discovery refresh. Metadata caches only — never an event log.
1960    fn refresh_discovered(&self) {
1961        self.refresh_discovered_roots();
1962    }
1963
1964    /// Whether `id` already has a residency entry (resident or evicted).
1965    fn is_known(&self, id: SessionId) -> bool {
1966        let residency = self
1967            .residency
1968            .lock()
1969            .unwrap_or_else(|poisoned| poisoned.into_inner());
1970        residency.resident.contains_key(&id) || residency.evicted.contains_key(&id)
1971    }
1972
1973    /// Installs an evicted summary without ever displacing fresher in-memory
1974    /// state (resident projections or an already-cached summary).
1975    fn cache_summary(&self, id: SessionId, summary: SessionSummary) {
1976        let mut residency = self
1977            .residency
1978            .lock()
1979            .unwrap_or_else(|poisoned| poisoned.into_inner());
1980        if residency.resident.contains_key(&id) {
1981            return;
1982        }
1983        residency.evicted.entry(id).or_insert(summary);
1984    }
1985
1986    /// v2 root-only discovery (§2.3): root `metadata` caches plus each root's
1987    /// tiny `subagents/index.json`. Directory placement encodes root-ness, so
1988    /// no `origin` check (and no child event log) is needed.
1989    fn refresh_discovered_roots(&self) {
1990        let roots = self.root_dir_ids();
1991        for root in roots.iter().copied() {
1992            let dir = self.workdir_dir.join(root.to_string());
1993            if !self.is_known(root) {
1994                // Invalid entries stay uncached so later discovery retries them
1995                // and repeats the diagnostic (today's semantics).
1996                match read_cache(&meta_path(&dir), &dir.join(EVENTS_FILE)) {
1997                    Ok(meta) if meta.session_id == root => {
1998                        // Root logs are part of startup discovery. Folding them
1999                        // here initializes usage while retaining the root-only
2000                        // invariant: child logs are never opened at startup.
2001                        match self.startup_snapshot(root) {
2002                            Ok(snapshot) => {
2003                                self.cache_summary(root, summary_from_projection(&snapshot))
2004                            }
2005                            Err(error) => eprintln!("session {root} usage skipped: {error}"),
2006                        }
2007                    }
2008                    Ok(_) => eprintln!("session {root} metadata ID does not match its directory"),
2009                    // A root directory without a `metadata` cache is a transient
2010                    // discovery state, not a fault. It is what discovery sees
2011                    // while a bare `<root>/subagents/` scaffold waits for its
2012                    // root to publish, while `publish_prepared_dir` merges a
2013                    // prepared directory into that scaffold, and, on filesystems
2014                    // without an atomic superseding replace, while the cache
2015                    // itself is rewritten. Skipping silently leaves the root
2016                    // uncached, so the next discovery retries it.
2017                    Err(SessionError::Io { source, .. })
2018                        if source.kind() == std::io::ErrorKind::NotFound =>
2019                    {
2020                        continue;
2021                    }
2022                    Err(error) => {
2023                        eprintln!("session {root} metadata skipped: {error}");
2024                        continue;
2025                    }
2026                }
2027            }
2028            self.record_location(root, SessionLocation::Root);
2029        }
2030        // Only once every root's placement is known: an `index.json` naming
2031        // another root must be rejected as the foreign entry it is (review L9).
2032        for root in roots {
2033            self.seed_tree_from_index(root);
2034        }
2035    }
2036
2037    /// Where the descendants of a *root* `id` live (one flat level, whatever the
2038    /// depth). Descendants are always filed under the root's own directory, so
2039    /// this needs no placement lookup.
2040    fn subagents_dir(&self, root: SessionId) -> PathBuf {
2041        self.path_for(SessionLocation::Root, root)
2042            .join(SUBAGENTS_DIR)
2043    }
2044
2045    fn subagent_index_path(&self, root: SessionId) -> PathBuf {
2046        self.subagents_dir(root).join(SUBAGENT_INDEX_FILE)
2047    }
2048
2049    fn read_subagent_index(&self, root: SessionId) -> Option<SubagentIndex> {
2050        let bytes = fs::read(self.subagent_index_path(root)).ok()?;
2051        let index = serde_json::from_slice::<SubagentIndex>(&bytes).ok()?;
2052        (index.version == SUBAGENT_INDEX_VERSION).then_some(index)
2053    }
2054
2055    /// Every descendant filed under `root`, at any depth, without opening a log:
2056    /// the seeded tree state plus the location cache. All descendants share one
2057    /// directory level, so depth comes from `origin`, not from nesting.
2058    fn tree_members(&self, root: SessionId) -> Vec<SessionId> {
2059        let mut members = self
2060            .trees
2061            .lock()
2062            .unwrap_or_else(|poisoned| poisoned.into_inner())
2063            .get(&root)
2064            .map(|state| {
2065                state
2066                    .children
2067                    .values()
2068                    .flatten()
2069                    .copied()
2070                    .collect::<Vec<_>>()
2071            })
2072            .unwrap_or_default();
2073        let located = {
2074            let locations = self
2075                .locations
2076                .lock()
2077                .unwrap_or_else(|poisoned| poisoned.into_inner());
2078            locations
2079                .iter()
2080                .filter(|(_, location)| {
2081                    matches!(location, SessionLocation::Child { root: parent } if *parent == root)
2082                })
2083                .map(|(id, _)| *id)
2084                .collect::<Vec<_>>()
2085        };
2086        members.extend(located);
2087        members.sort_by_key(|id| id.to_string());
2088        members.dedup();
2089        members
2090    }
2091
2092    /// Direct children of `parent` inside tree `root`: recorded edges plus every
2093    /// member whose origin names `parent`.
2094    fn direct_children(&self, root: SessionId, parent: SessionId) -> Vec<SessionId> {
2095        let mut children = self
2096            .trees
2097            .lock()
2098            .unwrap_or_else(|poisoned| poisoned.into_inner())
2099            .get(&root)
2100            .and_then(|state| state.children.get(&parent).cloned())
2101            .unwrap_or_default();
2102        for id in self.tree_members(root) {
2103            let filed_under = match self.cached_origin(id) {
2104                Some(SessionOrigin::Delegated {
2105                    parent_session_id, ..
2106                }) => parent_session_id,
2107                _ => root,
2108            };
2109            if filed_under == parent && !children.contains(&id) {
2110                children.push(id);
2111            }
2112        }
2113        children.sort_by_key(|id| id.to_string());
2114        children.dedup();
2115        children
2116    }
2117
2118    /// Files a newly created session under its root: tree edge plus placement,
2119    /// in memory only. The durable `index.json` rewrite waits until the child
2120    /// directory is actually published (`persist_buffered`, `fork`), because an
2121    /// index naming a session that was never published is exactly the stale entry
2122    /// startup then has to reject (review L10, §3.4).
2123    fn note_placed_child(&self, location: &SessionLocation, origin: &SessionOrigin, id: SessionId) {
2124        let SessionLocation::Child { root } = location else {
2125            return;
2126        };
2127        let parent = match origin {
2128            SessionOrigin::Delegated {
2129                parent_session_id, ..
2130            } => *parent_session_id,
2131            _ => *root,
2132        };
2133        self.record_child_edge(*root, parent, id);
2134    }
2135
2136    /// Records the `parent -> child` edge plus the child's placement, and the
2137    /// root's index cache is refreshed by the caller.
2138    fn record_child_edge(&self, root: SessionId, parent: SessionId, child: SessionId) {
2139        let mut trees = self
2140            .trees
2141            .lock()
2142            .unwrap_or_else(|poisoned| poisoned.into_inner());
2143        let siblings = trees
2144            .entry(root)
2145            .or_default()
2146            .children
2147            .entry(parent)
2148            .or_default();
2149        if !siblings.contains(&child) {
2150            siblings.push(child);
2151        }
2152    }
2153
2154    /// Descendant session ids filed under `parent`, from the directory listing
2155    /// only (non-uuid entries such as `index.json` are skipped).
2156    fn child_dir_ids(&self, parent: SessionId) -> Vec<SessionId> {
2157        let Ok(entries) = fs::read_dir(self.subagents_dir(parent)) else {
2158            return Vec::new();
2159        };
2160        let mut ids = entries
2161            .filter_map(|entry| entry.ok())
2162            .filter(|entry| entry.path().is_dir())
2163            .filter_map(|entry| {
2164                entry
2165                    .file_name()
2166                    .to_string_lossy()
2167                    .parse::<SessionId>()
2168                    .ok()
2169            })
2170            .collect::<Vec<_>>();
2171        ids.sort_by_key(|id| id.to_string());
2172        ids
2173    }
2174
2175    /// Sweeps the per-child ownership locks an older build wrote.
2176    ///
2177    /// Ownership is the tree's, so the only lock this build writes or reads is
2178    /// the root's. Anything matching a legacy child layout directly under
2179    /// `subagents/` — `<child>/owner.lock` on unix, `<child-id>.owner.lock` as
2180    /// a Windows sidecar — is dead weight. The sweep is best-effort: a file
2181    /// another process still holds open simply stays, and is never consulted.
2182    pub(super) fn remove_legacy_child_locks(&self, root: SessionId) {
2183        let Ok(entries) = fs::read_dir(self.subagents_dir(root)) else {
2184            return;
2185        };
2186        for entry in entries.flatten() {
2187            let name = entry.file_name().to_string_lossy().into_owned();
2188            if entry.path().is_dir() {
2189                if name.parse::<SessionId>().is_ok() {
2190                    let _ = fs::remove_file(entry.path().join(OWNER_LOCK_FILE));
2191                }
2192            } else if name
2193                .strip_suffix(OWNER_LOCK_SUFFIX)
2194                .is_some_and(|id| id.parse::<SessionId>().is_ok())
2195            {
2196                let _ = fs::remove_file(entry.path());
2197            }
2198        }
2199    }
2200
2201    /// Residency-only summary lookup (never opens a log, never completes a tree).
2202    ///
2203    /// This is the store's *non-triggering* summary read: `children`, the durable
2204    /// index rewrite and the listings want the cache exactly as it stands. A
2205    /// caller serving a session has to go through [`Self::summary`], which proves
2206    /// the cache current first (§3.2).
2207    fn cached_summary(&self, id: SessionId) -> Option<SessionSummary> {
2208        let residency = self
2209            .residency
2210            .lock()
2211            .unwrap_or_else(|poisoned| poisoned.into_inner());
2212        if let Some(session) = residency.resident.get(&id) {
2213            return Some(summary_from_projection(session));
2214        }
2215        residency.evicted.get(&id).cloned()
2216    }
2217
2218    /// Whether `id`'s child directory under `root` exists, i.e. the session has
2219    /// been published. A buffered child is in memory only and must stay out of
2220    /// the durable cache.
2221    fn is_published_child(&self, root: SessionId, id: SessionId) -> bool {
2222        self.path_for(SessionLocation::Child { root }, id).is_dir()
2223    }
2224
2225    /// Rebuilds and atomically rewrites a root's child-summary cache. Whole-file
2226    /// rewrite on purpose (§3.4). Required refreshes are part of the durability
2227    /// contract, so callers must handle failures.
2228    fn persist_subagent_index(&self, root: SessionId) -> Result<(), SessionError> {
2229        let cached = self
2230            .trees
2231            .lock()
2232            .unwrap_or_else(|poisoned| poisoned.into_inner())
2233            .get(&root)
2234            .map(|state| state.terminal_runs.clone())
2235            .unwrap_or_default();
2236        let children = self
2237            .tree_members(root)
2238            .into_iter()
2239            .filter_map(|id| {
2240                // Only a published child may be named: the cache is rewritten
2241                // from inside tree loads and appends, and an entry for a session
2242                // whose directory does not exist yet is exactly the crash-window
2243                // staleness startup then has to reject (§3.4, review L10).
2244                if !self.is_published_child(root, id) {
2245                    return None;
2246                }
2247                let summary = self.cached_summary(id)?;
2248                Some(IndexedChild {
2249                    summary,
2250                    terminal_runs: cached.get(&id).cloned().unwrap_or_default(),
2251                })
2252            })
2253            .collect::<Vec<_>>();
2254        let path = self.subagent_index_path(root);
2255        if children.is_empty() && !path.is_file() {
2256            return Ok(());
2257        }
2258        let index = SubagentIndex {
2259            version: SUBAGENT_INDEX_VERSION,
2260            children,
2261        };
2262        write_index_json(&path, &index)
2263    }
2264
2265    /// Remembers a terminal run status for the delegation registry cache, then
2266    /// refreshes the owning root's index.
2267    fn record_terminal_run(
2268        &self,
2269        id: SessionId,
2270        run_id: RunId,
2271        status: SessionStatus,
2272    ) -> Result<(), SessionError> {
2273        let Ok(root) = self.root_of(id) else {
2274            return Ok(());
2275        };
2276        if root == id {
2277            return Ok(());
2278        }
2279        let parent = match self.cached_origin(id) {
2280            Some(SessionOrigin::Delegated {
2281                parent_session_id, ..
2282            }) => parent_session_id,
2283            _ => root,
2284        };
2285        let refreshed = {
2286            let mut trees = self
2287                .trees
2288                .lock()
2289                .unwrap_or_else(|poisoned| poisoned.into_inner());
2290            let state = trees.entry(root).or_default();
2291            let siblings = state.children.entry(parent).or_default();
2292            if !siblings.contains(&id) {
2293                siblings.push(id);
2294            }
2295            state
2296                .terminal_runs
2297                .entry(id)
2298                .or_default()
2299                .insert(run_id.to_string(), status)
2300                .is_none()
2301        };
2302        if refreshed {
2303            self.persist_subagent_index(root)?;
2304        }
2305        Ok(())
2306    }
2307
2308    /// Terminal run statuses for a child, served from the index/tree cache
2309    /// without paging the child log (§4.1.3).
2310    #[allow(dead_code)] // consumed by the delegation registry rebuild (P2)
2311    pub(crate) fn terminal_run_status(
2312        &self,
2313        id: SessionId,
2314        run_id: RunId,
2315    ) -> Option<SessionStatus> {
2316        let root = self.root_of(id).ok()?;
2317        let trees = self
2318            .trees
2319            .lock()
2320            .unwrap_or_else(|poisoned| poisoned.into_inner());
2321        trees
2322            .get(&root)?
2323            .terminal_runs
2324            .get(&id)?
2325            .get(&run_id.to_string())
2326            .copied()
2327    }
2328
2329    pub fn session_usage(
2330        &self,
2331        id: SessionId,
2332        pricing: &cookie_agent_config::PricingConfig,
2333        catalog: &BTreeMap<
2334            cookie_agent_protocol::ModelKey,
2335            cookie_agent_models::catalog::CatalogModelCost,
2336        >,
2337    ) -> Result<cookie_agent_protocol::SessionUsageResult, SessionError> {
2338        // Terminal run records are the source of truth once the tree is complete
2339        // and every run has closed (§3.4.1, review L14); before that the durable
2340        // store simply has no answer, and the fold's stamps are reported so the
2341        // number never reads lower than the transcript footer.
2342        //
2343        // `summary` is what makes that "once" real: it completes the owning root's
2344        // tree before serving, so a cost question about an unloaded tree is
2345        // answered from the bulk pass instead of from the pre-load cache (§3.2).
2346        let mut usage = self.summary(id)?.usage_rollup;
2347        if usage.request_count == 0 {
2348            // A summary that arrived without usage — a session whose metadata cache
2349            // had to be rebuilt, or one that was seeded from discovery alone — is
2350            // not an answer to "what did this session cost": fold its log, which is
2351            // the same stamped accounting the transcript footer shows.
2352            usage = summary_from_projection(&self.get(id)?).usage_rollup;
2353        }
2354        Ok(cookie_agent_protocol::SessionUsageResult {
2355            session_id: id,
2356            usage: crate::usage::with_pricing(usage, pricing, catalog),
2357        })
2358    }
2359
2360    /// Counts one `events.jsonl` open+fold of `id` (§8.2 #5 counts reads, not
2361    /// load attempts, so a hidden second fold cannot hide behind a cached flag).
2362    fn note_log_open(&self, id: SessionId) {
2363        #[cfg(test)]
2364        {
2365            *self
2366                .log_opens
2367                .lock()
2368                .expect("log open counter lock poisoned")
2369                .entry(id)
2370                .or_insert(0) += 1;
2371        }
2372        #[cfg(not(test))]
2373        let _ = id;
2374    }
2375
2376    /// How many times this store opened and folded `id`'s `events.jsonl`.
2377    #[cfg(test)]
2378    pub(crate) fn log_open_count(&self, id: SessionId) -> usize {
2379        *self
2380            .log_opens
2381            .lock()
2382            .expect("log open counter lock poisoned")
2383            .get(&id)
2384            .unwrap_or(&0)
2385    }
2386
2387    #[must_use]
2388    pub fn is_resident(&self, id: SessionId) -> bool {
2389        self.residency
2390            .lock()
2391            .unwrap_or_else(|poisoned| poisoned.into_inner())
2392            .resident
2393            .contains_key(&id)
2394    }
2395
2396    #[must_use]
2397    pub fn resident_subagent_count(&self) -> usize {
2398        self.residency
2399            .lock()
2400            .unwrap_or_else(|poisoned| poisoned.into_inner())
2401            .resident
2402            .values()
2403            .filter(|session| matches!(session.meta.origin, SessionOrigin::Delegated { .. }))
2404            .count()
2405    }
2406
2407    /// Directory holding the work-dir files (artifacts, the grant journal,
2408    /// runtime revisions, `cwd` and `layout.json`).
2409    #[must_use]
2410    pub fn workdir_dir_path(&self) -> &Path {
2411        &self.workdir_dir
2412    }
2413
2414    #[must_use]
2415    pub fn cwd(&self) -> &Path {
2416        &self.cwd
2417    }
2418
2419    /// Best-effort directory for `id`, *without* locating anything: an unknown id
2420    /// is guessed as `workdir_dir/<id>`, which is structurally wrong for a v2
2421    /// child. Production paths must use [`Self::resolve_dir`]; this stays
2422    /// available to tests, which know where they put their own fixtures.
2423    #[cfg(test)]
2424    #[must_use]
2425    pub(crate) fn session_dir(&self, id: SessionId) -> PathBuf {
2426        self.cached_location(id)
2427            .map(|location| self.path_for(location, id))
2428            .unwrap_or_else(|| self.workdir_dir.join(id.to_string()))
2429    }
2430
2431    /// Metadata cache path to *read*.
2432    pub(crate) fn meta_cache_path(&self, id: SessionId) -> Result<PathBuf, SessionError> {
2433        Ok(meta_path(&self.resolve_dir(id)?))
2434    }
2435
2436    pub fn is_persisted(&self, id: SessionId) -> Result<bool, SessionError> {
2437        Ok(self.get(id)?.log.is_persisted())
2438    }
2439
2440    /// Every direct child of `parent`, resolved from *placement* rather than by
2441    /// scanning every resident projection: seeded tree state + location cache,
2442    /// then a `subagents/` directory scan that adopts children no cache
2443    /// mentions from their `metadata` cache alone (§3.4). No child event log is
2444    /// read here — children of a cold tree report their persisted status.
2445    ///
2446    /// Fails when the tree itself could not be completed: a listing built from
2447    /// the pre-load cache would silently report a stale tree, so the caller gets
2448    /// the load error instead (review L14). A missing or stale `index.json` is
2449    /// not such a failure — the directory scan recovers it (§3.4).
2450    pub fn children(&self, parent: SessionId) -> Result<Vec<ChildSummary>, SessionError> {
2451        // Listing a tree is a use of it: make sure it is complete first (§3.2.2).
2452        self.ensure_tree_for(parent)?;
2453        Ok(self
2454            .child_ids(parent)?
2455            .into_iter()
2456            .filter_map(|id| self.child_summary(id))
2457            .collect())
2458    }
2459
2460    /// Direct child ids of `parent`, resolved from placement: seeded tree state
2461    /// and location cache first, then a scan of the tree directory that adopts
2462    /// filed children no cache mentions from their `metadata` alone.
2463    fn child_ids(&self, parent: SessionId) -> Result<Vec<SessionId>, SessionError> {
2464        self.refresh_discovered();
2465        let root = match self.cached_location(parent) {
2466            Some(SessionLocation::Child { root }) => root,
2467            _ => parent,
2468        };
2469        let mut children = self.direct_children(root, parent);
2470        for id in self.child_dir_ids(root) {
2471            if children.contains(&id) {
2472                continue;
2473            }
2474            if self.adopt_filed_child(root, id)? == Some(parent) {
2475                children.push(id);
2476            }
2477        }
2478        children.sort_by_key(|id| id.to_string());
2479        Ok(children)
2480    }
2481
2482    /// Whether `id` is a root of this store — by cached placement or by the
2483    /// top-level directory it lives in. A root can never be another tree's child,
2484    /// and trusting an index entry that says otherwise would give a real root a
2485    /// wrong-layout path (review L9).
2486    fn is_indexed_root(&self, id: SessionId) -> bool {
2487        matches!(self.cached_location(id), Some(SessionLocation::Root))
2488            || self.workdir_dir.join(id.to_string()).is_dir()
2489    }
2490
2491    /// Checks one `index.json` entry against the directory it claims: the child
2492    /// directory must exist, must not be a root of this store, and its own
2493    /// metadata cache must parse, name that directory, and place it inside
2494    /// `root`. `None` means "ignore the entry" — never an error (§3.4).
2495    fn validated_index_child(&self, root: SessionId, id: SessionId) -> Option<SessionMeta> {
2496        let dir = self.path_for(SessionLocation::Child { root }, id);
2497        if !dir.is_dir() {
2498            eprintln!("subagent index for {root} names {id}, which has no directory");
2499            return None;
2500        }
2501        if self.workdir_dir.join(id.to_string()).is_dir() {
2502            eprintln!("subagent index for {root} names root session {id}; ignored");
2503            return None;
2504        }
2505        let Ok(meta) = read_cache(&meta_path(&dir), &dir.join(EVENTS_FILE)) else {
2506            eprintln!("subagent index for {root}: child {id} metadata is unreadable");
2507            return None;
2508        };
2509        if meta.session_id != id {
2510            eprintln!("session {id} metadata ID does not match its directory");
2511            return None;
2512        }
2513        match meta.origin {
2514            SessionOrigin::Delegated {
2515                root_session_id, ..
2516            } if root_session_id == root => {}
2517            _ => {
2518                eprintln!("subagent index for {root} names {id}, which is not in its tree");
2519                return None;
2520            }
2521        }
2522        Some(meta)
2523    }
2524
2525    /// Adopts a child that is present on disk but missing from the caches (e.g.
2526    /// a crash between the directory publish and the `index.json` refresh), and
2527    /// reports the parent its origin names.
2528    fn adopt_filed_child(
2529        &self,
2530        root: SessionId,
2531        id: SessionId,
2532    ) -> Result<Option<SessionId>, SessionError> {
2533        let dir = self.path_for(SessionLocation::Child { root }, id);
2534        let Ok(meta) = read_cache(&meta_path(&dir), &dir.join(EVENTS_FILE)) else {
2535            return Ok(None);
2536        };
2537        if meta.session_id != id {
2538            eprintln!("session {id} metadata ID does not match its directory");
2539            return Ok(None);
2540        }
2541        let parent = match meta.origin {
2542            SessionOrigin::Delegated {
2543                parent_session_id, ..
2544            } => parent_session_id,
2545            _ => root,
2546        };
2547        self.record_location(id, SessionLocation::Child { root });
2548        self.cache_summary(
2549            id,
2550            SessionSummary {
2551                meta,
2552                usage: None,
2553                usage_rollup: UsageRollup::default(),
2554                agent_usage: BTreeMap::new(),
2555            },
2556        );
2557        self.record_child_edge(root, parent, id);
2558        self.persist_subagent_index(root)?;
2559        Ok(Some(parent))
2560    }
2561
2562    /// Metadata for a tree member without rebuilding its log.
2563    fn summary_meta(&self, id: SessionId) -> Result<SessionMeta, SessionError> {
2564        if let Some(session) = self.get_resident(id) {
2565            return Ok(session.meta);
2566        }
2567        if let Some(summary) = self.cached_summary(id) {
2568            return Ok(summary.meta);
2569        }
2570        // Tree assembly must never page a child log in implicitly.
2571        Err(SessionError::Missing(id))
2572    }
2573
2574    /// Listing view of one child, preferring the live projection for resident
2575    /// sessions and the summary cache otherwise.
2576    fn child_summary(&self, id: SessionId) -> Option<ChildSummary> {
2577        if let Some(session) = self.get_resident(id) {
2578            return Some(ChildSummary {
2579                session_id: session.meta.session_id,
2580                agent: session.meta.creation_selection.agent.clone(),
2581                title: session.meta.title.clone(),
2582                title_updated_seq: session.meta.title_updated_seq,
2583                status: session.status,
2584                usage: session.usage,
2585            });
2586        }
2587        let summary = self.cached_summary(id)?;
2588        Some(ChildSummary {
2589            session_id: summary.meta.session_id,
2590            agent: summary.meta.creation_selection.agent.clone(),
2591            title: summary.meta.title.clone(),
2592            title_updated_seq: summary.meta.title_updated_seq,
2593            status: summary.meta.status,
2594            usage: summary.usage,
2595        })
2596    }
2597
2598    pub fn tree(&self, id: SessionId) -> Result<SessionTree, SessionError> {
2599        self.ensure_tree_for(id)?;
2600        // Assembly walks placement edges, so it costs one metadata cache read per
2601        // tree member instead of a rebuild of every resident session's log.
2602        let root = self.get(id)?;
2603        let mut metadata = HashMap::new();
2604        let mut children = HashMap::<SessionId, Vec<SessionId>>::new();
2605        metadata.insert(id, root.metadata());
2606        let mut queue = self.child_ids(id)?;
2607        if !queue.is_empty() {
2608            children.insert(id, queue.clone());
2609        }
2610        while let Some(current) = queue.pop() {
2611            if metadata.contains_key(&current) {
2612                continue;
2613            }
2614            metadata.insert(current, self.summary_meta(current)?);
2615            let descendants = self.child_ids(current)?;
2616            if !descendants.is_empty() {
2617                children.insert(current, descendants.clone());
2618                queue.extend(descendants);
2619            }
2620        }
2621
2622        fn build_tree(
2623            id: SessionId,
2624            metadata: &HashMap<SessionId, SessionMeta>,
2625            children: &HashMap<SessionId, Vec<SessionId>>,
2626        ) -> Result<SessionTree, SessionError> {
2627            Ok(SessionTree {
2628                session: metadata
2629                    .get(&id)
2630                    .cloned()
2631                    .ok_or(SessionError::Missing(id))?,
2632                children: children
2633                    .get(&id)
2634                    .into_iter()
2635                    .flatten()
2636                    .map(|child| build_tree(*child, metadata, children))
2637                    .collect::<Result<Vec<_>, _>>()?,
2638            })
2639        }
2640
2641        build_tree(id, &metadata, &children)
2642    }
2643
2644    pub(crate) fn release_ownership(&self) {
2645        self.closed.store(true, Ordering::Release);
2646        let _mutation = self.lock_mutation();
2647        let residency = self
2648            .residency
2649            .lock()
2650            .unwrap_or_else(|poisoned| poisoned.into_inner());
2651        for session in residency.resident.values() {
2652            let _ = session.log.suspend_writer();
2653        }
2654        // Dropping every tree entry drops every tree lock and every tree
2655        // authority, which invalidates all of their logs at once.
2656        let mut ownership = self
2657            .ownership
2658            .lock()
2659            .unwrap_or_else(|poisoned| poisoned.into_inner());
2660        ownership.trees.clear();
2661        ownership.writable.clear();
2662        ownership.adopting.clear();
2663    }
2664
2665    fn ensure_open(&self) -> Result<(), SessionError> {
2666        if self.closed.load(Ordering::Acquire) {
2667            Err(SessionError::StoreClosed)
2668        } else {
2669            Ok(())
2670        }
2671    }
2672}
2673
2674impl Drop for SessionStore {
2675    fn drop(&mut self) {
2676        self.release_ownership();
2677    }
2678}
2679
2680#[cfg(test)]
2681mod tests;
2682
2683#[cfg(all(test, windows))]
2684mod windows_tests {
2685    use cookie_agent_protocol::{
2686        AgentMode, AgentRevision, CatalogRevision, ClientRunId, CwdIdentity, EventPayload,
2687        ModelRevision, ProviderStateRevision, RecipeRegistryRevision, RunId, RuntimeRevision,
2688        SessionId, SessionOrigin,
2689    };
2690
2691    use crate::ownership::owner_lock_path;
2692
2693    use super::{SESSION_META_FILE, SESSIONS_ROOT_DIR, SessionStore, WORKDIR_CWD_FILE};
2694
2695    fn revision(label: char) -> String {
2696        format!("sha256:{}", label.to_string().repeat(64))
2697    }
2698
2699    #[test]
2700    fn windows_session_store_applies_private_acls_before_use() {
2701        let temporary = tempfile::tempdir().expect("temporary root");
2702        let cwd = temporary.path().join("workspace");
2703        std::fs::create_dir(&cwd).expect("workspace");
2704        let data = temporary.path().join("data");
2705        let store = SessionStore::open(&data, &cwd).unwrap_or_else(|error| {
2706            panic!("Windows session store open failed for data={data:?}, cwd={cwd:?}: {error:?}")
2707        });
2708        let workdir = store.workdir_dir_path();
2709        let sessions_root = workdir.parent().expect("sessions root");
2710        for path in [
2711            sessions_root.to_owned(),
2712            workdir.to_owned(),
2713            workdir.join(WORKDIR_CWD_FILE),
2714        ] {
2715            cookie_agent_models::secure_store::verify_windows_private_creation(&path)
2716                .unwrap_or_else(|error| {
2717                    panic!("private ACL validation failed for {path:?}: {error:?}")
2718                });
2719        }
2720    }
2721
2722    #[test]
2723    fn windows_session_store_uses_preexisting_untrusted_workdir_acl() {
2724        let temporary = tempfile::tempdir().expect("temporary root");
2725        let cwd = temporary.path().join("workspace");
2726        std::fs::create_dir(&cwd).expect("workspace");
2727        let data = temporary.path().join("data");
2728        let workdir = data
2729            .join(SESSIONS_ROOT_DIR)
2730            .join(SessionStore::workdir_key(&cwd));
2731        std::fs::create_dir_all(&workdir).expect("ordinary workdir");
2732        SessionStore::open(&data, &cwd).expect("ordinary existing workdir");
2733    }
2734
2735    #[test]
2736    fn windows_buffered_session_persists_private_files_before_writing() {
2737        let temporary = tempfile::tempdir().expect("temporary root");
2738        let cwd = temporary.path().join("workspace");
2739        std::fs::create_dir(&cwd).expect("workspace");
2740        let data = temporary.path().join("data");
2741        let store = SessionStore::open(&data, &cwd).expect("session store");
2742        let session_id = SessionId::new_v7();
2743        let agent = crate::test_support::agent_snapshot("test", AgentMode::Primary);
2744        let selection = crate::test_support::run_selection("test");
2745        let binding = agent.fallback_chain[0].clone();
2746        let runtime_revision = RuntimeRevision::new(revision('1')).unwrap();
2747        let catalog_revision = CatalogRevision::new(revision('2')).unwrap();
2748        let provider_state_revision = ProviderStateRevision::new(revision('3')).unwrap();
2749        let model_revision = ModelRevision::new(revision('4')).unwrap();
2750        let agent_revision = AgentRevision::new(revision('5')).unwrap();
2751        let recipe_registry_revision = RecipeRegistryRevision::new(revision('6')).unwrap();
2752        store
2753            .create(
2754                session_id,
2755                cookie_agent_protocol::EventOrigin::new("engine:test").unwrap(),
2756                EventPayload::SessionCreated {
2757                    short_id: None,
2758                    origin: SessionOrigin::Root,
2759                    cwd_identity: CwdIdentity::new("workspace:test").unwrap(),
2760                    creation_selection: selection.clone(),
2761                    creation_agent: Box::new(agent.clone()),
2762                    runtime_revision: runtime_revision.clone(),
2763                    catalog_revision: catalog_revision.clone(),
2764                    provider_state_revision: provider_state_revision.clone(),
2765                    model_revision: model_revision.clone(),
2766                    agent_revision: agent_revision.clone(),
2767                    recipe_registry_revision: recipe_registry_revision.clone(),
2768                    manifest_revision: binding.manifest_revision.clone(),
2769                },
2770            )
2771            .expect("buffered session");
2772        let run_id = RunId::new_v7();
2773        store
2774            .append(
2775                session_id,
2776                Some(run_id),
2777                cookie_agent_protocol::EventOrigin::new("engine:test").unwrap(),
2778                EventPayload::RunStarted {
2779                    client_run_id: ClientRunId::new("windows-buffered-session").unwrap(),
2780                    selection,
2781                    agent: Box::new(agent),
2782                    runtime_revision,
2783                    catalog_revision,
2784                    provider_state_revision,
2785                    model_revision,
2786                    agent_revision,
2787                    recipe_registry_revision,
2788                    manifest_revision: binding.manifest_revision.clone(),
2789                    selected_suffix: vec![binding],
2790                    internal_agents: Vec::new(),
2791                    input_through_seq: 1,
2792                },
2793            )
2794            .expect("start buffered run");
2795        store
2796            .append(
2797                session_id,
2798                Some(run_id),
2799                cookie_agent_protocol::EventOrigin::new("engine:test").unwrap(),
2800                EventPayload::UserInputSubmitted {
2801                    input: "persist me".into(),
2802                },
2803            )
2804            .expect("persist first input");
2805
2806        let session_dir = store.workdir_dir_path().join(session_id.to_string());
2807        for path in [
2808            session_dir.clone(),
2809            session_dir.join("events.jsonl"),
2810            session_dir.join(SESSION_META_FILE),
2811            owner_lock_path(&session_dir),
2812        ] {
2813            cookie_agent_models::secure_store::verify_windows_private_creation(&path)
2814                .unwrap_or_else(|error| {
2815                    panic!("private ACL validation failed for {path:?}: {error}")
2816                });
2817        }
2818    }
2819}