Skip to main content

chio_kernel/
session.rs

1#[cfg(loom)]
2use loom::sync::atomic::{AtomicU64, Ordering};
3use std::collections::{HashMap, HashSet, VecDeque};
4#[cfg(not(loom))]
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::{Instant, SystemTime, UNIX_EPOCH};
7
8use chio_core::crypto::{canonical_json_bytes, sha256_hex};
9use chio_core::session::{
10    CreateElicitationOperation, NormalizedRoot, OperationContext, OperationKind,
11    OperationTerminalState, ProgressToken, RequestId, RequestOwnershipSnapshot, RootDefinition,
12    SessionAnchorReference, SessionAuthContext, SessionId,
13};
14// Consumed only by SessionOperationResponse and queue_tool_server_event, both of
15// which are gated out under loom.
16#[cfg(not(loom))]
17use chio_core::session::{
18    CompletionResult, PromptDefinition, PromptResult, ResourceContent, ResourceDefinition,
19    ResourceTemplateDefinition,
20};
21use chio_core::{capability::token::CapabilityToken, AgentId};
22#[cfg(loom)]
23use loom::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
24#[cfg(not(loom))]
25use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
26
27#[cfg(not(loom))]
28use crate::{ToolCallResponse, ToolServerEvent};
29// Consumed only by SessionOperationResponse, gated out under loom.
30#[cfg(not(loom))]
31use chio_core::receipt::body::ChioReceipt;
32
33fn read_lock<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
34    match lock.read() {
35        Ok(value) => value,
36        Err(poisoned) => poisoned.into_inner(),
37    }
38}
39
40fn write_lock<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
41    match lock.write() {
42        Ok(value) => value,
43        Err(poisoned) => poisoned.into_inner(),
44    }
45}
46
47#[derive(Debug)]
48struct SnapshotState<T> {
49    current: RwLock<T>,
50}
51
52impl<T> SnapshotState<T> {
53    fn new(initial: T) -> Self {
54        Self {
55            current: RwLock::new(initial),
56        }
57    }
58
59    fn with_current<R>(&self, read: impl FnOnce(&T) -> R) -> R {
60        let current = read_lock(&self.current);
61        read(&current)
62    }
63
64    fn replace(&self, next: T) {
65        *write_lock(&self.current) = next;
66    }
67
68    fn replace_with<R>(&self, update: impl FnOnce(&T) -> (Option<T>, R)) -> R {
69        let mut current = write_lock(&self.current);
70        let (next, result) = update(&current);
71        if let Some(next) = next {
72            *current = next;
73        }
74        result
75    }
76}
77
78impl<T: Clone> Clone for SnapshotState<T> {
79    fn clone(&self) -> Self {
80        Self::new(self.with_current(Clone::clone))
81    }
82}
83
84/// Lifecycle state of a logical kernel session.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum SessionState {
87    Initializing,
88    Ready,
89    Draining,
90    Closed,
91}
92
93impl SessionState {
94    pub fn as_str(self) -> &'static str {
95        match self {
96            Self::Initializing => "initializing",
97            Self::Ready => "ready",
98            Self::Draining => "draining",
99            Self::Closed => "closed",
100        }
101    }
102}
103
104/// Feature flags negotiated with the peer at session establishment.
105#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
106pub struct PeerCapabilities {
107    pub supports_progress: bool,
108    pub supports_cancellation: bool,
109    pub supports_subscriptions: bool,
110    pub supports_chio_tool_streaming: bool,
111    pub supports_roots: bool,
112    pub roots_list_changed: bool,
113    pub supports_sampling: bool,
114    pub sampling_context: bool,
115    pub sampling_tools: bool,
116    pub supports_elicitation: bool,
117    pub elicitation_form: bool,
118    pub elicitation_url: bool,
119}
120
121/// Bookkeeping record for an in-flight request.
122#[derive(Debug, Clone)]
123pub struct InflightRequest {
124    pub request_id: RequestId,
125    pub parent_request_id: Option<RequestId>,
126    pub operation_kind: OperationKind,
127    pub session_anchor_id: String,
128    pub started_at: Instant,
129    pub progress_token: Option<ProgressToken>,
130    pub cancellation_requested: bool,
131    pub cancellable: bool,
132    pub pending_execution_nonce_id: Option<String>,
133}
134
135impl InflightRequest {
136    pub fn ownership(&self) -> RequestOwnershipSnapshot {
137        RequestOwnershipSnapshot::request_owned()
138    }
139}
140
141/// Registry of requests that are currently active within a session.
142#[derive(Debug)]
143pub struct InflightRegistry {
144    requests: RwLock<HashMap<RequestId, InflightRequest>>,
145    active_count: AtomicU64,
146}
147
148impl Clone for InflightRegistry {
149    fn clone(&self) -> Self {
150        let requests = self.read_requests().clone();
151        Self {
152            active_count: AtomicU64::new(requests.len() as u64),
153            requests: RwLock::new(requests),
154        }
155    }
156}
157
158impl Default for InflightRegistry {
159    fn default() -> Self {
160        Self {
161            requests: RwLock::new(HashMap::new()),
162            active_count: AtomicU64::new(0),
163        }
164    }
165}
166
167impl InflightRegistry {
168    fn read_requests(&self) -> RwLockReadGuard<'_, HashMap<RequestId, InflightRequest>> {
169        read_lock(&self.requests)
170    }
171
172    fn write_requests(&self) -> RwLockWriteGuard<'_, HashMap<RequestId, InflightRequest>> {
173        write_lock(&self.requests)
174    }
175
176    pub fn track(
177        &self,
178        context: &OperationContext,
179        operation_kind: OperationKind,
180        session_anchor_id: &str,
181        cancellable: bool,
182    ) -> Result<(), SessionError> {
183        let mut requests = self.write_requests();
184        if requests.contains_key(&context.request_id) {
185            return Err(SessionError::DuplicateInflightRequest {
186                request_id: context.request_id.clone(),
187            });
188        }
189
190        requests.insert(
191            context.request_id.clone(),
192            InflightRequest {
193                request_id: context.request_id.clone(),
194                parent_request_id: context.parent_request_id.clone(),
195                operation_kind,
196                session_anchor_id: session_anchor_id.to_string(),
197                started_at: Instant::now(),
198                progress_token: context.progress_token.clone(),
199                cancellation_requested: false,
200                cancellable,
201                pending_execution_nonce_id: None,
202            },
203        );
204        self.active_count.fetch_add(1, Ordering::AcqRel);
205        Ok(())
206    }
207
208    fn track_locked(
209        &self,
210        requests: &mut HashMap<RequestId, InflightRequest>,
211        context: &OperationContext,
212        operation_kind: OperationKind,
213        session_anchor_id: &str,
214        cancellable: bool,
215    ) -> Result<(), SessionError> {
216        if requests.contains_key(&context.request_id) {
217            return Err(SessionError::DuplicateInflightRequest {
218                request_id: context.request_id.clone(),
219            });
220        }
221
222        requests.insert(
223            context.request_id.clone(),
224            InflightRequest {
225                request_id: context.request_id.clone(),
226                parent_request_id: context.parent_request_id.clone(),
227                operation_kind,
228                session_anchor_id: session_anchor_id.to_string(),
229                started_at: Instant::now(),
230                progress_token: context.progress_token.clone(),
231                cancellation_requested: false,
232                cancellable,
233                pending_execution_nonce_id: None,
234            },
235        );
236        self.active_count.fetch_add(1, Ordering::AcqRel);
237        Ok(())
238    }
239
240    pub fn complete(&self, request_id: &RequestId) -> Result<InflightRequest, SessionError> {
241        let mut requests = self.write_requests();
242        let completed =
243            requests
244                .remove(request_id)
245                .ok_or_else(|| SessionError::RequestNotInflight {
246                    request_id: request_id.clone(),
247                })?;
248        if self
249            .active_count
250            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
251                current.checked_sub(1)
252            })
253            .is_err()
254        {
255            self.active_count
256                .store(requests.len() as u64, Ordering::Release);
257        }
258        Ok(completed)
259    }
260
261    pub fn mark_cancellation_requested(&self, request_id: &RequestId) -> Result<(), SessionError> {
262        let mut requests = self.write_requests();
263        let request =
264            requests
265                .get_mut(request_id)
266                .ok_or_else(|| SessionError::RequestNotInflight {
267                    request_id: request_id.clone(),
268                })?;
269
270        if !request.cancellable {
271            return Err(SessionError::RequestNotCancellable {
272                request_id: request_id.clone(),
273            });
274        }
275
276        request.cancellation_requested = true;
277        Ok(())
278    }
279
280    pub fn get(&self, request_id: &RequestId) -> Option<InflightRequest> {
281        self.read_requests().get(request_id).cloned()
282    }
283
284    pub fn mark_execution_nonce_pending(
285        &self,
286        request_id: &RequestId,
287        nonce_id: &str,
288    ) -> Result<(), SessionError> {
289        let mut requests = self.write_requests();
290        let request = requests.get_mut(request_id).ok_or_else(|| {
291            SessionError::ExecutionNonceRetryMismatch {
292                request_id: request_id.clone(),
293            }
294        })?;
295        if request.pending_execution_nonce_id.is_some() {
296            return Err(SessionError::ExecutionNonceRetryMismatch {
297                request_id: request_id.clone(),
298            });
299        }
300        request.pending_execution_nonce_id = Some(nonce_id.to_string());
301        Ok(())
302    }
303
304    pub fn len(&self) -> usize {
305        self.active_count.load(Ordering::Acquire) as usize
306    }
307
308    pub fn is_empty(&self) -> bool {
309        self.active_count.load(Ordering::Acquire) == 0
310    }
311
312    pub fn clear(&self) {
313        self.write_requests().clear();
314        self.active_count.store(0, Ordering::Release);
315    }
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Hash)]
319enum SubscriptionSubject {
320    Resource(String),
321}
322
323/// Registry for session-scoped subscriptions.
324#[derive(Debug)]
325pub struct SubscriptionRegistry {
326    subscriptions: RwLock<HashSet<SubscriptionSubject>>,
327    subscription_count: AtomicU64,
328}
329
330impl Clone for SubscriptionRegistry {
331    fn clone(&self) -> Self {
332        let subscriptions = read_lock(&self.subscriptions).clone();
333        Self {
334            subscription_count: AtomicU64::new(subscriptions.len() as u64),
335            subscriptions: RwLock::new(subscriptions),
336        }
337    }
338}
339
340impl Default for SubscriptionRegistry {
341    fn default() -> Self {
342        Self {
343            subscriptions: RwLock::new(HashSet::new()),
344            subscription_count: AtomicU64::new(0),
345        }
346    }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub enum LateSessionEvent {
351    ElicitationCompleted {
352        elicitation_id: String,
353        related_task_id: Option<String>,
354    },
355    ResourceUpdated {
356        uri: String,
357    },
358    ResourcesListChanged,
359    ToolsListChanged,
360    PromptsListChanged,
361}
362
363#[derive(Debug, Clone)]
364struct PendingUrlElicitation {
365    related_task_id: Option<String>,
366}
367
368impl SubscriptionRegistry {
369    pub fn subscribe_resource(&self, uri: impl Into<String>) {
370        let mut subscriptions = write_lock(&self.subscriptions);
371        subscriptions.insert(SubscriptionSubject::Resource(uri.into()));
372        self.subscription_count
373            .store(subscriptions.len() as u64, Ordering::Release);
374    }
375
376    pub fn unsubscribe_resource(&self, uri: &str) {
377        let mut subscriptions = write_lock(&self.subscriptions);
378        subscriptions.remove(&SubscriptionSubject::Resource(uri.to_string()));
379        self.subscription_count
380            .store(subscriptions.len() as u64, Ordering::Release);
381    }
382
383    pub fn contains_resource(&self, uri: &str) -> bool {
384        read_lock(&self.subscriptions).contains(&SubscriptionSubject::Resource(uri.to_string()))
385    }
386
387    pub fn len(&self) -> usize {
388        self.subscription_count.load(Ordering::Acquire) as usize
389    }
390
391    pub fn is_empty(&self) -> bool {
392        self.subscription_count.load(Ordering::Acquire) == 0
393    }
394
395    pub fn clear(&self) {
396        write_lock(&self.subscriptions).clear();
397        self.subscription_count.store(0, Ordering::Release);
398    }
399}
400
401const TERMINAL_HISTORY_LIMIT: usize = 256;
402
403/// Bounded history of terminal request outcomes for a session.
404#[derive(Debug, Clone)]
405struct TerminalRegistryInner {
406    states: HashMap<RequestId, OperationTerminalState>,
407    order: VecDeque<RequestId>,
408    limit: usize,
409}
410
411impl Default for TerminalRegistryInner {
412    fn default() -> Self {
413        Self {
414            states: HashMap::new(),
415            order: VecDeque::new(),
416            limit: TERMINAL_HISTORY_LIMIT,
417        }
418    }
419}
420
421/// Bounded history of terminal request outcomes for a session.
422#[derive(Debug)]
423pub struct TerminalRegistry {
424    inner: SnapshotState<TerminalRegistryInner>,
425}
426
427impl Clone for TerminalRegistry {
428    fn clone(&self) -> Self {
429        Self {
430            inner: self.inner.clone(),
431        }
432    }
433}
434
435impl Default for TerminalRegistry {
436    fn default() -> Self {
437        Self {
438            inner: SnapshotState::new(TerminalRegistryInner::default()),
439        }
440    }
441}
442
443impl TerminalRegistry {
444    pub fn record(&self, request_id: RequestId, state: OperationTerminalState) -> bool {
445        self.inner.replace_with(|current| {
446            if current.states.contains_key(&request_id) {
447                return (None, false);
448            }
449
450            let mut next = current.clone();
451            next.order.push_back(request_id.clone());
452            next.states.insert(request_id, state);
453
454            while next.order.len() > next.limit {
455                if let Some(oldest) = next.order.pop_front() {
456                    next.states.remove(&oldest);
457                }
458            }
459            (Some(next), true)
460        })
461    }
462
463    pub fn get(&self, request_id: &RequestId) -> Option<OperationTerminalState> {
464        self.inner
465            .with_current(|current| current.states.get(request_id).cloned())
466    }
467
468    pub fn remove(&self, request_id: &RequestId) {
469        self.inner.replace_with(|current| {
470            if !current.states.contains_key(request_id) {
471                return (None, ());
472            }
473
474            let mut next = current.clone();
475            next.states.remove(request_id);
476            next.order.retain(|existing| existing != request_id);
477            (Some(next), ())
478        });
479    }
480
481    pub fn len(&self) -> usize {
482        self.inner.with_current(|current| current.states.len())
483    }
484
485    pub fn is_empty(&self) -> bool {
486        self.inner.with_current(|current| current.states.is_empty())
487    }
488}
489
490/// Errors for session lifecycle and in-flight management.
491#[derive(Debug, thiserror::Error, PartialEq, Eq)]
492pub enum SessionError {
493    #[error("invalid session transition from {from} to {to}")]
494    InvalidTransition {
495        from: &'static str,
496        to: &'static str,
497    },
498
499    #[error("session {session_id} cannot handle {operation} while {state}")]
500    OperationNotAllowed {
501        session_id: SessionId,
502        operation: &'static str,
503        state: &'static str,
504    },
505
506    #[error("operation context session {actual} does not match runtime session {expected}")]
507    ContextSessionMismatch {
508        expected: SessionId,
509        actual: SessionId,
510    },
511
512    #[error("operation context agent {actual} does not match session agent {expected}")]
513    ContextAgentMismatch { expected: AgentId, actual: AgentId },
514
515    #[error("request {request_id} is already in flight")]
516    DuplicateInflightRequest { request_id: RequestId },
517
518    #[error("request {request_id} already has authoritative lineage in this session")]
519    DuplicateRequestLineage { request_id: RequestId },
520
521    #[error("request {request_id} is not in flight")]
522    RequestNotInflight { request_id: RequestId },
523
524    #[error(
525        "execution nonce retry for request {request_id} does not match a pending session preflight"
526    )]
527    ExecutionNonceRetryMismatch { request_id: RequestId },
528
529    #[error("request {request_id} is not cancellable")]
530    RequestNotCancellable { request_id: RequestId },
531
532    #[error("session {session_id} cannot close while {active_count} request(s) remain active")]
533    CloseRequiresDrain {
534        session_id: SessionId,
535        active_count: u64,
536    },
537
538    #[error("parent request {parent_request_id} is not in flight for child request {request_id}")]
539    ParentRequestNotInflight {
540        request_id: RequestId,
541        parent_request_id: RequestId,
542    },
543
544    #[error(
545        "parent request {parent_request_id} for child request {request_id} belongs to stale session anchor {parent_session_anchor_id}, current anchor is {current_session_anchor_id}"
546    )]
547    ParentRequestAnchorMismatch {
548        request_id: RequestId,
549        parent_request_id: RequestId,
550        parent_session_anchor_id: String,
551        current_session_anchor_id: String,
552    },
553}
554
555#[derive(Debug, PartialEq, Eq)]
556pub enum SessionPersistError<E> {
557    Session(SessionError),
558    Persist(E),
559}
560
561#[derive(Debug, Clone, PartialEq, Eq)]
562pub struct SessionAnchorState {
563    id: String,
564    auth_epoch: u64,
565    auth_context_hash: String,
566    issued_at: u64,
567}
568
569impl SessionAnchorState {
570    fn new(session_id: &SessionId, auth_context: &SessionAuthContext, auth_epoch: u64) -> Self {
571        let auth_context_hash = auth_context_hash(auth_context);
572        let hash_prefix = &auth_context_hash[..12.min(auth_context_hash.len())];
573        Self {
574            id: format!("{session_id}:anchor:{auth_epoch}:{hash_prefix}"),
575            auth_epoch,
576            auth_context_hash,
577            issued_at: current_unix_timestamp(),
578        }
579    }
580
581    pub fn id(&self) -> &str {
582        &self.id
583    }
584
585    pub fn auth_epoch(&self) -> u64 {
586        self.auth_epoch
587    }
588
589    pub fn auth_context_hash(&self) -> &str {
590        &self.auth_context_hash
591    }
592
593    pub fn issued_at(&self) -> u64 {
594        self.issued_at
595    }
596
597    pub fn reference(&self) -> SessionAnchorReference {
598        SessionAnchorReference::new(self.id.clone(), self.auth_context_hash.clone())
599    }
600}
601
602#[derive(Debug, Clone, PartialEq, Eq)]
603pub struct RequestLineageRecord {
604    pub request_id: RequestId,
605    pub session_anchor_id: String,
606    pub auth_epoch: u64,
607    pub parent_request_id: Option<RequestId>,
608    pub operation_kind: OperationKind,
609    pub started_at: u64,
610    pub terminal_state: Option<OperationTerminalState>,
611}
612
613#[derive(Debug, Clone)]
614struct SessionInner {
615    state: SessionState,
616}
617
618#[derive(Debug, Clone)]
619struct SessionAuthState {
620    auth_context: SessionAuthContext,
621    session_anchor: SessionAnchorState,
622}
623
624#[derive(Debug, Clone)]
625struct SessionRoots {
626    roots: Vec<RootDefinition>,
627    normalized_roots: Vec<NormalizedRoot>,
628}
629
630#[derive(Debug, Clone)]
631pub struct SessionAnchorSnapshot {
632    pub session_id: SessionId,
633    pub agent_id: AgentId,
634    pub auth_context: SessionAuthContext,
635    pub session_anchor: SessionAnchorState,
636}
637
638#[derive(Debug, Clone)]
639pub struct SessionRequestStart {
640    pub session: SessionAnchorSnapshot,
641    pub lineage: RequestLineageRecord,
642}
643
644/// Session host object owned by the kernel.
645#[derive(Debug)]
646pub struct Session {
647    id: SessionId,
648    agent_id: AgentId,
649    inner: RwLock<SessionInner>,
650    auth_state: SnapshotState<SessionAuthState>,
651    peer_capabilities: SnapshotState<PeerCapabilities>,
652    roots: SnapshotState<SessionRoots>,
653    issued_capabilities: Vec<CapabilityToken>,
654    inflight: InflightRegistry,
655    subscriptions: SubscriptionRegistry,
656    terminal: TerminalRegistry,
657    request_lineage: RwLock<HashMap<RequestId, RequestLineageRecord>>,
658    pending_url_elicitations: RwLock<HashMap<String, PendingUrlElicitation>>,
659    late_events: RwLock<VecDeque<LateSessionEvent>>,
660}
661
662fn operation_allowed_for_state(state: SessionState, operation: OperationKind) -> bool {
663    match state {
664        SessionState::Initializing => matches!(
665            operation,
666            OperationKind::ListCapabilities | OperationKind::Heartbeat
667        ),
668        SessionState::Ready => true,
669        SessionState::Draining => matches!(
670            operation,
671            OperationKind::ListCapabilities | OperationKind::Heartbeat
672        ),
673        SessionState::Closed => false,
674    }
675}
676
677fn validate_parent_request_lineage_locked(
678    request_id: &RequestId,
679    parent_request_id: &RequestId,
680    requests: &HashMap<RequestId, InflightRequest>,
681    request_lineage: &HashMap<RequestId, RequestLineageRecord>,
682    current_session_anchor_id: &str,
683) -> Result<RequestLineageRecord, SessionError> {
684    let Some(parent_inflight) = requests.get(parent_request_id) else {
685        return Err(SessionError::ParentRequestNotInflight {
686            request_id: request_id.clone(),
687            parent_request_id: parent_request_id.clone(),
688        });
689    };
690    let Some(parent_lineage) = request_lineage.get(parent_request_id).cloned() else {
691        return Err(SessionError::ParentRequestNotInflight {
692            request_id: request_id.clone(),
693            parent_request_id: parent_request_id.clone(),
694        });
695    };
696    if parent_lineage.session_anchor_id != current_session_anchor_id {
697        return Err(SessionError::ParentRequestAnchorMismatch {
698            request_id: request_id.clone(),
699            parent_request_id: parent_request_id.clone(),
700            parent_session_anchor_id: parent_inflight.session_anchor_id.clone(),
701            current_session_anchor_id: current_session_anchor_id.to_string(),
702        });
703    }
704    Ok(parent_lineage)
705}
706
707impl Clone for Session {
708    fn clone(&self) -> Self {
709        let inner = self.read_inner().clone();
710        Self {
711            id: self.id.clone(),
712            agent_id: self.agent_id.clone(),
713            inner: RwLock::new(inner),
714            auth_state: self.auth_state.clone(),
715            peer_capabilities: self.peer_capabilities.clone(),
716            roots: self.roots.clone(),
717            issued_capabilities: self.issued_capabilities.clone(),
718            inflight: self.inflight.clone(),
719            subscriptions: self.subscriptions.clone(),
720            terminal: self.terminal.clone(),
721            request_lineage: RwLock::new(self.read_request_lineage().clone()),
722            pending_url_elicitations: RwLock::new(self.read_pending_url_elicitations().clone()),
723            late_events: RwLock::new(self.read_late_events().clone()),
724        }
725    }
726}
727
728impl Session {
729    pub fn new(
730        id: SessionId,
731        agent_id: AgentId,
732        issued_capabilities: Vec<CapabilityToken>,
733    ) -> Self {
734        let auth_context = SessionAuthContext::in_process_anonymous();
735        let session_anchor = SessionAnchorState::new(&id, &auth_context, 0);
736        Self {
737            id,
738            agent_id,
739            inner: RwLock::new(SessionInner {
740                state: SessionState::Initializing,
741            }),
742            auth_state: SnapshotState::new(SessionAuthState {
743                auth_context,
744                session_anchor,
745            }),
746            peer_capabilities: SnapshotState::new(PeerCapabilities::default()),
747            roots: SnapshotState::new(SessionRoots {
748                roots: Vec::new(),
749                normalized_roots: Vec::new(),
750            }),
751            issued_capabilities,
752            inflight: InflightRegistry::default(),
753            subscriptions: SubscriptionRegistry::default(),
754            terminal: TerminalRegistry::default(),
755            request_lineage: RwLock::new(HashMap::new()),
756            pending_url_elicitations: RwLock::new(HashMap::new()),
757            late_events: RwLock::new(VecDeque::new()),
758        }
759    }
760
761    fn read_inner(&self) -> RwLockReadGuard<'_, SessionInner> {
762        read_lock(&self.inner)
763    }
764
765    fn write_inner(&self) -> RwLockWriteGuard<'_, SessionInner> {
766        write_lock(&self.inner)
767    }
768
769    fn read_request_lineage(
770        &self,
771    ) -> RwLockReadGuard<'_, HashMap<RequestId, RequestLineageRecord>> {
772        read_lock(&self.request_lineage)
773    }
774
775    fn read_pending_url_elicitations(
776        &self,
777    ) -> RwLockReadGuard<'_, HashMap<String, PendingUrlElicitation>> {
778        read_lock(&self.pending_url_elicitations)
779    }
780
781    fn write_pending_url_elicitations(
782        &self,
783    ) -> RwLockWriteGuard<'_, HashMap<String, PendingUrlElicitation>> {
784        write_lock(&self.pending_url_elicitations)
785    }
786
787    fn read_late_events(&self) -> RwLockReadGuard<'_, VecDeque<LateSessionEvent>> {
788        read_lock(&self.late_events)
789    }
790
791    fn write_late_events(&self) -> RwLockWriteGuard<'_, VecDeque<LateSessionEvent>> {
792        write_lock(&self.late_events)
793    }
794
795    fn write_request_lineage(
796        &self,
797    ) -> RwLockWriteGuard<'_, HashMap<RequestId, RequestLineageRecord>> {
798        write_lock(&self.request_lineage)
799    }
800
801    pub fn id(&self) -> &SessionId {
802        &self.id
803    }
804
805    pub fn agent_id(&self) -> &str {
806        &self.agent_id
807    }
808
809    pub fn state(&self) -> SessionState {
810        self.read_inner().state
811    }
812
813    pub fn auth_context(&self) -> SessionAuthContext {
814        self.auth_state
815            .with_current(|current| current.auth_context.clone())
816    }
817
818    pub fn session_anchor(&self) -> SessionAnchorState {
819        self.auth_state
820            .with_current(|current| current.session_anchor.clone())
821    }
822
823    pub fn session_anchor_snapshot(&self) -> SessionAnchorSnapshot {
824        self.auth_state
825            .with_current(|current| SessionAnchorSnapshot {
826                session_id: self.id.clone(),
827                agent_id: self.agent_id.clone(),
828                auth_context: current.auth_context.clone(),
829                session_anchor: current.session_anchor.clone(),
830            })
831    }
832
833    pub fn request_lineage(&self, request_id: &RequestId) -> Option<RequestLineageRecord> {
834        self.read_request_lineage().get(request_id).cloned()
835    }
836
837    pub fn peer_capabilities(&self) -> PeerCapabilities {
838        self.peer_capabilities.with_current(Clone::clone)
839    }
840
841    pub fn capabilities(&self) -> &[CapabilityToken] {
842        &self.issued_capabilities
843    }
844
845    pub fn roots(&self) -> Vec<RootDefinition> {
846        self.roots.with_current(|current| current.roots.clone())
847    }
848
849    pub fn normalized_roots(&self) -> Vec<NormalizedRoot> {
850        self.roots
851            .with_current(|current| current.normalized_roots.clone())
852    }
853
854    pub fn enforceable_filesystem_roots(&self) -> Vec<NormalizedRoot> {
855        self.roots.with_current(|current| {
856            current
857                .normalized_roots
858                .iter()
859                .filter(|root| root.is_enforceable_filesystem())
860                .cloned()
861                .collect()
862        })
863    }
864
865    pub fn inflight(&self) -> &InflightRegistry {
866        &self.inflight
867    }
868
869    pub fn mark_execution_nonce_pending(
870        &self,
871        request_id: &RequestId,
872        nonce_id: &str,
873    ) -> Result<(), SessionError> {
874        self.inflight
875            .mark_execution_nonce_pending(request_id, nonce_id)
876    }
877
878    pub fn validate_execution_nonce_retry(
879        &self,
880        context: &OperationContext,
881        operation_kind: OperationKind,
882        nonce_id: &str,
883    ) -> Result<(), SessionError> {
884        self.validate_context(context)?;
885        self.ensure_operation_allowed(operation_kind)?;
886        let current_anchor = self.session_anchor();
887        let request = self.inflight.get(&context.request_id).ok_or_else(|| {
888            SessionError::ExecutionNonceRetryMismatch {
889                request_id: context.request_id.clone(),
890            }
891        })?;
892        if request.operation_kind != operation_kind
893            || request.session_anchor_id != current_anchor.id()
894            || request.parent_request_id != context.parent_request_id
895            || request.progress_token != context.progress_token
896            || request.pending_execution_nonce_id.as_deref() != Some(nonce_id)
897        {
898            return Err(SessionError::ExecutionNonceRetryMismatch {
899                request_id: context.request_id.clone(),
900            });
901        }
902        Ok(())
903    }
904
905    pub fn subscriptions(&self) -> &SubscriptionRegistry {
906        &self.subscriptions
907    }
908
909    pub fn terminal(&self) -> TerminalRegistry {
910        self.terminal.clone()
911    }
912
913    pub fn register_pending_url_elicitation(
914        &self,
915        elicitation_id: impl Into<String>,
916        related_task_id: Option<String>,
917    ) {
918        self.write_pending_url_elicitations().insert(
919            elicitation_id.into(),
920            PendingUrlElicitation { related_task_id },
921        );
922    }
923
924    pub fn register_required_url_elicitations(
925        &self,
926        elicitations: &[CreateElicitationOperation],
927        related_task_id: Option<&str>,
928    ) {
929        for elicitation in elicitations {
930            let CreateElicitationOperation::Url { elicitation_id, .. } = elicitation else {
931                continue;
932            };
933            self.register_pending_url_elicitation(
934                elicitation_id.clone(),
935                related_task_id.map(ToString::to_string),
936            );
937        }
938    }
939
940    pub fn queue_late_event(&self, event: LateSessionEvent) {
941        self.write_late_events().push_back(event);
942    }
943
944    pub fn take_late_events(&self) -> Vec<LateSessionEvent> {
945        self.write_late_events().drain(..).collect()
946    }
947
948    #[cfg(not(loom))]
949    pub fn queue_tool_server_event(&self, event: ToolServerEvent) {
950        match event {
951            ToolServerEvent::ElicitationCompleted { elicitation_id } => {
952                let Some(pending) = self
953                    .write_pending_url_elicitations()
954                    .remove(&elicitation_id)
955                else {
956                    return;
957                };
958                self.queue_late_event(LateSessionEvent::ElicitationCompleted {
959                    elicitation_id,
960                    related_task_id: pending.related_task_id,
961                });
962            }
963            ToolServerEvent::ResourceUpdated { uri } => {
964                if self.is_resource_subscribed(&uri) {
965                    self.queue_late_event(LateSessionEvent::ResourceUpdated { uri });
966                }
967            }
968            ToolServerEvent::ResourcesListChanged => {
969                self.queue_late_event(LateSessionEvent::ResourcesListChanged);
970            }
971            ToolServerEvent::ToolsListChanged => {
972                self.queue_late_event(LateSessionEvent::ToolsListChanged);
973            }
974            ToolServerEvent::PromptsListChanged => {
975                self.queue_late_event(LateSessionEvent::PromptsListChanged);
976            }
977        }
978    }
979
980    pub fn queue_elicitation_completion(&self, elicitation_id: &str) {
981        let Some(pending) = self.write_pending_url_elicitations().remove(elicitation_id) else {
982            return;
983        };
984        self.queue_late_event(LateSessionEvent::ElicitationCompleted {
985            elicitation_id: elicitation_id.to_string(),
986            related_task_id: pending.related_task_id,
987        });
988    }
989
990    pub fn subscribe_resource(&self, uri: impl Into<String>) {
991        self.subscriptions.subscribe_resource(uri);
992    }
993
994    pub fn unsubscribe_resource(&self, uri: &str) {
995        self.subscriptions.unsubscribe_resource(uri);
996    }
997
998    pub fn is_resource_subscribed(&self, uri: &str) -> bool {
999        self.subscriptions.contains_resource(uri)
1000    }
1001
1002    pub fn set_auth_context(
1003        &self,
1004        auth_context: SessionAuthContext,
1005    ) -> (bool, SessionAnchorSnapshot, Option<String>) {
1006        self.auth_state.replace_with(|current| {
1007            let rotated = current.auth_context != auth_context;
1008            if rotated {
1009                let previous_anchor_id = current.session_anchor.id().to_string();
1010                let next_epoch = current.session_anchor.auth_epoch.saturating_add(1);
1011                let session_anchor = SessionAnchorState::new(&self.id, &auth_context, next_epoch);
1012                let snapshot = SessionAnchorSnapshot {
1013                    session_id: self.id.clone(),
1014                    agent_id: self.agent_id.clone(),
1015                    auth_context: auth_context.clone(),
1016                    session_anchor: session_anchor.clone(),
1017                };
1018                (
1019                    Some(SessionAuthState {
1020                        auth_context,
1021                        session_anchor,
1022                    }),
1023                    (true, snapshot, Some(previous_anchor_id)),
1024                )
1025            } else {
1026                (
1027                    None,
1028                    (
1029                        false,
1030                        SessionAnchorSnapshot {
1031                            session_id: self.id.clone(),
1032                            agent_id: self.agent_id.clone(),
1033                            auth_context: current.auth_context.clone(),
1034                            session_anchor: current.session_anchor.clone(),
1035                        },
1036                        None,
1037                    ),
1038                )
1039            }
1040        })
1041    }
1042
1043    pub fn set_auth_context_persisted<E>(
1044        &self,
1045        auth_context: SessionAuthContext,
1046        persist: impl FnOnce(&SessionAnchorSnapshot, Option<&str>) -> Result<(), E>,
1047    ) -> Result<(), SessionPersistError<E>> {
1048        let state_guard = self.write_inner();
1049        if state_guard.state == SessionState::Closed {
1050            return Err(SessionPersistError::Session(
1051                SessionError::OperationNotAllowed {
1052                    session_id: self.id.clone(),
1053                    operation: "set_auth_context",
1054                    state: state_guard.state.as_str(),
1055                },
1056            ));
1057        }
1058
1059        self.auth_state.replace_with(|current| {
1060            let rotated = current.auth_context != auth_context;
1061            let (next, snapshot, supersedes_anchor_id) = if rotated {
1062                let previous_anchor_id = current.session_anchor.id().to_string();
1063                let next_epoch = current.session_anchor.auth_epoch.saturating_add(1);
1064                let session_anchor = SessionAnchorState::new(&self.id, &auth_context, next_epoch);
1065                let snapshot = SessionAnchorSnapshot {
1066                    session_id: self.id.clone(),
1067                    agent_id: self.agent_id.clone(),
1068                    auth_context: auth_context.clone(),
1069                    session_anchor: session_anchor.clone(),
1070                };
1071                (
1072                    Some(SessionAuthState {
1073                        auth_context,
1074                        session_anchor,
1075                    }),
1076                    snapshot,
1077                    Some(previous_anchor_id),
1078                )
1079            } else {
1080                (
1081                    None,
1082                    SessionAnchorSnapshot {
1083                        session_id: self.id.clone(),
1084                        agent_id: self.agent_id.clone(),
1085                        auth_context: current.auth_context.clone(),
1086                        session_anchor: current.session_anchor.clone(),
1087                    },
1088                    None,
1089                )
1090            };
1091
1092            let result = persist(&snapshot, supersedes_anchor_id.as_deref());
1093            match result {
1094                Ok(()) => (next, Ok(())),
1095                Err(error) => (None, Err(SessionPersistError::Persist(error))),
1096            }
1097        })
1098    }
1099
1100    pub fn set_peer_capabilities(&self, peer_capabilities: PeerCapabilities) {
1101        self.peer_capabilities.replace(peer_capabilities);
1102    }
1103
1104    pub fn replace_roots(&self, roots: Vec<RootDefinition>) {
1105        let normalized_roots = roots
1106            .iter()
1107            .map(RootDefinition::normalize_for_runtime)
1108            .collect();
1109        self.roots.replace(SessionRoots {
1110            roots,
1111            normalized_roots,
1112        });
1113    }
1114
1115    pub fn activate(&self) -> Result<(), SessionError> {
1116        self.transition(SessionState::Ready)
1117    }
1118
1119    pub fn begin_draining(&self) -> Result<(), SessionError> {
1120        self.transition(SessionState::Draining)
1121    }
1122
1123    pub fn close(&self) -> Result<(), SessionError> {
1124        {
1125            let mut inner = self.write_inner();
1126            if inner.state == SessionState::Closed {
1127                return Ok(());
1128            }
1129
1130            let active_count = self.inflight.len() as u64;
1131            if active_count > 0 {
1132                if inner.state != SessionState::Closed {
1133                    inner.state = SessionState::Draining;
1134                }
1135                return Err(SessionError::CloseRequiresDrain {
1136                    session_id: self.id.clone(),
1137                    active_count,
1138                });
1139            }
1140
1141            inner.state = SessionState::Closed;
1142        }
1143
1144        self.inflight.clear();
1145        self.subscriptions.clear();
1146        self.auth_state.replace_with(|current| {
1147            let auth_context = SessionAuthContext::in_process_anonymous();
1148            let next_epoch = current.session_anchor.auth_epoch.saturating_add(1);
1149            let session_anchor = SessionAnchorState::new(&self.id, &auth_context, next_epoch);
1150            (
1151                Some(SessionAuthState {
1152                    auth_context,
1153                    session_anchor,
1154                }),
1155                (),
1156            )
1157        });
1158        self.roots.replace(SessionRoots {
1159            roots: Vec::new(),
1160            normalized_roots: Vec::new(),
1161        });
1162        self.write_pending_url_elicitations().clear();
1163        self.write_late_events().clear();
1164        Ok(())
1165    }
1166
1167    pub fn close_persisted<E>(
1168        &self,
1169        persist: impl FnOnce(&SessionAnchorSnapshot, Option<&str>) -> Result<(), E>,
1170    ) -> Result<(), SessionPersistError<E>> {
1171        let mut inner = self.write_inner();
1172        if inner.state == SessionState::Closed {
1173            return Ok(());
1174        }
1175
1176        let active_count = self.inflight.len() as u64;
1177        if active_count > 0 {
1178            if inner.state != SessionState::Closed {
1179                inner.state = SessionState::Draining;
1180            }
1181            return Err(SessionPersistError::Session(
1182                SessionError::CloseRequiresDrain {
1183                    session_id: self.id.clone(),
1184                    active_count,
1185                },
1186            ));
1187        }
1188
1189        self.auth_state.replace_with(|current| {
1190            let auth_context = SessionAuthContext::in_process_anonymous();
1191            let previous_anchor_id = current.session_anchor.id().to_string();
1192            let next_epoch = current.session_anchor.auth_epoch.saturating_add(1);
1193            let session_anchor = SessionAnchorState::new(&self.id, &auth_context, next_epoch);
1194            let snapshot = SessionAnchorSnapshot {
1195                session_id: self.id.clone(),
1196                agent_id: self.agent_id.clone(),
1197                auth_context: auth_context.clone(),
1198                session_anchor: session_anchor.clone(),
1199            };
1200            let result = persist(&snapshot, Some(previous_anchor_id.as_str()));
1201            match result {
1202                Ok(()) => (
1203                    Some(SessionAuthState {
1204                        auth_context,
1205                        session_anchor,
1206                    }),
1207                    Ok(()),
1208                ),
1209                Err(error) => (None, Err(SessionPersistError::Persist(error))),
1210            }
1211        })?;
1212
1213        inner.state = SessionState::Closed;
1214        drop(inner);
1215
1216        self.inflight.clear();
1217        self.subscriptions.clear();
1218        self.roots.replace(SessionRoots {
1219            roots: Vec::new(),
1220            normalized_roots: Vec::new(),
1221        });
1222        self.write_pending_url_elicitations().clear();
1223        self.write_late_events().clear();
1224        Ok(())
1225    }
1226
1227    pub fn ensure_operation_allowed(&self, operation: OperationKind) -> Result<(), SessionError> {
1228        let state = self.state();
1229        let allowed = operation_allowed_for_state(state, operation);
1230
1231        if allowed {
1232            Ok(())
1233        } else {
1234            Err(SessionError::OperationNotAllowed {
1235                session_id: self.id.clone(),
1236                operation: operation.as_str(),
1237                state: state.as_str(),
1238            })
1239        }
1240    }
1241
1242    pub fn track_request(
1243        &self,
1244        context: &OperationContext,
1245        operation_kind: OperationKind,
1246        cancellable: bool,
1247    ) -> Result<SessionRequestStart, SessionError> {
1248        self.validate_context(context)?;
1249
1250        let state_guard = self.read_inner();
1251        let state = state_guard.state;
1252        if !operation_allowed_for_state(state, operation_kind) {
1253            return Err(SessionError::OperationNotAllowed {
1254                session_id: self.id.clone(),
1255                operation: operation_kind.as_str(),
1256                state: state.as_str(),
1257            });
1258        }
1259
1260        let start = self.auth_state.with_current(|auth_state| {
1261            let session_snapshot = SessionAnchorSnapshot {
1262                session_id: self.id.clone(),
1263                agent_id: self.agent_id.clone(),
1264                auth_context: auth_state.auth_context.clone(),
1265                session_anchor: auth_state.session_anchor.clone(),
1266            };
1267            let mut requests = self.inflight.write_requests();
1268            let mut request_lineage = self.write_request_lineage();
1269            if requests.contains_key(&context.request_id) {
1270                return Err(SessionError::DuplicateInflightRequest {
1271                    request_id: context.request_id.clone(),
1272                });
1273            }
1274            if let Some(parent_request_id) = &context.parent_request_id {
1275                validate_parent_request_lineage_locked(
1276                    &context.request_id,
1277                    parent_request_id,
1278                    &requests,
1279                    &request_lineage,
1280                    auth_state.session_anchor.id(),
1281                )?;
1282            }
1283            if request_lineage.contains_key(&context.request_id) {
1284                return Err(SessionError::DuplicateRequestLineage {
1285                    request_id: context.request_id.clone(),
1286                });
1287            }
1288            self.inflight.track_locked(
1289                &mut requests,
1290                context,
1291                operation_kind,
1292                auth_state.session_anchor.id(),
1293                cancellable,
1294            )?;
1295            let lineage = RequestLineageRecord {
1296                request_id: context.request_id.clone(),
1297                session_anchor_id: auth_state.session_anchor.id().to_string(),
1298                auth_epoch: auth_state.session_anchor.auth_epoch(),
1299                parent_request_id: context.parent_request_id.clone(),
1300                operation_kind,
1301                started_at: current_unix_timestamp(),
1302                terminal_state: None,
1303            };
1304            request_lineage.insert(context.request_id.clone(), lineage.clone());
1305            Ok(SessionRequestStart {
1306                session: session_snapshot,
1307                lineage,
1308            })
1309        })?;
1310        drop(state_guard);
1311        Ok(start)
1312    }
1313
1314    pub fn complete_request(
1315        &self,
1316        request_id: &RequestId,
1317    ) -> Result<InflightRequest, SessionError> {
1318        self.complete_request_with_terminal_state(request_id, OperationTerminalState::Completed)
1319    }
1320
1321    pub fn complete_request_with_terminal_state(
1322        &self,
1323        request_id: &RequestId,
1324        terminal_state: OperationTerminalState,
1325    ) -> Result<InflightRequest, SessionError> {
1326        let inflight = self.inflight.complete(request_id)?;
1327        self.mark_request_terminal(request_id, terminal_state);
1328        Ok(inflight)
1329    }
1330
1331    pub fn discard_unpersisted_request_start(&self, request_id: &RequestId) {
1332        let _ = self.inflight.complete(request_id);
1333        self.write_request_lineage().remove(request_id);
1334        self.terminal.remove(request_id);
1335    }
1336
1337    fn mark_request_terminal(
1338        &self,
1339        request_id: &RequestId,
1340        terminal_state: OperationTerminalState,
1341    ) {
1342        let recorded = self
1343            .terminal
1344            .record(request_id.clone(), terminal_state.clone());
1345        if recorded {
1346            if let Some(lineage) = self.write_request_lineage().get_mut(request_id) {
1347                lineage.terminal_state = Some(terminal_state);
1348            }
1349        }
1350    }
1351
1352    pub fn request_cancellation(&self, request_id: &RequestId) -> Result<(), SessionError> {
1353        self.inflight.mark_cancellation_requested(request_id)
1354    }
1355
1356    pub fn validate_parent_request_lineage(
1357        &self,
1358        request_id: &RequestId,
1359        parent_request_id: &RequestId,
1360    ) -> Result<RequestLineageRecord, SessionError> {
1361        self.auth_state.with_current(|auth_state| {
1362            let requests = self.inflight.read_requests();
1363            let request_lineage = self.read_request_lineage();
1364            validate_parent_request_lineage_locked(
1365                request_id,
1366                parent_request_id,
1367                &requests,
1368                &request_lineage,
1369                auth_state.session_anchor.id(),
1370            )
1371        })
1372    }
1373
1374    fn transition(&self, next: SessionState) -> Result<(), SessionError> {
1375        let mut inner = self.write_inner();
1376        let valid = match (inner.state, next) {
1377            (SessionState::Initializing, SessionState::Ready)
1378            | (SessionState::Initializing, SessionState::Closed)
1379            | (SessionState::Ready, SessionState::Draining)
1380            | (SessionState::Ready, SessionState::Closed)
1381            | (SessionState::Draining, SessionState::Closed) => true,
1382            _ if inner.state == next => true,
1383            _ => false,
1384        };
1385
1386        if !valid {
1387            return Err(SessionError::InvalidTransition {
1388                from: inner.state.as_str(),
1389                to: next.as_str(),
1390            });
1391        }
1392
1393        inner.state = next;
1394        Ok(())
1395    }
1396
1397    pub fn validate_context(&self, context: &OperationContext) -> Result<(), SessionError> {
1398        if context.session_id != self.id {
1399            return Err(SessionError::ContextSessionMismatch {
1400                expected: self.id.clone(),
1401                actual: context.session_id.clone(),
1402            });
1403        }
1404
1405        if context.agent_id != self.agent_id {
1406            return Err(SessionError::ContextAgentMismatch {
1407                expected: self.agent_id.clone(),
1408                actual: context.agent_id.clone(),
1409            });
1410        }
1411
1412        Ok(())
1413    }
1414}
1415
1416fn current_unix_timestamp() -> u64 {
1417    SystemTime::now()
1418        .duration_since(UNIX_EPOCH)
1419        .map(|duration| duration.as_secs())
1420        .unwrap_or(0)
1421}
1422
1423fn auth_context_hash(auth_context: &SessionAuthContext) -> String {
1424    canonical_json_bytes(auth_context)
1425        .map(|bytes| sha256_hex(&bytes))
1426        .unwrap_or_else(|_| "session-auth-context-hash-unavailable".to_string())
1427}
1428
1429/// Session-aware kernel response, decoupled from the current wire protocol.
1430#[cfg(not(loom))]
1431#[derive(Debug)]
1432pub enum SessionOperationResponse {
1433    ToolCall(ToolCallResponse),
1434    RootList {
1435        roots: Vec<RootDefinition>,
1436    },
1437    ResourceList {
1438        resources: Vec<ResourceDefinition>,
1439    },
1440    ResourceRead {
1441        contents: Vec<ResourceContent>,
1442    },
1443    ResourceReadDenied {
1444        receipt: ChioReceipt,
1445    },
1446    ResourceTemplateList {
1447        templates: Vec<ResourceTemplateDefinition>,
1448    },
1449    PromptList {
1450        prompts: Vec<PromptDefinition>,
1451    },
1452    PromptGet {
1453        prompt: PromptResult,
1454    },
1455    Completion {
1456        completion: CompletionResult,
1457    },
1458    CapabilityList {
1459        capabilities: Vec<CapabilityToken>,
1460    },
1461    Heartbeat,
1462}
1463
1464#[cfg(test)]
1465#[allow(clippy::expect_used, clippy::unwrap_used)]
1466mod tests;