1mod 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";
64pub(crate) const SESSIONS_ROOT_DIR: &str = "sessions";
66pub(crate) const LAYOUT_MARKER_FILE: &str = "layout.json";
68pub(crate) const SESSION_META_FILE: &str = "metadata";
70pub(crate) const SUBAGENTS_DIR: &str = "subagents";
72pub(crate) const SUBAGENT_INDEX_FILE: &str = "index.json";
74const SUBAGENT_INDEX_VERSION: u32 = 1;
76const PERSISTED_SUBSCRIBER_QUEUE_CAPACITY: usize = 256;
77pub(crate) const EVENTS_FILE: &str = "events.jsonl";
79pub(crate) const LAYOUT_VERSION: u32 = 2;
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub(crate) enum SessionLocation {
85 Root,
87 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 #[error("tree load rejected: {0}")]
133 TreeRejected(Box<crate::runtime::EngineError>),
134 #[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#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
176struct IndexedChild {
177 summary: SessionSummary,
178 terminal_runs: BTreeMap<String, SessionStatus>,
180}
181
182#[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#[derive(Debug)]
219enum TreeOwnership {
220 PendingPublish { authority: WriteAuthority },
223 Adopting {
225 _lock: HeldLock,
226 authority: WriteAuthority,
227 },
228 Owned {
229 _lock: HeldLock,
230 authority: WriteAuthority,
231 },
232 Foreign,
234}
235
236impl TreeOwnership {
237 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 fn is_settled(&self) -> bool {
251 matches!(self, Self::PendingPublish { .. } | Self::Owned { .. })
252 }
253}
254
255#[derive(Debug, Default)]
260struct OwnershipState {
261 trees: HashMap<SessionId, TreeOwnership>,
263 writable: HashMap<SessionId, SessionId>,
265 adopting: HashMap<SessionId, SessionId>,
267}
268
269impl OwnershipState {
270 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 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#[derive(Debug)]
308enum ForkTree {
309 NewRoot(WriteAuthority),
311 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 workdir_dir: PathBuf,
329 cwd: PathBuf,
330 locations: Mutex<HashMap<SessionId, SessionLocation>>,
333 trees: Mutex<HashMap<SessionId, TreeState>>,
335 tree_locks: Mutex<HashMap<SessionId, Arc<TreeGate>>>,
337 pending_loads: Mutex<PendingLoads>,
339 residency: Mutex<SessionResidency>,
340 ownership: Mutex<OwnershipState>,
342 adoption_locks: Mutex<HashMap<SessionId, Arc<Mutex<()>>>>,
345 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 #[cfg(test)]
358 log_opens: Mutex<HashMap<SessionId, usize>>,
359 #[cfg(test)]
362 tree_load_read_hook: TreeLoadReadHook,
363}
364
365impl SessionStore {
366 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 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 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 #[must_use]
447 pub fn data_root(&self) -> &Path {
448 &self.data_root
449 }
450
451 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 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 fn tree_root_dir(&self, root: SessionId) -> PathBuf {
492 self.path_for(SessionLocation::Root, root)
493 }
494
495 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 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 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 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 fn creation_origin(creation: &EventPayload) -> Option<SessionOrigin> {
562 match creation {
563 EventPayload::SessionCreated { origin, .. } => Some(origin.clone()),
564 _ => None,
565 }
566 }
567
568 fn dir_for_placement(&self, location: SessionLocation, id: SessionId) -> PathBuf {
570 self.path_for(location, id)
571 }
572
573 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 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 fsync_directory(final_dir)?;
636 Ok(())
637 }
638
639 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 #[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 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 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 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 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 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 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 #[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 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 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 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 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 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 if !was_persisted && let Some(root) = self.parent_root_of(id) {
1375 self.persist_subagent_index(root)?;
1376 }
1377 }
1378 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 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 self.subscribers
1428 .lock()
1429 .unwrap_or_else(|p| p.into_inner())
1430 .entry(envelope.session_id)
1431 .or_default()
1432 .retain(|sender| {
1433 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 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 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 let location = self.placement_for(&source.meta.origin);
1510 let root = location.root_of().unwrap_or(session_id);
1511 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 #[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 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 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 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 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 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 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 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 fn is_filed_child(&self, id: SessionId) -> bool {
1852 matches!(
1853 self.cached_location(id),
1854 Some(SessionLocation::Child { .. })
1855 )
1856 }
1857
1858 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 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 fn summary_cache_is_current(&self, id: SessionId) -> bool {
1941 let root = match self.cached_location(id) {
1942 Some(SessionLocation::Child { root }) => root,
1943 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 None => false,
1955 Some(state) => state.loaded || !state.stale_seeds.contains(&id),
1956 }
1957 }
1958
1959 fn refresh_discovered(&self) {
1961 self.refresh_discovered_roots();
1962 }
1963
1964 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 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 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 match read_cache(&meta_path(&dir), &dir.join(EVENTS_FILE)) {
1997 Ok(meta) if meta.session_id == root => {
1998 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 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 for root in roots {
2033 self.seed_tree_from_index(root);
2034 }
2035 }
2036
2037 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 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 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 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 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 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 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 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 fn is_published_child(&self, root: SessionId, id: SessionId) -> bool {
2222 self.path_for(SessionLocation::Child { root }, id).is_dir()
2223 }
2224
2225 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 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 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 #[allow(dead_code)] 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 let mut usage = self.summary(id)?.usage_rollup;
2347 if usage.request_count == 0 {
2348 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 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 #[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 #[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 #[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 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 pub fn children(&self, parent: SessionId) -> Result<Vec<ChildSummary>, SessionError> {
2451 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 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 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 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 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 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 Err(SessionError::Missing(id))
2572 }
2573
2574 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 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(¤t) {
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 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}