pub struct OperatorSessionRecord {
pub sid: SessionId,
pub token_digest: String,
pub capability_manifest: Option<AgentProviderManifest>,
pub joined_at_secs: u64,
pub last_access_secs: u64,
pub desc: Option<String>,
pub observed: Vec<ObservedAssignment>,
pub observed_total: u64,
}Expand description
One persisted Operator login-flow session.
Field-for-field the durable subset of the server’s LoginSession —
everything except the process-lifetime WS adapter state, which is
rebuilt empty on reconnect.
§The bearer token is never stored
token_digest holds
hex(SHA-256(bearer)) — the same fingerprint shape the /v1/sessions
path already keys its store by, for the same reason (“the sid handed to
the client is the token nonce itself (a bearer secret), so the server
never uses it as a map key”; see mse_server::SessionStore). Every
consumer of this record only ever compares a presented bearer
(verify_bearer), so nothing downstream needs
the plaintext — it exists only inside POST /v1/operators, between
minting and the mint response.
Fields§
§sid: SessionIdServer-minted session id (S-<hex>).
token_digest: Stringhex(SHA-256(bearer)) of the auth token required on the WS upgrade
and admin routes. Derive with Self::digest_of; compare with
Self::verify_bearer. The plaintext bearer is deliberately absent
(see the type doc).
capability_manifest: Option<AgentProviderManifest>Provider-owned effective capability manifest submitted at join.
joined_at_secs: u64Unix epoch seconds when POST /v1/operators minted this session.
last_access_secs: u64Unix epoch seconds when this session was last accessed — model
§4.1’s 最終アクセス, the clock the 24h expiry
(OPERATOR_SESSION_MAX_IDLE_SECS) runs from.
§Access, not activity
Self::last_activity_secs answers “when was this session last
assigned something”, which is what D5 sorts the 記名 list by.
This answers “when did the driver behind this session last show
itself”, which is a wider set of events: attaching a WebSocket,
reading its own session, being assigned a seat. A driver can be very
much alive and hold no seat for a day, so expiring on activity would
reap live sessions.
§Why this one is stored and its sibling is derived
last_activity_secs is a maximum over the observed ring, so it
cannot go stale — every value it reads from is already persisted.
An access leaves no such trace: nothing about a WS connect or a
GET /v1/operators/:sid is written down anywhere else, so if this
were derived there would be nothing to derive it from. It is
advanced by Self::touch and written through by the server.
Additive with #[serde(default)]. A row persisted before this field
existed decodes as 0, which would read as “accessed at the epoch”
and expire it on sight — so every reader goes through
Self::last_access_secs, which folds 0 back onto the join time.
desc: Option<String>The confirmed part of this session’s 記名 (model §4.2, D1): roughly 50 characters the joining AI wrote about what it is working on, fixed at join and never rewritten afterwards.
It is what the observed part cannot supply. Two drivers in the same
worktree produce the same project_root / work_dir and can hold
Runs of the same Blueprint; the sentence one of them wrote at join
exists only in that conversation, which is what makes it an
identifier (§4.2: 観測部分だけでは足りない).
None = the session joined without one. Kept as an absence rather
than an empty string so a reader can tell “nothing was written” from
“something was written and it was blank” — POST /v1/operators does
not reject a missing desc (unlike A9 on the assignment side,
D1-D5 name no 400), so the absence is a real and readable
state.
D4: nothing matches on this. It is read by humans and AIs to tell sessions apart, never by the server to decide identity.
Additive with #[serde(default)] — rows persisted before the 記名
existed decode as None.
observed: Vec<ObservedAssignment>The observed part of this session’s 記名 (model §4.2, D2):
one entry per seat this session was assigned, appended by the server
at each Assign and never removed by any API.
Oldest first. Bounded by OBSERVED_CAP and de-duplicated per
(run_id, slot) — see Self::record_observed.
Additive with #[serde(default)].
observed_total: u64How many Assigns have been recorded onto Self::observed over
this session’s life, including the ones the ring has since dropped
and the re-assignments folded into an existing entry.
Monotone. observed_total > observed.len() is the visible signal
that the reader is looking at a window rather than the whole
history.
Additive with #[serde(default)].
Implementations§
Source§impl OperatorSessionRecord
impl OperatorSessionRecord
Sourcepub fn record_observed(&mut self, entry: ObservedAssignment)
pub fn record_observed(&mut self, entry: ObservedAssignment)
Append one Assign to the observed part (D2).
Two shaping rules, both about keeping the log readable rather than about deleting anything:
- One entry per
(run_id, slot). Re-acquiring a seat this session already holds is the same fact with a newer timestamp, so the existing entry is replaced and moved to the newest position instead of accumulating a row per acquire. A driver that re-acquires after every reconnect would otherwise fill the whole window with one Run. - Newest
OBSERVED_CAPkept. Past the cap the oldest entry is dropped.
Self::observed_total counts every call regardless, so a reader
can tell that folding or dropping happened.
Sourcepub fn last_activity_secs(&self) -> u64
pub fn last_activity_secs(&self) -> u64
When this session was last seen doing something — the newest
ObservedAssignment::at_secs, or Self::joined_at_secs for a
session that has never been assigned anything.
D5’s default ordering key. Derived rather than stored: a separate column would be a second thing to keep in step with the log, and the ring only ever drops entries older than the newest one, so the derivation cannot go stale.
Sourcepub fn last_access_secs(&self) -> u64
pub fn last_access_secs(&self) -> u64
When this session was last accessed, for the 24h expiry clock.
Reads Self::last_access_secs, with two foldings that make the
value safe to compare against a horizon:
- a
0(a row persisted before the field existed, or a session never touched since it was minted) reads as the join time, so a fresh session is never a day old on arrival; - an assignment counts as an access even if nothing touched the
field, so
Self::last_activity_secsis folded in as well. A session being handed seats is being used, whatever else it does.
Sourcepub fn touch(&mut self, now: u64) -> bool
pub fn touch(&mut self, now: u64) -> bool
Advance Self::last_access_secs to now, never backwards.
Monotone because the clock is not: a SystemTime that steps back
(NTP correction, a suspended laptop) must not make a session look
older than the last time something saw it. Returns whether the value
moved, so a caller can skip a durable write that would change
nothing.
Sourcepub fn is_expired_at(&self, now: u64) -> bool
pub fn is_expired_at(&self, now: u64) -> bool
The 24h horizon: has this session gone
OPERATOR_SESSION_MAX_IDLE_SECS without being accessed, as of
now?
A pure predicate over the record. What it cannot see is whether a
socket is attached right now, which is why the server’s expiry
checks pair it with a connectivity read — a driver holding an idle
WebSocket open is present, and reaping it would be the reaper
causing the outage it exists to prevent. See
mse_server::operator_ws::login’s expiry note.
Sourcepub fn digest_of(bearer: &str) -> String
pub fn digest_of(bearer: &str) -> String
Digest a plaintext bearer into the at-rest shape
(Self::token_digest).
Callers mint a bearer with
operator_bearer_token and
keep the plaintext only long enough to answer the mint request.
Sourcepub fn verify_bearer(&self, bearer: &str) -> bool
pub fn verify_bearer(&self, bearer: &str) -> bool
Constant-time check of a presented bearer against
Self::token_digest.
The comparison runs over the two digests (fixed-width hex), so it carries no timing signal about the bearer itself.
Trait Implementations§
Source§impl Clone for OperatorSessionRecord
impl Clone for OperatorSessionRecord
Source§fn clone(&self) -> OperatorSessionRecord
fn clone(&self) -> OperatorSessionRecord
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for OperatorSessionRecord
impl Debug for OperatorSessionRecord
Source§impl<'de> Deserialize<'de> for OperatorSessionRecord
impl<'de> Deserialize<'de> for OperatorSessionRecord
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl PartialEq for OperatorSessionRecord
impl PartialEq for OperatorSessionRecord
Source§impl Serialize for OperatorSessionRecord
impl Serialize for OperatorSessionRecord
impl StructuralPartialEq for OperatorSessionRecord
Auto Trait Implementations§
impl Freeze for OperatorSessionRecord
impl RefUnwindSafe for OperatorSessionRecord
impl Send for OperatorSessionRecord
impl Sync for OperatorSessionRecord
impl Unpin for OperatorSessionRecord
impl UnsafeUnpin for OperatorSessionRecord
impl UnwindSafe for OperatorSessionRecord
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more